web-dev-qa-db-ja.com

C ++でのmake_sharedと通常のshared_ptrの違い

std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));

これに関するGoogleやstackoverflowの投稿は多数ありますが、make_sharedを直接使用するよりもshared_ptrのほうが効率的な理由を理解できません。

make_sharedがどのように効率的であるかを理解できるようにするために、誰かが私に作成されたオブジェクトとその両方によって実行された操作のステップ順を追って説明することはできますか。私は参考のために上記の一例を挙げました。

231
Anup Buchke

違いは、std::make_sharedは1回のヒープ割り当てを実行するのに対し、std::shared_ptrコンストラクタの呼び出しは2回実行することです。

ヒープ割り当てはどこで行われますか?

std::shared_ptrは2つのエンティティを管理します。

  • 制御ブロック(参照カウント、型消去された削除者などのメタデータを格納する)
  • 管理されているオブジェクト

std::make_sharedは、制御ブロックとデータの両方に必要なスペースを考慮して、単一のヒープ割り当て会計を実行します。それ以外の場合、new Obj("foo")は管理対象データ用のヒープ割り当てを呼び出し、std::shared_ptrコンストラクターは制御ブロック用に別のコンストラクターを実行します。

詳細については、実装に関する注意事項cppreference で確認してください。

アップデートI:例外安全

OPは物事の例外安全側について疑問に思っているようであるので、私は私の答えを更新しました。

この例を考えてください。

void F(const std::shared_ptr<Lhs> &lhs, const std::shared_ptr<Rhs> &rhs) { /* ... */ }

F(std::shared_ptr<Lhs>(new Lhs("foo")),
  std::shared_ptr<Rhs>(new Rhs("bar")));

C++では部分式の評価を任意の順序で行うことができるため、考えられる順序は次のとおりです。

  1. new Lhs("foo"))
  2. new Rhs("bar"))
  3. std::shared_ptr<Lhs>
  4. std::shared_ptr<Rhs>

ここで、ステップ2で例外がスローされたとします(たとえば、メモリ不足例外、Rhsコンストラクタが何らかの例外をスローしました)。それをクリーンアップする機会がなかったので、私たちはその後ステップ1で割り当てられたメモリを失います。ここでの問題の中心は、生のポインタがすぐにstd::shared_ptrコンストラクタに渡されなかったことです。

これを修正する1つの方法は、この任意の順序付けが発生しないように別々の行でそれらを実行することです。

auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);

もちろんこれを解決するための好ましい方法は代わりにstd::make_sharedを使うことです。

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

アップデートII:std::make_sharedのデメリット

引用 Casey さんのコメント:

割り当ては1つしかないため、制御ブロックが使用されなくなるまで、指示先のメモリの割り当てを解除することはできません。 weak_ptrは、制御ブロックを無期限に存続させることができます。

weak_ptrsのインスタンスが制御ブロックを生き続けているのはなぜですか?

weak_ptrsが管理対象オブジェクトがまだ有効かどうかを判断する方法がなければなりません(例えばlockの場合)。これを行うには、管理オブジェクトを所有しているshared_ptrの数をチェックします。これは、制御ブロックに格納されています。その結果、shared_ptrカウントとweak_ptrカウントが両方とも0に達するまで、制御ブロックは生きています。

std::make_sharedに戻る

std::make_sharedは、制御ブロックと管理対象オブジェクトの両方に対して単一のヒープ割り当てを行うので、制御ブロックと管理対象オブジェクトのメモリーを別々に解放する方法はありません。制御ブロックと管理対象オブジェクトの両方を解放できるようになるまで待つ必要があります。これは、shared_ptrsまたはweak_ptrsが存在しなくなるまで起こります。

代わりに、newおよびshared_ptrコンストラクターを介して、制御ブロックと管理対象オブジェクトに対して2つのヒープ割り当てを実行したとします。その後、生きているshared_ptrが存在しない場合は管理オブジェクト用のメモリを解放し、生きているweak_ptrが存在しない場合は制御ブロック用のメモリを解放します。

293
mpark

共有ポインタは、オブジェクト自体と、参照カウントとその他のハウスキーピングデータを含む小さなオブジェクトの両方を管理します。 make_sharedは、これら両方を保持するために単一のメモリブロックを割り当てることができます。ポインタからすでに割り当てられているオブジェクトへの共有ポインタを構築するには、参照カウントを格納するために2番目のブロックを割り当てる必要があります。

この効率性と同様に、make_sharedを使用することはnewと生のポインタを扱う必要が全くないことを意味し、より良い例外安全性を与えます - オブジェクトを割り当てた後でスマートポインタに割り当てる前に例外を投げる可能性はありません。

20
Mike Seymour

非公開コンストラクタ(protectedまたはprivate)を呼び出す必要がある場合、make_sharedはそれにアクセスできない可能性がありますが、新しいバリアントではうまくいきますが、2つの可能性が異なることもあります。 。

class A
{
public:

    A(): val(0){}

    std::shared_ptr<A> createNext(){ return std::make_shared<A>(val+1); }
    // Invalid because make_shared needs to call A(int) **internally**

    std::shared_ptr<A> createNext(){ return std::shared_ptr<A>(new A(val+1)); }
    // Works fine because A(int) is called explicitly

private:

    int val;

    A(int v): val(v){}
};
16
Dr_Sam

Shared_ptrによって制御されるオブジェクトに特別なメモリアライメントが必要な場合は、make_sharedに頼ることはできませんが、それを使用しないことの唯一の良い理由だと思います。

4
Simon Ferquel

Shared_ptr:2つのヒープ割り当てを行います

  1. 制御ブロック(参照数)
  2. 管理対象オブジェクト

Make_shared:ヒープ割り当てを1回だけ実行します

  1. 制御ブロックとオブジェクトのデータ。
2
James

私はstd :: make_sharedに一つ問題があるのを見ます、それはプライベート/保護されたコンストラクタをサポートしません

1
icebeat

効率性と割り当てに費やす時間について、以下のこの簡単なテストを行いました。これら2つの方法で(一度に1つずつ)多数のインスタンスを作成しました。

for (int k = 0 ; k < 30000000; ++k)
{
    // took more time than using new
    std::shared_ptr<int> foo = std::make_shared<int> (10);

    // was faster than using make_shared
    std::shared_ptr<int> foo2 = std::shared_ptr<int>(new int(10));
}

つまり、make_sharedを使用すると、newを使用するのに比べて2倍の時間がかかりました。したがって、newを使用すると、make_sharedを使用するのではなく、2つのヒープ割り当てがあります。たぶんこれは愚かなテストですが、make_sharedを使うのはnewを使うよりも時間がかかることを示していませんか?もちろん、私は時間だけについて話しています。

0
orlando