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


188

誰かがネストされたクラスを理解して使用するためのいくつかの素晴らしいリソースに私を向けることができますか?プログラミング原則のような資料や、このIBM Knowledge Center-Nested Classesなどの資料があります。

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


15
C ++でのネストされたクラスに対する私のアドバイスは、単にネストされたクラスを使用しないことです。
Billy ONeal 2010

7
ネストされていることを除いて、通常のクラスとまったく同じです。クラスの内部実装が非常に複雑で、いくつかの小さなクラスで最も簡単にモデル化できる場合に使用します。
貧乳

12
@ビリー:なぜ?私には広すぎるようです。
John Dibling 2010

30
ネストされたクラスがその性質上なぜ悪いのか、私はまだ議論を見ていません。
John Dibling 2010

7
@ 7vies:1.必要ないため-外部で定義されたクラスでも同じことができます。これにより、特定の変数のスコープが縮小されます。これは良いことです。2.ネストされたクラスで実行できるすべてのことを実行できるためtypedef。3.あなたが単一で2つの概念的に別のオブジェクトを宣言しているので、長い行を回避することは、すでに4困難な場合、彼らは環境にインデントの追加レベルを追加するためclassなどの宣言、
ビリーONeal

回答:


228

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

リスト:

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 2010

6
@Billy ONeal:STLやboostのようなヘッダーファイルの実装を行っている場合はどうなりますか。
マーティンヨーク

5
@Billy ONeal:いいえ。それは意見ではなく、優れたデザインの問題です。名前空間に配置しても、使用から保護されません。これは、永久に維持する必要があるパブリックAPIの一部になりました。
マーティンヨーク

21
@Billy ONeal:偶発的な使用から保護します。また、非公開であり、使用すべきではない(愚かなことをしない限り使用できない)ことも記載されています。したがって、それをサポートする必要はありません。名前空間に配置すると、パブリックAPIの一部になります(この会話で欠落しているもの。パブリックAPIはサポートする必要があることを意味します)。
マーティンヨーク

10
@Billy ONeal:ネストされたクラスには、ネストされた名前空間よりもいくつかの利点があります。名前空間のインスタンスは作成できませんが、クラスのインスタンスは作成できます。とおりdetail大会:代わりに心の中で自分を保つために、このような表記の1つのニーズを応じて、それはあなたのためにそれらを追跡し、コンパイラに依存する方が良いでしょう。
SasQ 2014年

142

ネストされたクラスは通常のクラスと同じですが、次のようになります。

  • (クラス定義内のすべての定義と同様に)追加のアクセス制限があります。
  • それらは与えられた名前空間、例えばグローバル名前空間を汚染しません。クラスBがクラスAに深く関連しているように感じても、AとBのオブジェクトが必ずしも関連していない場合は、クラスBにアクセスするには、Aクラス(Aと呼ばれる) ::クラス)。

いくつかの例:

関連するクラスのスコープに配置するための、クラスを公にネスト


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

  1. 2つのクラスを宣言しますSomeSpecificCollectionElement-悪い、名前「要素」は、一般的に十分可能名前の衝突を起こさせるためにあるので、

  2. 名前空間someSpecificCollectionを導入し、クラスsomeSpecificCollection::Collectionを宣言しますsomeSpecificCollection::Element。名前の衝突のリスクはありませんが、さらに冗長になる可能性はありますか?

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

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

    • Elementはグローバル名前空間にないため、名前が衝突する危険はありません。
    • SomeSpecificCollectionあなたの実装では、だけElementで、他の場所ではSomeSpecificCollection::Element-と同じように+-と見なされますが、より明確です。
    • 「コレクションの特定の要素」ではなく、「特定のコレクションの要素」であることが簡単にわかります
    • SomeSpecificCollectionクラスでもあることは明らかです。

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

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

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


これは、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を入力すると、提案された目的のスコープから列挙型のみが取得されます。これにより、ミスをするリスクも軽減されます。

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

同様に、必要に応じて、テンプレート内で多数のtypedefを「整理」することができます。それは時々便利なパターンです。

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; このエラーはハーブサッターによって一般化されました。不完全な型にはauto_ptrを使用しないでください。少なくとも、誤ったコードが生成されないように予防策を講じてください。
ジーンブシュエフ2010

2
@Billy ONeal:auto_ptrほとんどの実装では不完全な型を宣言できることを私が認識している限り、技術的にはC ++ 0xの一部のテンプレート(例:)とは異なりunique_ptr、テンプレートパラメータが不完全な型と、型が完全でなければならない場所。(例:の使用~unique_ptr
CBベイリー

2
@Billy ONeal:C ++ 03 17.4.6.3 [lib.res.on.functions]で、「特に、次の場合の効果は未定義です:[...]不完全な型がテンプレート引数として使用されている場合テンプレートコンポーネントをインスタンス化するとき。」一方、C ++ 0xでは、「テンプレートコンポーネントを具体的に許可しない限り、テンプレートコンポーネントをインスタンス化するときに、不完全な型がテンプレート引数として使用される場合」と書かれています。以降(例):「のテンプレートパラメータTunique_ptr不完全な型である可能性があります。」
CBベイリー

1
@MilesRoutそれはあまりにも一般的です。クライアントコードが継承できるかどうかによって異なります。ルール:基本クラスポインターを通じて削除しないことが確実な場合、仮想dtorは完全に冗長です。
Kos

2
@IsaacPascual aww、これで更新しましたenum class
コス

21

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

たとえばField、ID番号、タイプコード、フィールド名を持つジェネリッククラスを考えてみます。ID番号または名前のいずれかvectorでこれらFieldのを検索する場合は、それを実行するファンクタを作成します。

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()のコンストラクタがパブリックであることに気づきました。コンストラクターは常にパブリックである必要はないと思います。その場合、クラスの外でインスタンス化することはできません。
2010

1
@user:STLファンクタの場合、コンストラクタはパブリックである必要があります。
John Dibling 2010

1
@Billy:私はまだネストされたクラスが悪い理由を任意の具体的な推論を見ていません。
John Dibling 2010

@ジョン:すべてのコーディングスタイルガイドラインは意見の問題に帰着します。私はこの周りのいくつかのコメントにいくつかの理由を挙げましたが、それらはすべて(私の意見では)合理的です。コードが有効であり、未定義の動作を引き起こさない限り、「事実上の」引数はありません。ただし、ここに記述したコード例は、ネストされたクラスを回避する大きな理由、つまり名前の衝突を指摘していると思います。
Billy ONeal 2010

1
もちろん、マクロよりインラインを好む技術的な理由があります!!
マイルズルート2013年

14

入れ子のクラスでビルダーパターンを実装できます。特にC ++では、個人的には意味的にクリーンだと思います。例えば:

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

のではなく:

class Product {}
class ProductBuilder {}

もちろん、ビルドが1つしかない場合は機能しますが、複数の具象ビルダーが必要な場合は厄介です。慎重に設計を決定する必要があります:)
irsis
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.