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


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

多くのグーグルとスタックオーバーフローの投稿がこれにありますがmake_shared、直接使用するよりもなぜ効率的であるのか理解できませんshared_ptr

誰かが私にどのようmake_sharedに効率的であるかを理解できるように、作成されたオブジェクトのシーケンスと両方で行われた操作を順を追って説明してくれませんか?参考のために、上記の例を1つ示しました。


4
それはより効率的ではありません。これを使用する理由は、例外的な安全のためです。
Yuushi

いくつかの記事は、それはいくつかの建設のオーバーヘッドを回避します、これについてもっと説明していただけませんか?
Anup Buchke 2014年

16
@Yuushi:例外の安全性はそれを使用する十分な理由ですが、より効率的でもあります。
マイクシーモア

3
32:15は、私が上記にリンクしたビデオで彼が始まるところです。
クリスヤン

4
マイナーコードスタイルの利点:を使用しmake_sharedて記述できauto p1(std::make_shared<A>())、p1は正しい型になります。
Ivan Vergiliev 2014年

回答:


333

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

ヒープ割り当てはどこで発生しますか?

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

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

std::make_shared制御ブロックとデータの両方に必要なスペースを考慮して、単一のヒープ割り当てを実行します。もう1つのケースでnew Obj("foo")は、管理対象データのヒープ割り当てを呼び出し、std::shared_ptrコンストラクターが制御ブロックに対して別の割り当てを実行します。

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

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

注(2019/08/30):関数の引数の評価順序が変更されたため、これはC ++ 17以降の問題ではありません。特に、関数の各引数は、他の引数を評価する前に完全に実行する必要があります。

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つは次のとおりです。

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

weak_ptrsのインスタンスが制御ブロックを存続させるのはなぜですか?

weak_ptr管理対象オブジェクトがまだ有効かどうかを判断する方法が必要です(例:for lock)。shared_ptrこれは、制御ブロックに格納されている管理対象オブジェクトを所有するの数をチェックすることによって行われます。その結果、制御ブロックは、shared_ptrカウントとweak_ptrカウントの両方が0になるまで有効です。

戻る std::make_shared

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

代わりにnewshared_ptrコンストラクターを介して制御ブロックと管理対象オブジェクトに2つのヒープ割り当てを実行したとします。次に、shared_ptrs が存在しない場合は管理対象オブジェクトのメモリを解放し(多分早い)、s が存在しない場合は制御ブロックのメモリを解放します(多分後で)weak_ptr


53
コーナーケースの小さな欠点についても言及することをお勧めしmake_sharedます。割り当てが1つしかないため、制御ブロックが使用されなくなるまで、指示先のメモリの割り当てを解除できません。A weak_ptrは、制御ブロックを無期限に存続させることができます。
Casey

14
もう1つのより文体的なポイントは、次のとおりです。一貫して使用するmake_sharedmake_unique、生のポインターを所有できず、すべての発生をnewコードのにおいとして扱うことができます。
フィリップ

6
が1つしかshared_ptrなく、weak_ptrs がない場合reset()shared_ptrインスタンスを呼び出すと、制御ブロックが削除されます。しかし、これはmake_shared使用されたかどうかに関係なくです。管理オブジェクトに割り当てられたメモリのmake_shared寿命を延ばすことができるので、を使用すると違いが生じます。ときにshared_ptrカウントが0に当たる、管理対象オブジェクトのデストラクタは関わらず呼び出されmake_sharedますが、あればそのメモリを解放することのみ行うことができmake_sharedではない使用します。これがより明確になることを願っています。
mpark 2014年

4
また、make_sharedは、制御ブロックをより小さなポインターにできる「We Know Where You You Live」最適化を利用できることにも言及する価値があります。(詳細については、Stephan T. LavavejのGN2012プレゼンテーション( 12分頃)を参照してください)make_sharedは、割り当てを回避するだけでなく、割り当てられる総メモリも少なくなります。
KnowItAllWannabe 2014年

1
@HannaKhalil:これはおそらく、あなたが探しているものの領域ですか?melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGH
mpark

26

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

この効率だけでなく、使用make_sharedするとnew、ポインターをそのまま処理する必要がなくなり、例外の安全性が向上します。オブジェクトを割り当てた後、スマートポインターに割り当てる前に例外をスローする可能性はありません。


2
最初のポイントを正しく理解しました。例外の安全性について2番目の点について詳しく説明したり、リンクを付けたりできますか?
Anup Buchke 2014年

22

すでに述べたものに加えて、2つの可能性が異なる別のケースがあります:非公開コンストラクター(保護またはプライベート)を呼び出す必要がある場合、make_sharedはそれにアクセスできない可能性がありますが、新しいバリアントは正常に機能します。

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){}
};

私はこの正確な問題に遭遇し、使用することに決めましたnew、そうでなければ私は使用しmake_sharedたでしょう。これについての関連質問は次のとおりです:stackoverflow.com/questions/8147027/…
jigglypuff、2018年

6

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


2
make_sharedが不適切な2番目の状況は、カスタムの削除機能を指定する場合です。
KnowItAllWannabe 2014年

5

std :: make_sharedに問題が1つあります。プライベート/保護されたコンストラクタをサポートしていません


3

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

  1. 制御ブロック(参照カウント)
  2. 管理されているオブジェクト

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

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

0

効率と割り当てに費やされる時間について、以下の簡単なテストを行いました。これらの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を使用するよりも時間がかかることを示していませんか?もちろん、私は時間だけを話しているのです。


4
そのテストはやや無意味です。テストは最適化されたリリース構成で行われましたか?また、すべてのアイテムがすぐに解放されるため、現実的ではありません。
Phil1970 2017

0

mpark氏の回答の例外的な安全性の部分は依然として有効な懸念事項だと思います。shared_ptr <T>(new T)のようなshared_ptrを作成すると、新しいTは成功する可能性がありますが、shared_ptrの制御ブロックの割り当ては失敗する可能性があります。このシナリオでは、shared_ptrがインプレースで作成されたことを知る方法がなく、削除しても安全であるため、新しく割り当てられたTはリークします。または私は何かを逃していますか?関数パラメーターの評価に関するより厳密なルールは、ここでは決して役に立たないと思います...

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.