5

ただの設計/最適化の質問。ポインタやオブジェクトをいつ保存しますか、またその理由は何ですか。たとえば、私はこれらの両方の作業を信じています(コンパイルエラーを除いて):

class A{
  std::unique_ptr<Object> object_ptr;
};

A::A():object_ptr(new Object()){}

class B{
  Object object;
};

B::B():object(Object()){}

スタックまたはヒープでインスタンス化するときに1つの違いが生じると思いますか?

例えば:

   int main(){
      std::unique_ptr<A> a_ptr;
      std::unique_ptr<B> b_ptr;
      a_ptr = new A(); //(*object_ptr) on heap, (*a_ptr) on heap?
      b_ptr = new B(); //(*object_ptr) on heap, object on heap?

      A a;   //a on stack, (*object_ptr) on heap?
      B b;   //b on stack, object on stack?
}

また、sizeof(A)< sizeof(B)?私が見逃している他の問題はありますか?(ダニエルはコメントの彼の関連する投稿で継承の問題について私に思い出させました)

スタック割り当ては一般にヒープ割り当てよりも高速ですが、AのサイズはBよりも小さいので、これらは、移動セマンティクスを使用しても問題のケースでパフォーマンスをテストせずに答えることができないトレードオフの1つですか?または、経験則いくつかのルールを使用する方が有利な場合はどうでしょうか。(San Jacintoは、スタック/ヒープの割り当てが高速であり、スタック/ヒープではないことについて私を修正しました)

より多くのコピーを作成すると、同じパフォーマンスの問題が発生すると思います(3つのコピーは、最初のインスタンスを開始する場合と同様のパフォーマンスヒットの約3倍になります)。しかし、移動構築は、スタックを可能な限り使用する方が有利な場合がありますか?

これは関連する質問ですが、まったく同じではありません。 C ++ STL:オブジェクト全体を保存する必要がありますか、それともオブジェクトへのポインターを保存する必要がありますか?

ありがとう!

4

4 に答える 4

6

内に大きなオブジェクトがある場合は、そのオブジェクトA classへのポインターを保存しますが、小さなオブジェクトまたはプリミティブ型の場合、ほとんどの場合、ポインターを保存する必要はありません。

また、何かがスタックまたはヒープ (フリーストア) に格納される場合は、実際には実装に依存し、A a常にスタックに格納されるとは限りません。

宣言されている関数のスコープによって保存期間が決定されるため、これを自動オブジェクトと呼ぶ方が適切です。関数が戻ると、a破棄されます。

ポインターを使用する必要があり、new多少のオーバーヘッドが発生しますが、今日のマシンでは、何百万ものオブジェクトを起動していない限り、ほとんどの場合は些細なことだと思いnewます。その後、パフォーマンスの問題が発生し始めます。

状況はそれぞれ異なり、自動オブジェクトの代わりにポインターを使用する必要がある場合と使用しない場合は、状況に大きく依存します。

于 2011-09-13T17:08:37.120 に答える
5

ヒープはスタックよりも「遅く」ありません。ヒープの割り当てはスタックの割り当てよりも遅くなる可能性があり、連続したメモリ アクセスがあまりないようにオブジェクトとデータ構造を設計すると、キャッシュの局所性が低いと、多くのキャッシュ ミスが発生する可能性があります。したがって、この観点からは、設計とコードの使用の目標が何であるかによって異なります。

これはさておき、コピーのセマンティクスについても疑問を呈する必要があります。オブジェクトのディープ コピーが必要な場合 (オブジェクトのオブジェクトもディープ コピーされる場合)、ポインタを保存する必要があるでしょうか。コピー セマンティクスのためにメモリを共有しても問題ない場合は、ポインタを格納しますが、dtor でメモリを 2 回解放しないようにしてください。

私は 2 つの条件下でポインターを使用する傾向があります。クラス メンバーの初期化順序が重要であり、オブジェクトに依存関係を注入しています。他のほとんどの場合、非ポインター型を使用します。

編集:ポインターを使用する場合、さらに2つのケースがあります。1)循環インクルード依存関係を回避するため(場合によっては参照を使用する場合があります)、2)ポリモーフィック関数呼び出しを使用することを意図しています。

于 2011-09-13T17:12:13.457 に答える
5

これは多くの特定の要因に依存し、どちらのアプローチにもメリットがあります。動的割り当てによって外部オブジェクトのみを使用する場合は、すべてのメンバーを直接メンバーにして、追加のメンバー割り当てを回避することもできます。一方、外側のオブジェクトが自動的に割り当てられる場合、大きなメンバーはおそらくunique_ptr.

ポインターのみを介してメンバーを処理することには、追加の利点があります。コンパイル時の依存関係を削除すると、外部クラスのヘッダー ファイルは、内部クラスを完全に含める必要がなく、内部クラスの前方宣言を回避できる場合があります。クラスのヘッダー (「PIMPL」)。大規模なプロジェクトでは、この種のデカップリングが経済的に賢明であることが判明する場合があります。

