38


19

コンストラクターで行う(しない)こと

C ++のコンストラクタに関するベストプラクティスをお聞きしたいと思います。 コンストラクターで何をすべきか、何をすべきではないかはよくわかりません。

属性の初期化、親コンストラクターの呼び出しなどにのみ使用する必要がありますか? または、構成データの読み取りと解析、外部ライブラリa.s.oのセットアップなど、より複雑な機能を追加することもできます。

または、このために特別な関数を作成する必要がありますか? 担当者 init() / cleanup()

ここにあるPROとCONは何ですか?

たとえば、 `init()`と `cleanup()`を使用すると、共有ポインタを削除できることがわかりました。 スタック上にオブジェクトをクラス属性として作成し、後で構築中に初期化できます。

コンストラクタで処理する場合は、実行時にインスタンス化する必要があります。 次に、ポインターが必要です。

私は本当に決める方法がわかりません。

たぶんあなたは私を助けることができますか?

13 Answer


27


コンストラクタとデストラクタで行う*最も一般的な間違い*は、ポリモーフィズムを使用することです。 * 多態性はしばしばコンストラクタで機能しません *!

例えば。:

class A
{
public:
    A(){ doA();}
    virtual void doA(){};
}

class B : public A
{
public:
    virtual void doA(){ doB();};
    void doB(){};
}


void testB()
{
    B b; // this WON'T call doB();
}

これは、マザークラスAのコンストラクターの実行中にオブジェクトBがまだ構築されていないためです。 したがって、オーバーライドされたバージョンの `void doA();`を呼び出すことはできません。

Benは、以下のコメントで、コンストラクタでポリモーフィズムが機能する例について尋ねてきました。

例えば。:

class A
{
public:
    void callAPolymorphicBehaviour()
    {
        doOverridenBehaviour();
    }

    virtual void doOverridenBehaviour()
    {
        doA();
    }

    void doA(){}
};

class B : public A
{
public:
    B()
    {
        callAPolymorphicBehaviour();
    }

    virtual void doOverridenBehaviour()
    {
        doB()
    }

    void doB(){}
};

void testB()
{
   B b; // this WILL call doB();
}

今回、背後にある理由は: virtual`関数 doOverridenBehaviour() `が呼び出されたとき、オブジェクトbはすでに初期化されています(まだ構築されていません)。つまり、その仮想テーブルが初期化され、実行できることを意味します多型。


22


複雑なロジックとコンストラクターが常にうまく混ざるわけではなく、コンストラクターで重い作業を行うことに強い支持者がいます(理由があります)。

基本的なルールは、コンストラクターが完全に使用可能なオブジェクトを生成することです。

class Vector
{
public:
  Vector(): mSize(10), mData(new int[mSize]) {}
private:
  size_t mSize;
  int mData[];
};

それは完全に初期化されたオブジェクトを意味するのではなく、ユーザーがそれについて考える必要がない限り、いくつかの初期化を遅らせることができます(怠laだと考えてください)。

class Vector
{
public:
  Vector(): mSize(0), mData(0) {}

  // first call to access element should grab memory

private:
  size_t mSize;
  int mData[];
};

重い作業が必要な場合は、コンストラクターを呼び出す前に重い作業を行うビルダーメソッドを選択することができます。 たとえば、データベースから設定を取得し、設定オブジェクトを構築することを想像してください。

// in the constructor
Setting::Setting()
{
  // connect
  // retrieve settings
  // close connection (wait, you used RAII right ?)
  // initialize object
}

// Builder method
Setting Setting::Build()
{
  // connect
  // retrieve settings

  Setting setting;
  // initialize object
  return setting;
}

このビルダーメソッドは、オブジェクトの構築を延期することで大きなメリットが得られる場合に役立ちます。 たとえば、オブジェクトが大量のメモリを取得する場合、失敗する可能性が高いタスクの後にメモリの取得を延期することは悪い考えではありません。

