왜 C ++에서 중첩 클래스를 사용합니까?


188

누군가가 중첩 클래스를 이해하고 사용하는 데 도움이되는 멋진 자료를 알려 주시겠습니까? Programming Principles와 같은 자료와이 IBM Knowledge Center-Nested Classes 와 같은 자료가 있습니다.

그러나 나는 여전히 그들의 목적을 이해하는 데 어려움을 겪고 있습니다. 누군가 제발 도와주세요?


15
C ++에서 중첩 클래스에 대한 조언은 단순히 중첩 클래스를 사용하지 않는 것입니다.
Billy ONeal

7
중첩 된 것을 제외하고는 정확히 일반 클래스와 같습니다. 클래스의 내부 구현이 너무 복잡하여 여러 개의 작은 클래스로 가장 쉽게 모델링 할 수있는 경우에 사용하십시오.
meagar

12
@ 빌리 : 왜? 지나치게 넓은 것 같습니다.
John

30
중첩 클래스가 그 특성상 나쁜 이유에 대해서는 여전히 논쟁의 여지가 없습니다.
John Dibling

7
@ 7vies : 1. 단순히 필요하지 않기 때문에-외부 적으로 정의 된 클래스를 사용하여 동일한 작업을 수행 할 수 있습니다. 이렇게하면 주어진 변수의 범위가 줄어 듭니다. 2. 중첩 클래스로 할 수있는 모든 것을 할 수 있기 때문입니다 typedef. 3. 긴 줄을 피하는 것이 어려운 환경에서 추가 들여 쓰기 수준을 추가하기 때문입니다. 4. 단일 class선언 등으로 개념적으로 분리 된 두 개의 객체를 선언하기 때문입니다 .
Billy ONeal

답변:


229

중첩 클래스는 구현 세부 정보를 숨기는 데 유용합니다.

명부:

class List
{
    public:
        List(): head(nullptr), tail(nullptr) {}
    private:
        class Node
        {
              public:
                  int   data;
                  Node* next;
                  Node* prev;
        };
    private:
        Node*     head;
        Node*     tail;
};

다른 사람들이 클래스를 사용하기로 결정할 수 있으므로 Node를 노출하고 싶지 않으며 노출 된 것이 공개 API의 일부이므로 영구적 으로 유지 관리해야하기 때문에 클래스를 업데이트하지 못하게 됩니다 . 클래스를 비공개로 설정하면 구현을 숨길뿐만 아니라 이것이 내 것이라고 말하고 언제든지 사용할 수 없도록 변경할 수 있습니다.

std::list또는 std::map그들은 모두 숨겨진 클래스를 포함 (또는합니까?). 요점은 그럴 수도 있고 아닐 수도 있지만 구현이 비공개이며 숨겨져 있기 때문에 STL 빌더는 코드 사용 방법에 영향을주지 않고 STL 주위에 많은 오래된 수하물을 남기지 않고 코드를 업데이트 할 수있었습니다. 안에 숨겨져있는 Node 클래스를 사용하기로 결정한 일부 바보와의 호환성을 유지하기 위해 list.


9
이 작업을 수행하는 경우 Node헤더 파일에 전혀 노출되어서는 안됩니다.
Billy ONeal

6
@ 빌리 ONeal : STL 또는 부스트와 같은 헤더 파일 구현을 수행하는 경우 어떻게됩니까?
Martin York

6
@Billy ONeal : 아닙니다. 좋은 디자인의 문제는 의견이 아닙니다. 네임 스페이스에 넣어도 사용되지 않습니다. 이제 영구성을 유지 관리해야하는 공용 API의 일부입니다.
Martin York

21
@Billy ONeal : 우발적 인 사용으로부터 보호합니다. 또한 개인 소유이며 사용해서는 안된다는 사실을 문서화합니다 (어리석은 일을하지 않으면 사용할 수 없음). 따라서 지원하지 않아도됩니다. 네임 스페이스에 넣으면 공개 API의 일부가됩니다 (이 대화에서 누락 된 부분입니다. 공개 API는이를 지원해야 함을 의미합니다).
Martin York

10
@Billy ONeal : 중첩 클래스는 중첩 네임 스페이스에 비해 몇 가지 장점이 있습니다. 네임 스페이스의 인스턴스는 만들 수 없지만 클래스의 인스턴스는 만들 수 있습니다. detail컨벤션에 관해서 : 대신 그러한 컨벤션에 따라 스스로를 염두에 두어야합니다. 컴파일러에 의존하는 것이 좋습니다.
SasQ

142

중첩 클래스는 일반 클래스와 비슷하지만

  • (클래스 정의 내부의 모든 정의와 마찬가지로) 추가 액세스 제한이 있습니다.
  • 그들은 주어진 네임 스페이스를 오염시키지 않는 글로벌 네임 스페이스를 예. 클래스 B가 클래스 A에 너무 깊게 연결되어 있다고 생각하지만 A와 B의 객체가 반드시 관련이있는 것은 아니라면 A 클래스 범위 지정을 통해서만 클래스 B에 액세스 할 수 있기를 원할 수 있습니다 (A라고 함). ::수업).

