web-dev-qa-db-ja.com

なぜC ++でネストされたクラスを使用するのですか?

ネストされたクラスを理解して使用するための素敵なリソースを誰かに教えてもらえますか?プログラミング原則などの資料があります IBM Knowledge Center-ネストされたクラス

しかし、私は彼らの目的を理解するのにまだ苦労しています。誰か助けてくれませんか?

170
zengal

ネストされたクラスは、実装の詳細を隠すのに便利です。

リスト:

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の一部であり、維持する必要があるため、クラスの更新が妨げられます( forever。クラスをプライベートにすることで、実装を隠すだけでなく、これが私の実装であると言っているだけでなく、いつでも変更できるので、使用できません。

std::listまたはstd::mapを見てください。これらはすべて非表示のクラスを含んでいます(またはそれらを実行していますか?)。要点はそうであるかもしれないし、そうでないかもしれないが、実装は非公開であり、隠されているため、STLのビルダーはコードの使用方法に影響を与えずにコードを更新でき、必要なために多くの古い荷物をSTLの周りに残しておくことができましたlist内に隠されていたNodeクラスを使用したいと決めたバカとの後方互換性を維持するため。

206
Martin York

ネストされたクラスは通常のクラスに似ていますが、次のとおりです。

  • 追加のアクセス制限があります(クラス定義内のすべての定義にあります)。
  • 指定された名前空間を汚染しない、例えばグローバル名前空間。クラスBがクラスAと非常に深く結びついているが、AとBのオブジェクトが必ずしも関連していないと感じる場合は、クラスBをAクラスのスコープを介してのみアクセス可能にしたい場合があります(Aと呼ばれます) ::クラス)。

いくつかの例:

関連するクラスのスコープに配置するためのパブリックネストクラス


クラスSomeSpecificCollectionのオブジェクトを集約するクラスElementが必要だとします。その後、次のいずれかを実行できます。

  1. SomeSpecificCollectionElementの2つのクラスを宣言します-名前の衝突を引き起こすには名前「Element」が一般的であるため、不良です

  2. 名前空間someSpecificCollectionを導入し、クラスsomeSpecificCollection::CollectionおよびsomeSpecificCollection::Elementを宣言します。名前の衝突のリスクはありませんが、これ以上冗長になることはありますか?

  3. 2つのグローバルクラスSomeSpecificCollectionおよびSomeSpecificCollectionElementを宣言します。これには小さな欠点がありますが、おそらく大丈夫です。

  4. グローバルクラスSomeSpecificCollectionとクラスElementをネストされたクラスとして宣言します。次に:

    • elementはグローバル名前空間にないため、名前が衝突する危険はありません。
    • SomeSpecificCollectionの実装では、単にElementを参照し、他のすべてではSomeSpecificCollection::Elementを参照します。これは、3と同じように見えますが、より明確です。
    • 「コレクションの特定の要素」ではなく、「特定のコレクションの要素」であることは単純です
    • SomeSpecificCollectionもクラスであることがわかります。

私の意見では、最後のバリアントは間違いなく最も直感的であり、したがって最高のデザインです。

強調させてください-より冗長な名前を持つ2つのグローバルクラスを作成することと大きな違いはありません。ほんの少しの詳細ですが、コードをより明確にします。

クラススコープ内に別のスコープを導入する


これは、typedefまたは列挙を導入する場合に特に便利です。ここにコード例を投稿します。

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);

次に、IDEでProduct::ProductType::と入力すると、推奨されるスコープから列挙のみが取得されます。これにより、間違いを犯すリスクも軽減されます。

もちろん、これは小さなクラスには必要ないかもしれませんが、多くの列挙型がある場合、クライアントプログラマーにとって物事が簡単になります。

同様に、必要に応じて、テンプレートに大量のtypedefを「整理」できます。時々便利なパターンです。

PIMPLイディオム


PIMPL(Pointer to IMPLementation)は、クラスの実装の詳細をヘッダーから削除するのに便利なイディオムです。これにより、ヘッダーの「実装」部分が変更されるたびに、クラスのヘッダーに応じてクラスを再コンパイルする必要が少なくなります。

通常、ネストされたクラスを使用して実装されます。

X.h:

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

これは、完全なクラス定義が、重いまたは単なるjustいヘッダーファイル(WinAPIを使用)を持つ外部ライブラリの型の定義を必要とする場合に特に役立ちます。 PIMPLを使用する場合、WinAPI固有の機能を.cppのみで囲み、.hに含めることはできません。

136
Kos

ネストされたクラスはあまり使用しませんが、時々使用します。特に、ある種のデータ型を定義し、そのデータ型用に設計されたSTLファンクタを定義したい場合は特にそうです。

たとえば、ID番号、型コード、およびフィールド名を持つ一般的なFieldクラスを考えます。これらのvectorsのFieldをID番号または名前で検索したい場合、そうするためのファンクターを作成できます。

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_;
  };
};

次に、これらのFieldsを検索する必要があるコードは、matchクラス自体内でスコープされたFieldを使用できます。

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

ネストされたクラスでBuilderパターンを実装できます 。特にC++では、個人的には意味的にきれいだと思います。例えば:

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

のではなく:

class Product {}
class ProductBuilder {}
12
Yeo