于 2011-09-13T17:18:56.137 に答える
2

ポインタを格納する以外に選択肢がほとんどない場合がいくつかあります。明らかなものの1つは、バイナリツリーのようなものを作成している場合です。

template <class T>
struct tree_node { 
    struct tree_node *left, *right;
    T data;
}

この場合、定義は基本的に再帰的であり、ツリーノードが持つ可能性のある子孫の数を事前に知ることはできません。ポインタの格納(少なくともいくつかのバリエーション)と、必要に応じて子孫ノードの割り当てにかなり悩まされています。

親オブジェクトに単一のオブジェクト(またはオブジェクトの配列)しかない動的文字列のような場合もありますが、そのサイズは、必要なだけの十分な範囲で変化する可能性があります(少なくとも可能性を提供します)。動的割り当てを使用します。文字列の場合、小さいサイズが一般的であるため、かなり広く使用されている「短い文字列の最適化」があります。この場合、文字列オブジェクトには、ある制限までの文字列用の十分なスペースと、文字列がそれを超えた場合に動的に割り当てることができるポインタが含まれます。サイズ:

template <class T>
class some_string { 
    static const limit = 20;
    size_t allocated;
    size_t in_use;
    union { 
        T short_data[limit];
        T *long_data;
    };

    // ...

};

サブオブジェクトを直接格納する代わりにポインタを使用する理由はあまり明白ではありませんが、例外の安全性のためです。明らかな例の1つとして、親オブジェクトにポインタのみを格納する場合、保証を提供swapするオブジェクトにを提供することは簡単になります(通常はそうなります)。nothrow

template <class T>
class parent { 
    T *data;

    void friend swap(parent &a, parent &b) throw() { 
         T *temp = a.data;
         a.data = b.data;
         b.data = temp;
    }
};

いくつかの(通常は有効な)仮定のみで:

  1. ポインタは最初から有効であり、
  2. 有効なポインタを割り当てると、例外がスローされることはありません

...このスワップが無条件に保証を与えることは簡単ですnothrow(つまり、「スワップはスローされません」とだけ言うことができます)。ポインタの代わりにオブジェクトを直接格納する場合parent、条件付きでのみ保証できます(たとえば、swapTのコピーコンストラクタまたは代入演算子がスローされた場合にのみスローされます。」)

C ++ 11の場合、このようなポインターを頻繁に使用すると(通常は?)、非常に効率的な移動コンストラクターを簡単に提供できます(これによりnothrow保証も提供されます)。もちろん、データ(のほとんど)へのポインターを使用することは、高速移動構造への唯一の可能なルートではありません-しかし、それは簡単なものです。

最後に、質問をしたときに頭に浮かんだと思われるケースがあります。関連するロジックが、自動割り当てと動的割り当てのどちらを使用する必要があるかを必ずしも示していない場合です。この場合、それは(明らかに)判断の呼びかけです。純粋に理論的な観点からは、これらの場合に使用するものにはおそらくまったく違いはありません。ただし、実用的な観点からは、かなりの違いが生じる可能性があります。CまたはC++標準のどちらも保証していませんが(またはヒントさえ)どんな種類のものでも、現実には、ほとんどの一般的なシステムでは、自動割り当てを使用するオブジェクトがスタックに配置されます。ほとんどの一般的なシステム(Windows、Linuxなど)では、スタックは使用可能なメモリのごく一部に制限されています(通常、1桁から2桁未満のメガバイトのオーダー)。

これは、任意の時点で存在する可能性のあるこれらのタイプのすべてのオブジェクトが数メガバイト(またはそれ以上)を超える可能性がある場合、データ(少なくともほとんど)が自動的ではなく動的に割り当てられるようにする必要があることを意味します。これを行うには2つの方法があります。使用可能なスタックスペースを超える可能性がある場合に親オブジェクトを動的に割り当てるようにユーザーに任せるか、または割り当てる比較的小さな「シェル」オブジェクトをユーザーに操作させることができます。ユーザーに代わって動的にスペースを空けます。

それが問題になる可能性が非常に高い場合は、ほとんどの場合、ユーザーに強制するのではなく、クラスが動的割り当てを処理することをお勧めします。これには2つの明らかな良い点があります。

  1. ユーザーはスタックベースのリソース管理(SBRM、別名RAII)を使用できるようになり、
  2. 限られたスタックスペースの効果は、デザイン全体に「浸透」するのではなく、制限されます。

結論:特に、格納されているタイプが事前にわからないテンプレートの場合、ポインターと動的割り当てを好む傾向があります。サブオブジェクトの直接ストレージは、主に、格納されているタイプが(ほぼ?)常に非常に小さいことがわかっている状況、または動的割り当てが実際の速度の問題を引き起こしていることをプロファイリングが示している状況に予約します。operator newただし、後者の場合、そのクラスのオーバーロードなどの代替案に少なくともいくつかを与えます。

于 2011-09-13T18:04:36.507 に答える