몇 가지 예 :

클래스를 공개적으로 중첩하여 관련 클래스의 범위에 배치


class의 SomeSpecificCollection객체를 집계 하는 클래스를 원한다고 가정하십시오 Element. 그러면 다음 중 하나를 수행 할 수 있습니다.

  1. 두 개의 클래스를 선언합니다 : SomeSpecificCollection그리고 Element- "Element"라는 이름이 이름 충돌을 일으킬 정도로 일반적이기 때문에 나쁩니다.

  2. 네임 스페이스를 소개 someSpecificCollection하고 클래스를 선언 someSpecificCollection::Collection하고 someSpecificCollection::Element. 이름 충돌의 위험은 없지만 더 자세한 정보를 얻을 수 있습니까?

  3. 이 개 글로벌 클래스 선언 SomeSpecificCollectionSomeSpecificCollectionElement- 사소한 단점이 있지만, 아마 OK이다.

  4. 전역 클래스 SomeSpecificCollection와 클래스 Element를 중첩 클래스로 선언하십시오 . 그때:

    • Element가 전역 네임 스페이스에 없기 때문에 이름 충돌의 위험이 없습니다.
    • SomeSpecificCollection당신 을 구현할 때 just Element, 그리고 다른 곳 SomeSpecificCollection::Element은-3과 같지만 +와 동일하지만 더 명확합니다.
    • "컬렉션의 특정 요소"가 아니라 "특정 컬렉션의 요소"라는 것이 단순 해집니다.
    • 그 볼 SomeSpecificCollection도 클래스입니다.

제 생각에는 마지막 변형이 가장 직관적이며 따라서 최고의 디자인입니다.

스트레스를 드리겠습니다-더 자세한 이름을 가진 두 개의 글로벌 클래스를 만드는 것과 큰 차이는 없습니다. 아주 작은 세부 사항이지만 코드를 더 명확하게 만듭니다.

클래스 범위 내에서 다른 범위 소개


이것은 typedef 또는 enum을 도입 할 때 특히 유용합니다. 여기에 코드 예제를 게시하겠습니다.

class Product {
public:
    enum ProductType {
        FANCY, AWESOME, USEFUL
    };
    enum ProductBoxType {
        BOX, BAG, CRATE
    };
    Product(ProductType t, ProductBoxType b, String name);

    // the rest of the class: fields, methods
};

그런 다음 다음을 호출합니다.

Product p(Product::FANCY, Product::BOX);

그러나에 대한 코드 완성 제안을 살펴보면 Product::가능한 모든 열거 형 값 (BOX, FANCY, CRATE)이 나열되어 있고 여기에서 실수하기 쉽습니다 (C ++ 0x의 강력한 형식의 열거 형은 해결하지만 결코 신경 쓰지 않습니다) ).

그러나 중첩 클래스를 사용하여 열거 형에 대한 추가 범위를 도입하면 다음과 같이 보일 수 있습니다.

class Product {
public:
    struct ProductType {
        enum Enum { FANCY, AWESOME, USEFUL };
    };
    struct ProductBoxType {
        enum Enum { BOX, BAG, CRATE };
    };
    Product(ProductType::Enum t, ProductBoxType::Enum b, String name);

    // the rest of the class: fields, methods
};

그런 다음 호출은 다음과 같습니다.

Product p(Product::ProductType::FANCY, Product::ProductBoxType::BOX);

그런 다음 Product::ProductType::IDE 에 입력 하면 원하는 범위에서 열거 형을 얻을 수 있습니다. 또한 실수 할 위험이 줄어 듭니다.

물론 이것은 작은 클래스에는 필요하지 않지만 열거 형이 많으면 클라이언트 프로그래머가 더 쉽게 만들 수 있습니다.

같은 방법으로, 필요한 경우 템플릿에 많은 타입 정의를 "구성"할 수 있습니다. 때로는 유용한 패턴입니다.

PIMPL 관용구


PIMPL (Pointer to IMPLementation의 줄임말)은 헤더에서 클래스의 구현 세부 사항을 제거하는 데 유용한 관용구입니다. 이렇게하면 헤더의 "구현"부분이 변경 될 때마다 클래스 헤더에 따라 클래스를 다시 컴파일 할 필요가 줄어 듭니다.

일반적으로 중첩 클래스를 사용하여 구현됩니다.

Xh :

class X {
public:
    X();
    virtual ~X();
    void publicInterface();
    void publicInterface2();
private:
    struct Impl;
    std::unique_ptr<Impl> impl;
}

X.cpp :

#include "X.h"
#include <windows.h>

struct X::Impl {
    HWND hWnd; // this field is a part of the class, but no need to include windows.h in header
    // all private fields, methods go here

    void privateMethod(HWND wnd);
    void privateMethod();
};

X::X() : impl(new Impl()) {
    // ...
}

// and the rest of definitions go here

이것은 전체 클래스 정의가 무겁거나 못생긴 헤더 파일 (WinAPI 사용)이있는 일부 외부 라이브러리의 유형 정의가 필요한 경우에 특히 유용합니다. PIMPL을 사용하는 경우 모든 WinAPI 관련 기능 만에 .cpp포함시킬 수 있으며에 포함시킬 수 없습니다 .h.


