18

私の具体的な質問は、C ++でシングルトンクラスを実装する場合、パフォーマンス、副次的な問題などに関して、以下の2つのコードの間に実質的な違いがあるかどうかです。

class singleton
{
    // ...
    static singleton& getInstance()
    {
        // allocating on heap
        static singleton* pInstance = new singleton();
        return *pInstance;
    }
    // ...
};

この:

class singleton
{
    // ...
    static singleton& getInstance()
    {
        // using static variable
        static singleton instance;
        return instance;
    }
    // ...
};


(ヒープベースの実装での逆参照はパフォーマンスに影響を与えないことに注意してください。AFAIKでは逆参照用に生成された追加のマシンコードはありません。ポインターと区別するのは構文の問題のようです。)

アップデート:

ここに要約しようとしている興味深い回答とコメントがあります。(興味のある方は詳細な回答を読むことをお勧めします。):

  • 静的ローカル変数を使用するシングルトンでは、クラスデストラクタはプロセスの終了時に自動的に呼び出されますが、動的割り当ての場合は、スマートポインタを使用するなど、いつかオブジェクトの破棄を管理する必要があります。
    static singleton& getInstance() {
        static std::auto_ptr<singleton> instance (new singleton());
        return *instance.get(); 
    }
  • 動的割り当てを使用するシングルトンは、静的シングルトン変数よりも「遅延」です。後者の場合、シングルトンオブジェクトに必要なメモリは、プロセスの起動時に(プログラムのロードに必要なメモリ全体の一部として)予約されます。 )そしてシングルトンコンストラクターの呼び出しのみがgetInstance()call-timeに延期されます。sizeof(singleton)これは、が大きい場合に問題になる可能性があります。

  • どちらもC++11ではスレッドセーフです。ただし、以前のバージョンのC ++では、実装固有です。

  • 動的割り当ての場合は、1レベルの間接参照を使用してシングルトンオブジェクトにアクセスしますが、静的シングルトンオブジェクトの場合は、オブジェクトの直接アドレスが決定され、コンパイル時にハードコーディングされます。


PS:@TonyDの回答に従って、元の投稿で使用した用語を修正しました。

4

2 に答える 2

7
  • バージョンはnew明らかに実行時にメモリを割り当てる必要がありますが、非ポインタ バージョンではコンパイル時にメモリが割り当てられます (ただし、どちらも同じ構築を行う必要があります)。

  • バージョンはnewプログラムの終了時にオブジェクトのデストラクタを呼び出しませんが、非newバージョンは呼び出します: スマート ポインタを使用してこれを修正できます。

    • 一部の静的/名前空間スコープのオブジェクトのデストラクタが、その静的ローカル インスタンスのデストラクタが実行された後にシングルトンを呼び出さないように注意する必要があります...これが心配な場合は、シングルトンの有効期間とそれらを管理するためのアプローチ。Andrei Alexandrescu の Modern C++ Design には、非常に読みやすい扱いがあります。
  • C++03 では、どちらがスレッド セーフであるかは実装定義です。(GCCはそうなる傾向があると思いますが、Visual Studioはそうではない傾向があります-確認/修正のコメントを歓迎します。)

  • C++11 では、安全です: 6.7.4 「変数の初期化中に制御が同時に宣言に入った場合、同時実行は初期化の完了を待つ必要があります。」(再帰なし)。

コンパイル時と実行時の割り当てと初期化に関する議論

要約といくつかのコメントの言い方から、静的変数の割り当てと初期化の微妙な側面を完全に理解していないと思われます....

プログラムに 3 つのローカル静的 32 ビットints - abおよびc- が異なる関数にあるとします。コンパイラは、OS ローダーにそれらの静的のために 3x32 ビット = 12 バイトのメモリを残すように指示するバイナリをコンパイルする可能性があります。コンパイラは、これらの変数のそれぞれがどのオフセットにあるかを決定しますa。データ セグメントのオフセット 1000 hex、b1004、およびc1008 で。プログラムが実行されるとき、OS ローダーはそれぞれに個別にメモリを割り当てる必要はありません。知っているのは合計 12 バイトだけです。とにかく、プロセスが他のユーザーのプログラムからの残りのメモリコンテンツを認識できないようにする必要がある場合があります。プログラム内の機械語命令は、通常、 へのアクセス用にオフセット 1000、1004、1008 をハードコードするためa、実行時にこれらのアドレスを割り当てる必要はありませんbc

動的メモリ割り当ては、先ほど説明したように、コンパイル時にポインター(たとえばp_ap_bp_c

  • ポイント先のメモリ(abおよびのそれぞれc)は、実行時に見つける必要があります(通常、静的関数が最初に実行されますが、他の回答に関する私のコメントに従って、コンパイラはそれを以前に実行することが許可されています)。
    • 動的割り当てを成功させるには、現在オペレーティング システムによってプロセスに割り当てられているメモリが少なすぎる場合、プログラム ライブラリは OS に追加のメモリを要求します (たとえば、 を使用sbreak())。OS は通常、セキュリティ上の理由からメモリを消去します。
    • abおよびのそれぞれに割り当てられた動的アドレスはc、ポインタp_ap_bおよびにコピーして戻す必要がありp_cます。

この動的なアプローチは明らかに複雑です。

于 2013-02-25T08:42:12.307 に答える
3

主な違いは、ローカルを使用するstaticと、プログラムを閉じるときにオブジェクトが破棄されることです。代わりに、ヒープに割り当てられたオブジェクトは破棄されずに破棄されます。

C ++では、関数内で静的変数を宣言すると、プログラムの開始時ではなく、スコープに初めて入るときに初期化されることに注意してください(グローバル静的期間変数の場合のように)。

プログラムの起動とシャットダウンはデリケートなフェーズであり、デバッグが非常に難しいため、一般的に、私は何年にもわたってレイジー初期化から明示的な制御された初期化に切り替えました。クラスが複雑なことを何もしておらず、失敗することができない場合(たとえば、単なるレジストリ)、怠惰な初期化でも問題ありません...そうでなければ、制御されていると、かなり多くの問題を回避できます。

mainの最初の命令を入力する前、またはの最後の命令を実行した後にクラッシュするプログラムはmain、デバッグが困難です。

シングルトンの遅延構造を使用する場合のもう1つの問題は、コードがマルチスレッドの場合、同時スレッドがシングルトンを同時に初期化するリスクに注意を払う必要があることです。シングルスレッドコンテキストで初期化とシャットダウンを行う方が簡単です。

マルチスレッドコードでの関数レベルの静的インスタンスの初期化中に発生する可能性のある競合は、言語が公式のマルチスレッドサポートを追加したC ++ 11以降で解決されました。通常の場合、適切な同期ガードがコンパイラによって自動的に追加されるため、これは問題になりません。 C++11以降のコード。ただし、静的関数の初期化が関数をa呼び出す場合b、またはその逆の場合、2つの関数が異なるスレッドによって同時に最初に呼び出されると、デッドロックが発生する可能性があります(これは、コンパイラが単一のミューテックスを使用する場合にのみ問題になります。すべての統計)。また、静的オブジェクトの初期化コード内から静的オブジェクトを含む関数を再帰的に呼び出すことは許可されていないことにも注意してください。

于 2013-02-25T08:32:40.810 に答える