C++のコンストラクターに関するベストプラクティスをお聞きしたいと思います。コンストラクターで何をすべきか、何をすべきでないかはよくわかりません。
属性の初期化、親コンストラクターの呼び出しなどにのみ使用する必要がありますか?または、構成データの読み取りと解析、外部ライブラリa.s.oのセットアップなど、より複雑な関数をそれらに組み込むこともできます。
それとも、このための特別な関数を書く必要がありますか?それぞれinit()
/cleanup()
?
ここにあるPROとCONは何ですか?
たとえば、init()
とcleanup()
を使用すると、共有ポインタを取り除くことができることを私はまだ理解していました。スタック上にクラス属性としてオブジェクトを作成し、後で構築済みのときに初期化することができます。
コンストラクターで処理する場合は、実行時にインスタンス化する必要があります。次に、ポインタが必要です。
どうやって決めるのか本当にわかりません。
多分あなたは私を助けることができますか?
複雑なロジックとコンストラクターは常にうまく混ざり合うわけではなく、コンストラクターで重い作業を行うことに対して強い支持者がいます(理由があります)。
基本的なルールは、コンストラクターが完全に使用可能なオブジェクトを生成する必要があるということです。
class Vector
{
public:
Vector(): mSize(10), mData(new int[mSize]) {}
private:
size_t mSize;
int mData[];
};
これは完全に初期化されたオブジェクトを意味するものではありません。ユーザーがそれについて考える必要がない限り、初期化を延期することができます(怠惰だと思います)。
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)に依存している場合は、依存性注入について考えてください。ユニットテストに非常に役立ちます。
delete this
またはデストラクタを呼び出さないでください。簡単な答え:状況によります。
ソフトウェアを設計するときは、 [〜#〜] raii [〜#〜] 原則(「リソースの取得は初期化です」)を使用してプログラムする必要があります。これは、(とりわけ)オブジェクト自体がそのリソースに責任があり、呼び出し元には責任がないことを意味します。また、あなたはあなた自身に精通したいかもしれません 例外安全性 (さまざまな程度で)。
たとえば、次のことを考慮してください。
void func(){ MyFile f( "myfile.dat"); doSomething(f); }
クラスMyFile
をこのように設計すると、doSomething(f)
の前にf
が初期化されることを確認でき、それをチェックする手間を大幅に節約できます。また、デストラクタでf
によって保持されているリソースを解放する、つまりファイルハンドルを閉じると、安全で使いやすくなります。
この特定のケースでは、コンストラクターの特別なプロパティを使用できます。
virtual
メソッドがある場合は、 あなたはそれらを呼ぶべきではありません コンストラクターの内部から、何をしているのかがわからない限り、仮想オーバーライドメソッドが呼び出されない理由に驚かれるかもしれません。誰かを混乱させないのが最善です。コンストラクターはオブジェクトを 使える 状態。そしてそれは賢明であるため aPIを間違って使用するのを難しくします、最善の方法は 正しく使いやすくする (sicからScott Meyersへ)。コンストラクター内で初期化を行うことをデフォルトの戦略にする必要がありますが、もちろん例外は常にあります。
したがって、初期化にコンストラクターを使用するのは良い方法です。常に可能であるとは限りません。たとえば、GUIフレームワークを構築してから、初期化する必要がある場合がよくあります。しかし、ソフトウェアを完全にこの方向に設計すれば、多くの手間を省くことができます。
FromC++プログラミング言語:
init()
などの関数を使用してクラスオブジェクトの初期化を提供することは、エレガントではなく、エラーが発生しやすくなります。オブジェクトを初期化する必要があるとはどこにも述べられていないため、プログラマーはそうすることを忘れることができます-または2回行う(多くの場合、同じように悲惨な結果になります)。より良いアプローチは、プログラマーがオブジェクトを初期化するという明確な目的で関数を宣言できるようにすることです。このような関数は特定の型の値を作成するため、コンストラクターと呼ばれます。
私は通常、クラスを設計するときに次のルールを考慮します。コンストラクターの実行後にクラスの任意のメソッドを使用安全にできる必要があります。ここで安全とは、オブジェクトのinit()
メソッドが呼び出されていない場合はいつでも例外をスローできることを意味しますが、実際に使用できるものが必要です。
たとえば、デフォルトのコンストラクタを使用すると、class std::string
はメモリを割り当てない可能性があります。これは、ほとんどのメソッド(begin()
とend()
)が両方ともnullポインタを返し、c_str()
は、他の設計上の理由から必ずしも現在のバッファを返すとは限らないため、いつでもメモリを割り当てる準備をする必要があります。この場合、メモリを割り当てないと、完全に使用可能な文字列インスタンスになります。
逆に、ミューテックスロックのスコープガードでのRAIIの使用は、任意の長い時間(ロックの所有者がロックを解放するまで)実行できるコンストラクターの例ですが、それでも一般的にはグッドプラクティスとして受け入れられています。
いずれにせよ、レイジー初期化はinit()
メソッドを使用するよりも安全な方法で実行できます。 1つの方法は、コンストラクターへのすべてのパラメーターをキャプチャーする中間クラスを使用することです。もう1つは、ビルダーパターンを使用することです。
コンストラクターは、Wordgoから使用できるオブジェクトを作成することが期待されています。何らかの理由で使用可能なオブジェクトを作成できない場合は、例外をスローして処理する必要があります。したがって、オブジェクトが正しく機能するために必要なすべての補足メソッド/関数は、コンストラクターから呼び出す必要があります(機能のような遅延読み込みが必要な場合を除く)
私はむしろ尋ねたい:
What all to do in the constructor?
上記でカバーされていないものはすべて、OPの質問への回答です。
コンストラクターの唯一の目的は
すべてのメンバー変数を初期化する既知の状態に、そして
リソースを割り当てます(該当する場合)。
項目#1はとても単純に聞こえますが、定期的に忘れられたり無視されたりし、静的分析ツールによってのみ思い出されることがわかります。これを過小評価しないでください(しゃれを意図しています)。
コンストラクターからスローすることができます。多くの場合、ゾンビオブジェクト、つまり「失敗」状態のオブジェクトを作成するよりも優れたオプションです。
ただし、デストラクタからスローしないでください。
コンパイラは、メンバーオブジェクトが構築される順序、つまりヘッダーに表示される順序を認識します。ただし、デストラクタはあなたが言ったようには呼び出されません。つまり、コンストラクタ内でnewを複数回呼び出す場合、デストラクタが削除を呼び出すことを信頼することはできません。それらをスマートポインタオブジェクトに入れると、これらのオブジェクトは削除されるので問題ありません。それらを生のポインターとして使用する場合は、コンストラクターがスローしなくなることがわかるまで一時的にauto_ptrオブジェクトに配置してから、すべてのauto_ptrでrelease()を呼び出します。
一番大事なのはちょっと常識だと思います!すべきこととすべきでないことについては多くの話がありますが、すべてうまくいっていますが、考慮すべき重要な点は、オブジェクトがどのように使用されるかです。例えば、
このオブジェクトが単一のインスタンスであり、それが最初に構築され、構築がクリティカルパスにない場合、コンストラクターで重い作業を行わないのはなぜですか(例外を適切に使用する限り、それはさらに意味があります)?一方、ループで短時間作成および破棄されるのが非常に軽量なオブジェクトである場合は、(たとえば、メンバーの初期化を除いて)できるだけ少なくしてみてください(ファンクターは非常に良い例です)これの...)
二相負荷(または何でも)を持つことには利点がありますが、主な欠点はそれを呼び出すのを忘れることです-私たちの何人がそれをしましたか? :)
ですから、私のタペンスは、厳格なルールに固執するのではなく、オブジェクトがどのように使用されるかを注意深く見て、それに合うように設計することです!
コンストラクターは、オブジェクトを構築するために使用されます-それ以上でもそれ以下でもありません。コンストラクター内でクラス不変条件を確立するために必要なことは何でもする必要があり、それがどれほど複雑であるかは、初期化されるオブジェクトのタイプによって異なります。
何らかの理由で例外を使用できない場合にのみ、個別のinit()関数を使用することをお勧めします。
ええと、"コンストラクター"は、建設、構築、セットアップから来ています。したがって、すべての初期化が行われる場所があります。クラスをインスタンス化するときはいつでも、コンストラクターを使用して、新しいオブジェクトを操作可能にするためにすべてが完了していることを確認してください。
やりたいことはできますが、その目的のためにコンストラクターを使用して、オブジェクトを作成します。そのために他のメソッドを呼び出す必要がある場合は、それで問題ありません。 1つのルールに従うだけです。必要以上に複雑にしないでください。コンストラクターをできるだけ単純にすることをお勧めしますが、それはメンバーを初期化するだけでよいという意味ではありません。
理想的には、(属性の割り当てを除いて)コンストラクターにコードを含めないでください。重要な理由が1つあります。それは、オブジェクトの構成を防ぎ、オブジェクトを拡張不能にすることです。
これについての私のブログ投稿は次のとおりです: コンストラクターはコードフリーでなければなりません