3
struct Impl; std::auto_ptr<Impl> impl; 이 오류는 Herb Sutter에 의해 대중화되었습니다. 불완전한 유형에는 auto_ptr을 사용하지 말거나 잘못된 코드가 생성되지 않도록주의해야합니다.
Gene Bushuyev

2
@Billy ONeal : 내가 아는 auto_ptr한 대부분의 구현에서 불완전한 유형을 선언 할 수 는 있지만 기술적으로 C ++ 0x의 일부 템플릿과 달리 UB입니다 (예 : unique_ptr템플릿 매개 변수는 불완전한 유형 및 유형이 정확히 완료되어야하는 위치 (예 : 사용 ~unique_ptr)
CB 베일리

2
@Billy ONeal : C ++ 03 17.4.6.3에서 [lib.res.on.functions]는 "특히 다음과 같은 경우에는 효과가 정의되지 않습니다. [...] 불완전한 형식이 템플릿 인수로 사용되는 경우 템플릿 구성 요소를 인스턴스화 할 때 " 반면 C ++ 0x에서는 "구성 요소에 대해 특별히 허용되지 않는 한 템플리트 구성 요소를 인스턴스화 할 때 불완전한 유형이 템플리트 인수로 사용되는 경우"라고 표시됩니다. 이상 (예 : "의 템플릿 매개 변수 Tunique_ptr불완전한 유형일 수 있습니다.")
CB Bailey

1
@MilesRout 너무 일반적인 방법입니다. 클라이언트 코드의 상속 가능 여부에 따라 다릅니다. 규칙 : 기본 클래스 포인터를 통해 삭제하지 않을 경우 가상 dtor는 완전히 중복됩니다.
Kos

2
@IsaacPascual aww, 나는 우리가 지금 그것을 업데이트해야합니다 enum class.
Kos

21

중첩 클래스를 많이 사용하지 않지만 지금은 사용합니다. 특히 일종의 데이터 유형을 정의 할 때 해당 데이터 유형에 맞게 설계된 STL 펑터를 정의하려고합니다.

예를 들어, FieldID 번호, 유형 코드 및 필드 이름이 있는 일반 클래스를 고려하십시오 . ID 번호 또는 이름 vector으로 이들 Field중 하나 를 검색 하려면 functor를 구성하여 그렇게 할 수 있습니다.

class Field
{
public:
  unsigned id_;
  string name_;
  unsigned type_;

  class match : public std::unary_function<bool, Field>
  {
  public:
    match(const string& name) : name_(name), has_name_(true) {};
    match(unsigned id) : id_(id), has_id_(true) {};
    bool operator()(const Field& rhs) const
    {
      bool ret = true;
      if( ret && has_id_ ) ret = id_ == rhs.id_;
      if( ret && has_name_ ) ret = name_ == rhs.name_;
      return ret;
    };
    private:
      unsigned id_;
      bool has_id_;
      string name_;
      bool has_name_;
  };
};

그런 다음 이들을 검색해야하는 코드 는 클래스 자체 내에서 범위를 Field사용할 수 있습니다 .matchField

vector<Field>::const_iterator it = find_if(fields.begin(), fields.end(), Field::match("FieldName"));

STL 기능에 대해 잘 모르지만 훌륭한 예와 의견에 감사드립니다. match ()의 생성자가 공개임을 알았습니다. 나는 생성자가 항상 공개 될 필요는 없다고 가정합니다.이 경우 클래스 외부에서 인스턴스화 할 수 없습니다.
bespectacled

1
@user : STL functor의 경우 생성자가 공개되어야합니다.
John Dibling

1
@Billy : 나는 여전히 중첩 된 클래스가 나쁜 이유는 어떤 구체적인 이유를보고 아직.
John Dibling

@ 존 : 모든 코딩 스타일 가이드 라인은 의견에 달려 있습니다. 나는 몇 가지 이유를 여기 주위에 여러 의견으로 나열했는데, 그 이유는 모두 내 의견으로는 합리적이다. 코드가 유효하고 정의되지 않은 동작을 호출하지 않는 한 만들 수있는 "사실"인수는 없습니다. 그러나 여기에 넣은 코드 예제는 중첩 클래스를 피하는 큰 이유, 즉 이름 충돌을 지적한다고 생각합니다.
Billy ONeal

1
물론 매크로보다 인라인을 선호하는 기술적 이유가 있습니다 !!
Miles Rout

14

중첩 클래스를 사용하여 빌더 패턴을 구현할 수 있습니다 . 특히 C ++에서는 개인적으로 의미가 더 깨끗합니다. 예를 들면 다음과 같습니다.

class Product{
    public:
        class Builder;
}
class Product::Builder {
    // Builder Implementation
}

오히려 :

class Product {}
class ProductBuilder {}

물론, 빌드가 하나만 있으면 작동하지만 여러 콘크리트 빌더가 필요한 경우 불쾌 할 것입니다. 신중하게 디자인 결정을 내려야합니다. :)
irsis
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.