このビルダーメソッドは、プライベートコンストラクターとパブリック(またはフレンド)ビルダーを意味します。 Privateコンストラクターを使用すると、クラスで実行できる使用法にいくつかの制限が課されることに注意してください(たとえば、STLコンテナーに格納できません)。したがって、他のパターンにマージする必要がある場合があります。 そのため、この方法は例外的な状況でのみ使用する必要があります。

外部エンティティ(ファイル/ DB)に依存している場合、依存性注入について考えると、このようなエンティティもテストする方法を検討することをお勧めします。これは単体テストに役立ちます。


15


  • 「delete this」やコンストラクターのデストラクターを呼び出さないでください。

  • init()/ cleanup()メンバーを使用しないでください。 init()を毎回呼び出す必要がある場合 インスタンスを作成するときは、init()のすべてがコンストラクター内にある必要があります。 コンストラクターは、インスタンスを一貫性のある状態にすることで、パブリックメンバーが明確に定義された動作で呼び出されるようにします。 同様に、cleanup()に加えてcleanup()は、http://en.wikipedia.org/wiki/Resource_acquisition_is_initialization [RAII]を強制終了します。 (ただし、複数のコンストラクターがある場合、それらによって呼び出されるプライベートinit()関数があると便利です。)

  • コンストラクターでより複雑なことをしても大丈夫です。 クラスの使用目的と全体的な設計。 たとえば、ある種のIntegerまたはPointクラスのコンストラクターでファイルを読み取ることはお勧めできません。ユーザーはそれらが安価に作成できることを期待しています。 また、ファイルアクセスコンストラクターが単体テストの記述能力にどのように影響するかを考慮することも重要です。 通常、最善の解決策は、メンバーを構築するために必要なデータのみを取得し、ファイルを解析してインスタンスを返す非メンバー関数を作成するコンストラクターを持つことです。


9


簡単な答え:それは異なります。

ソフトウェアを設計する際には、http://en.wikipedia.org/wiki/Resource_acquisition_is_initialization [RAII]原則(「リソースの取得は初期化」)を使用してプログラムすることをお勧めします。 これは、(特に)オブジェクト自体がそのリソースに対して責任があり、呼び出し側には責任がないことを意味します。 また、_http://en.wikipedia.org/wiki/Exception_handling#Exception_safety [例外の安全性] _(程度は異なります)に慣れておくとよいでしょう。

たとえば、

void func() {
    MyFile f("myfile.dat");
    doSomething(f);
}

クラス MyFile`を設計する場合、 doSomething(f) の前に f`が初期化されていることを確認できるので、それをチェックする手間が省けます。 また、デストラクタでfによって保持されているリソースを解放する場合、つまり ファイルハンドルを閉じると、安全な側にあり、簡単に使用できます。

この特定のケースでは、コンストラクターの特別なプロパティを使用できます。

  • コンストラクターから外部に例外をスローすると、 オブジェクトは_作成されません_。 つまり、デストラクタは呼び出されず、メモリはすぐに解放されます。

  • コンストラクターを呼び出す必要があります。 ユーザーに強制的に使用させることはできません 他の関数(デストラクタを除く)、慣例のみ。 したがって、ユーザーにオブジェクトの初期化を強制する場合、コンストラクターを使用しないのはなぜですか?

  • 「仮想」メソッドがある場合、それらから呼び出さないでください コンストラクター内では、何をしているのかわからない限り、あなた(または後のユーザー)が仮想オーバーライドメソッドが呼び出されない理由に驚くかもしれません。 誰も混乱させないでください。

コンストラクターは、オブジェクトを_usable_状態のままにする必要があります。 また、APIを間違って使用するのを難しくする_のは賢明ではないので、行うべき最善のことは、_正しい使用を簡単にする(Scott Meyersへの_sic_)です。 コンストラクター内で初期化を行うことはデフォルトの戦略である必要がありますが、もちろん例外は常に存在します。

したがって:初期化にコンストラクターを使用するのに良い方法です。 常に可能であるとは限りません。たとえば、GUIフレームワークを構築してから初期化する必要があることがよくあります。 しかし、この方向でソフトウェアを完全に設計すれば、多くの手間を省くことができます。


6


_The C ++ Programming Language_から:

_ クラスオブジェクトの初期化を提供するための `init()`などの関数の使用は、洗練されておらずエラーが発生しやすいです。 オブジェクトを初期化する必要があるとはどこにも述べられていないため、*プログラマーは初期化するのを忘れることがあります。 より良いアプローチは、プログラマがオブジェクトを初期化するという明確な目的で関数を宣言できるようにすることです。 このような関数は特定の型の値を構成するため、コンストラクターと呼ばれます。 _

クラスを設計するときは、通常、次のルールを考慮します。コンストラクターの実行後、クラスの*任意のメソッドを*安全に*使用できる必要があります。 ここで安全なのは、オブジェクトの `init()`メソッドが呼び出されていない場合は常に例外をスローできることを意味しますが、実際に使用できるものが望ましいです。

たとえば、デフォルトのコンストラクターを使用すると、ほとんどのメソッド(つまり、 begin()`と `end())は両方がnullポインタを返し、 `c_str()`が他の設計上の理由で現在のバッファを必ずしも返さない場合、正しく動作します。したがって、任意の場所にメモリを割り当てる準備をする必要があります時間。 この場合、メモリを割り当てないと、文字列インスタンスが完全に使用可能になります。

逆に、ミューテックスロックのスコープガードでRAIIを使用することは、(ロックの所有者が解放するまで)任意の時間実行できるコンストラクタの例ですが、それでも良い習慣として一般に受け入れられています。

いずれにしても、遅延初期化は、 `init()`メソッドを使用するよりも安全な方法で実行できます。 1つの方法は、コンストラクターにすべてのパラメーターをキャプチャする中間クラスを使用することです。 別の方法は、ビルダーパターンを使用することです。


4


コンストラクターは、goという単語から使用できるオブジェクトを作成する必要があります。 何らかの理由で使用可能なオブジェクトを作成できない場合は、例外をスローして処理を完了する必要があります。 そのため、オブジェクトが適切に機能するために必要なすべての補足的なメソッド/関数は、コンストラクターから呼び出す必要があります(機能のような遅延読み込みが必要な場合を除く)


4


私はむしろ尋ねたい:

What all to do in the constructor?

上記で説明されていないものは、OPの質問に対する答えです。

コンストラクターの唯一の目的は

  1. *すべてのメンバー変数を既知の状態に初期化*し、

  2. リソースを割り当てます(該当する場合)。

アイテム#1はとてもシンプルに聞こえますが、定期的に忘れられたり無視されたりするのは、静的分析ツールによってのみ思い出されることです。 これを過小評価しないでください(しゃれを意図)。


4


コンストラクタから投げることができますが、多くの場合、ゾンビオブジェクトを作成するよりも優れたオプションです。 「失敗」状態のオブジェクト。

ただし、デストラクタから決して投げないでください。

コンパイラは、メンバーオブジェクトが構築される順序(ヘッダーに表示される順序)を認識します。 ただし、あなたが言ったようにデストラクタは呼び出されません。つまり、コンストラクタ内でnewを複数回呼び出す場合、デストラクタが削除を呼び出すことに頼ることはできません。 これらをスマートポインターオブジェクトに入れると、これらのオブジェクトは削除されるため問題ありません。 生のポインタとして使用したい場合は、コンストラクタがスローしなくなることがわかるまで一時的にauto_ptrオブジェクトに入れてから、すべてのauto_ptrsでrelease()を呼び出します。


3


コンストラクターを使用してオブジェクトを構築します-これ以上でもそれ以下でもありません。 コンストラクター内でクラス不変式を確立するために必要なことは何でもする必要があり、それがどれほど複雑であるかは、初期化されるオブジェクトのタイプに依存します。

何らかの理由で例外を使用できない場合にのみ、別個のinit()関数を使用することをお勧めします。


2


さて、«constructor»は建設、建築、セットアップから来ています。 したがって、すべての初期化が行われる場所があります。 クラスをインスタンス化するたびに、コンストラクターを使用して、新しいオブジェクトを使用可能にするためにすべてが完了していることを確認します。