web-dev-qa-db-ja.com

C ++-std :: shared_ptrまたはboost :: shared_ptrへの参照を渡す

shared_ptrで動作する必要がある関数がある場合、それへの参照を渡す方が効率的ではありませんか(shared_ptrオブジェクトのコピーを避けるため)?考えられる悪い副作用は何ですか?次の2つのケースを想定しています。

1)関数内で、引数のコピーが作成されます。

ClassA::take_copy_of_sp(boost::shared_ptr<foo> &sp)  
{  
     ...  
     m_sp_member=sp; //This will copy the object, incrementing refcount  
     ...  
}  

2)関数内では、引数は次のようにのみ使用されます

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}  

どちらの場合も、boost::shared_ptr<foo>を参照ではなく値で渡す正当な理由がわかりません。値を渡すと、コピーのために参照カウントが「一時的に」増加し、関数スコープを終了するときに参照カウントが減少します。私は何かを見落としていますか?

明確にするために、いくつかの答えを読んだ後、私は時期尚早の最適化の懸念に完全に同意し、常にホットスポットで最初にプロファイルを作成しようとします。あなたが私が何を意味するか知っていれば、私の質問は純粋に技術的なコードの観点からのものでした。

113
abigagli

別個のshared_ptrインスタンスのポイントは、このshared_ptrがスコープ内にある限り、その参照カウントが少なくとも1になるため、それが指すオブジェクトがまだ存在することを(可能な限り)保証することです。

Class::only_work_with_sp(boost::shared_ptr<foo> sp)
{
    // sp points to an object that cannot be destroyed during this function
}

したがって、shared_ptrへの参照を使用すると、その保証が無効になります。 2番目の場合:

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}

sp->do_something()がNULLポインターのために爆破しないことをどのように知っていますか?

これらはすべて、コードの「...」セクションの内容によって異なります。 shared_ptrを同じオブジェクトにクリアするという副作用(コードの別の部分)がある最初の '...'の間に何かを呼び出すとどうなりますか?そして、それがたまたまそのオブジェクトに残っている唯一の別個のshared_ptrだったらどうなるでしょうか?さようならオブジェクト、ちょうどあなたがそれを試して使用しようとしている場所。

したがって、その質問に答えるには2つの方法があります。

  1. 関数本体でオブジェクトが死なないことが確実になるまで、プログラム全体のソースを慎重に調べてください。

  2. パラメータを変更して、参照ではなく個別のオブジェクトに戻します。

ここに当てはまる一般的なアドバイス:プロファイラーで現実的な状況で製品のタイミングを計り、行いたい変更が最終的にもたらすものであると最終的に測定するまで、パフォーマンスのためにコードに危険な変更を加えないでくださいパフォーマンスの大きな違い。

コメンターJQの更新

これが不自然な例です。意図的にシンプルなので、間違いは明らかです。実際の例では、実際の詳細の層に隠れているため、間違いはそれほど明白ではありません。

どこかにメッセージを送信する関数があります。大きなメッセージになる可能性があるため、複数の場所に渡されるときにコピーされる可能性が高いstd::stringを使用するのではなく、文字列にshared_ptrを使用します。

void send_message(std::shared_ptr<std::string> msg)
{
    std::cout << (*msg.get()) << std::endl;
}

(この例では、コンソールに「送信」するだけです)。

次に、前のメッセージを記憶する機能を追加します。次の動作が必要です。最後に送信されたメッセージを含む変数が存在する必要がありますが、現在メッセージが送信されている間は、前のメッセージはありません(送信前に変数をリセットする必要があります)。したがって、新しい変数を宣言します。

std::shared_ptr<std::string> previous_message;

次に、指定した規則に従って関数を修正します。

void send_message(std::shared_ptr<std::string> msg)
{
    previous_message = 0;
    std::cout << *msg << std::endl;
    previous_message = msg;
}

したがって、送信を開始する前に現在の前のメッセージを破棄し、送信が完了した後、新しい前のメッセージを保存できます。すべて良い。テストコードは次のとおりです。

send_message(std::shared_ptr<std::string>(new std::string("Hi")));
send_message(previous_message);

そして予想どおり、これはHi!を2回出力します。

さて、メンテナー氏がやって来ます。メンテナーはコードを見て、次のように考えています。ねえ、send_messageのパラメーターはshared_ptrです

void send_message(std::shared_ptr<std::string> msg)

明らかにそれは次のように変更できます:

void send_message(const std::shared_ptr<std::string> &msg)

これがもたらすパフォーマンスの向上を考えてください! (通常、いくつかのチャネルを介して大きなメッセージを送信しようとしているので、パフォーマンスの向上は非常に小さいため、測定できません)。

しかし、実際の問題は、テストコードが未定義の動作を示すようになることです(Visual C++ 2010デバッグビルドでは、クラッシュします)。

メンテナー氏はこれに驚いていますが、問題の発生を止めるためにsend_messageに防御チェックを追加します。

void send_message(const std::shared_ptr<std::string> &msg)
{
    if (msg == 0)
        return;

ただし、send_messageが呼び出されたときにmsgがnullになることはないため、もちろん先に進み、クラッシュします。

私が言うように、些細な例ではすべてのコードが非常に接近しているので、間違いを見つけるのは簡単です。しかし、実際のプログラムでは、相互にポインターを保持する可変オブジェクト間のより複雑な関係があるため、間違いを簡単にmake作成し、間違いを検出するために必要なテストケース。

簡単な解決策は、関数がshared_ptrに依存し続けることを望む場合、関数が既存のshared_ptrへの参照に依存するのではなく、真のshared_ptrを割り当てることです。

欠点は、コピーされたshared_ptrは無料ではないということです。「ロックフリー」の実装でさえ、インターロック操作を使用してスレッド化の保証を尊重する必要があります。そのため、shared_ptrshared_ptr &に変更することにより、プログラムを大幅に高速化できる場合があります。しかし、これはすべてのプログラムに安全に行える変更ではありません。プログラムの論理的な意味を変更します。

std::stringの代わりに、そして次の代わりにstd::shared_ptr<std::string>を使用すると、同様のバグが発生することに注意してください。

previous_message = 0;

メッセージをクリアするには、次のように言いました。

previous_message.clear();

症状は、未定義の動作ではなく、誤って空のメッセージを送信することです。非常に大きな文字列の余分なコピーのコストは、shared_ptrのコピーのコストよりもはるかに大きいため、トレードオフが異なる場合があります。

108

私は自分が最高得票の答えに同意しないことに気付いたので、専門家の意見を探しに行きました。から http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything

ハーブサッター:「shared_ptrを渡すと、コピーは高価になります」

スコット・マイヤーズ:「値で渡すか、参照で渡すかについては、shared_ptrに関して特別なことはありません。他のユーザー定義型に使用するのとまったく同じ分析を使用してください。すべての管理上の問題、およびそれが小さいため、値渡しは必然的に安価です。コピーする必要があり、それに関連するコストがかかります。値渡しするのは高価です。私のプログラムで適切なセマンティクスを使用して、代わりにconstまたはreferenceへの参照で渡します」

Herb Sutter:「常にconstへの参照で渡します。非常にまれに、呼び出し元が参照元を変更する可能性があることを知っているため、値渡しする場合があります。パラメータとしてコピーする場合は、とにかく、その参照カウントは生き続けているので、参照カウントを上げる必要はほとんどありません。参照で渡す必要があります。

更新:ハーブはここでこれを拡張しました: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ 、ただし物語の教訓はshared_ptrを「所有権を共有または譲渡するなど、スマートポインター自体を使用または操作する場合を除いて」渡すことはできません。

111
Jon

あなたと一緒に仕事をする他のプログラマーが本当にあなたが何をしているのかを知らない限り、私はこの慣習に反対することをお勧めします。

まず、クラスへのインターフェースがどのように進化するかわからないため、他のプログラマーが悪いことをしないようにしたい。 shared_ptrを参照渡しすることは、プログラマーが期待するものではありません。慣用的ではなく、誤って使用しやすいからです。防御的なプログラム:インターフェースを誤って使用しにくくします。参照渡しは、後で問題を引き起こすだけです。

第二に、この特定のクラスが問題になることがわかるまで最適化しないでください。最初にプロファイルを作成し、次に参照渡しによるプログラムが本当に必要な場合は、多分。そうでない場合は、小さなもの(つまり、値渡しに必要な余分なN命令)を気にせずに、代わりに設計、データ構造、アルゴリズム、および長期的な保守性を心配します。

22
Mark Kegel

はい、参照を取得しても問題ありません。メソッドに所有権を共有するつもりはありません。それだけで働きたい。とにかくそれをコピーするので、最初のケースの参照を取ることもできます。しかし、最初のケースでは、所有権を取ります。まだ一度だけコピーするには、このトリックがあります:

void ClassA::take_copy_of_sp(boost::shared_ptr<foo> sp) {
    m_sp_member.swap(sp);
}

また、返却時にコピーする必要があります(つまり、参照を返却しない)。あなたのクラスはクライアントがそれを使って何をしているのかわからないからです(クラスへのポインタを保存するとビッグバンが発生する可能性があります)。後でそれがボトルネックであることが判明した場合(最初のプロファイル!)、参照を返すことができます。


編集:もちろん、他の人が指摘しているように、これはコードを知っていて、渡された共有ポインターを何らかの方法でリセットしないことがわかっている場合にのみ当てはまります。疑わしい場合は、値で渡します。

shared_ptrconst&sを渡すのが賢明です。おそらく問題を引き起こすことはありません(Earwickerで詳述されているように、参照されたshared_ptrが関数呼び出し中に削除されるというまれなケースを除く)。覚えておいてください。デフォルトのboost::shared_ptrはスレッドセーフであるため、コピーするとスレッドセーフな増分が含まれます。

一時オブジェクトは非const参照によって渡されない可能性があるため、const&だけでなく&を使用するようにしてください。 (MSVCの言語拡張機能により、とにかくそれを行うことができます)

11
Magnus Hoff

2番目の場合、これを行うのは簡単です。

Class::only_work_with_sp(foo &sp)
{    
    ...  
    sp.do_something();  
    ...  
}

あなたはそれを呼び出すことができます

only_work_with_sp(*sp);
10
Greg Rogers

Const参照によって共有ポインターを渡すことを推奨します-ポインターで渡される関数がポインターを所有しないというセマンティクスは、開発者にとってクリーンなイディオムです。

唯一の落とし穴は、共有ポインタによってポイントされているオブジェクトが別のスレッドで破壊される複数のスレッドプログラムにあります。そのため、共有ポインタのconst参照を使用することは、シングルスレッドプログラムでは安全であると言えます。

非const参照による共有ポインタの受け渡しは危険な場合があります。その理由は、関数が戻った後でも有効と見なされるオブジェクトを破棄するために、関数が内部で呼び出すスワップおよびリセット関数です。

それは時期尚早な最適化に関するものではない、私は推測します-あなたが何をしたいのか明確で、コーディングイディオムが仲間の開発者によってしっかりと採用されたとき、それはCPUサイクルの不必要な浪費を避けることについてです。

ちょうど私の2セント:-)

3
zmqiu

ここでのすべての長所と短所は、shared_ptrだけでなく、参照によって渡される任意の型に実際に一般化できるようです。私の意見では、参照渡し、const参照、値の意味を理解し、正しく使用する必要があります。しかし、すべての参照が悪いと思わない限り、shared_ptrを参照で渡すことに本質的に問題はありません...

例に戻るには:

_Class::only_work_with_sp( foo &sp ) //Again, no copy here  
{    
    ...  
    sp.do_something();  
    ...  
}
_

sp.do_something()が宙ぶらりんのポインターのために爆発しないことをどうやって知っていますか?

真実は、shared_ptrか否か、constかどうかに関係なく、スレッド間でspの所有権を直接または間接的に共有し、_delete this_ 、循環所有権またはその他の所有権エラーがあります。

3
Sandy

関数が明示的にポインターを変更しない限り、「プレーン」参照を避けます。

const &は、小さな関数を呼び出すときの賢明なマイクロ最適化かもしれません。いくつかの条件をインライン化するなど、さらなる最適化を可能にします。また、増分/減分は(スレッドセーフなので)同期ポイントです。ただし、ほとんどのシナリオでこれが大きな違いをもたらすとは思わないでしょう。

一般的に、理由がない限り、よりシンプルなスタイルを使用する必要があります。次に、const &一貫して、またはいくつかの場所でそれを使用する場合の理由に関するコメントを追加します。

3
peterchen

まだ言及していないことの1つは、参照によって共有ポインターを渡すと、派生クラスの共有ポインターを基本クラスの共有ポインターへの参照を介して渡したい場合に得られる暗黙の変換が失われることです。

たとえば、このコードはエラーを生成しますが、test()を変更して、共有ポインターが参照によって渡されないようにすると機能します。

#include <boost/shared_ptr.hpp>

class Base { };
class Derived: public Base { };

// ONLY instances of Base can be passed by reference.  If you have a shared_ptr
// to a derived type, you have to cast it manually.  If you remove the reference
// and pass the shared_ptr by value, then the cast is implicit so you don't have
// to worry about it.
void test(boost::shared_ptr<Base>& b)
{
    return;
}

int main(void)
{
    boost::shared_ptr<Derived> d(new Derived);
    test(d);

    // If you want the above call to work with references, you will have to manually cast
    // pointers like this, EVERY time you call the function.  Since you are creating a new
    // shared pointer, you lose the benefit of passing by reference.
    boost::shared_ptr<Base> b = boost::dynamic_pointer_cast<Base>(d);
    test(b);

    return 0;
}
2
Malvineous

時期尚早の最適化 に精通しており、アカデミックの目的か、パフォーマンスの低い既存のコードを分離したためにこれを求めていると仮定します。

参照渡しでも大丈夫

Const参照による受け渡しの方が適切であり、通常は使用できます。これは、ポイントされたオブジェクトにconst-nessを強制しないためです。

参照を使用しているためにポインターを失うリスクがありますnot。その参照は、スタックの初期にスマートポインターのコピーがあり、1つのスレッドだけが呼び出しスタックを所有しているため、既存のコピーが消えないという証拠です。

参照の使用は、頻繁により効率的です、あなたが言及する理由のために、しかし保証されません。オブジェクトの逆参照にも作業が必要になる場合があることに注意してください。理想的な参照使用シナリオは、コーディングスタイルに多くの小さな関数が含まれ、ポインタが使用される前に関数から関数に渡される場合です。

スマートポインターを参照として保存しないように常に回避する必要がありますClass::take_copy_of_sp(&sp)の例は、その正しい使用法を示しています。

1
Drew Dormann

Constの正確性に関係ないと仮定すると(または、関数が渡されるデータの所有権を変更または共有できるようにすることを意味します)、boost :: shared_ptrを値で渡すことは、参照で渡すよりも安全です元のboost :: shared_ptrが自身の有効期間を制御できるようにします。次のコードの結果を考慮してください...

void FooTakesReference( boost::shared_ptr< int > & ptr )
{
    ptr.reset(); // We reset, and so does sharedA, memory is deleted.
}

void FooTakesValue( boost::shared_ptr< int > ptr )
{
    ptr.reset(); // Our temporary is reset, however sharedB hasn't.
}

void main()
{
    boost::shared_ptr< int > sharedA( new int( 13 ) );
    boost::shared_ptr< int > sharedB( new int( 14 ) );

    FooTakesReference( sharedA );

    FooTakesValue( sharedB );
}

上記の例から、参照によってsharedAを渡すと、FooTakesReferenceが元のポインターをリセットでき、使用カウントが0に減り、データが破壊されることがわかります。 FooTakesValueただし、元のポインターをリセットすることはできず、sharedB'sデータが引き続き使用可能であることが保証されます。別の開発者が必然的にやって来てsharedA's脆弱な存在に便乗しようとすると、混乱が続きます。しかし、幸運なsharedB開発者は、すべてが彼の世界で正しいので、早く帰宅します。

この場合、コードの安全性は、コピーが作成する速度の改善をはるかに上回ります。同時に、boost :: shared_ptrはコードの安全性を向上させることを目的としています。この種のニッチ最適化が必要な場合は、コピーから参照に移動するのがはるかに簡単になります。

1
Kit10

私は、スマートポインターを値で渡すという原則が非常に強いというプロジェクトで働いていました。パフォーマンス分析を行うように依頼されたとき、スマートポインターの参照カウンターの増分と減分のために、アプリケーションが使用されているプロセッサー時間の4〜6%を費やすことがわかりました。

ダニエル・アーウィッカーから説明されているように、奇妙なケースで問題が発生するのを避けるために、値でスマートポインターを渡したい場合は、支払う価格を必ず確認してください。

参照を使用することにした場合、const参照を使用する主な理由は、インターフェイスで使用するクラスを継承するクラスからオブジェクトに共有ポインターを渡す必要があるときに暗黙のアップキャストを可能にするためです。

1
gsf

Sandyは次のように書いています。「ここのすべての長所と短所は、shared_ptrだけでなく、参照によって渡される任意の型に実際に一般化できるようです」

ある程度は正しいですが、shared_ptrを使用するポイントは、オブジェクトの有効期間に関する懸念を排除し、コンパイラにそれを処理させることです。参照によって共有ポインターを渡し、参照カウントされたオブジェクトのクライアントがオブジェクトデータを解放する可能性のある非constメソッドを呼び出すことを許可する場合、共有ポインターの使用はほとんど無意味です。

パフォーマンスが懸念される可能性があるため、前の文で「ほぼ」を書きましたが、まれに正当化される可能性がありますが、自分でこのシナリオを避けて、真剣に見るなど、他のすべての可能な最適化ソリューションを探します別のレベルの間接参照、遅延評価などを追加します。

作者の過去に存在するコード、または作者のメモリを投稿するコードは、動作、特にオブジェクトの有効期間に関する暗黙の仮定を必要とし、明確で簡潔で読みやすいドキュメントを必要とし、多くのクライアントはそれを読まないでしょう!シンプルさはほとんどの場合効率よりも優先され、ほとんどの場合、効率を上げる方法は他にもあります。参照カウントオブジェクト(および等号演算子)のコピーコンストラクターによるディープコピーを回避するために、参照によって値を渡す必要がある場合は、ディープコピーデータを参照カウントポインターにする方法を検討する必要があります。すぐにコピーしました。 (もちろん、それはあなたの状況に当てはまらないかもしれないただ一つの設計シナリオです)。

1
Bill H

Litbが言ったことに加えて、おそらく2番目の例ではconst参照で渡されることを指摘したいので、誤って変更しないようにしてください。

0
Leon Timmermans
struct A {
  shared_ptr<Message> msg;
  shared_ptr<Message> * ptr_msg;
}
  1. 値渡し:

    void set(shared_ptr<Message> msg) {
      this->msg = msg; /// create a new shared_ptr, reference count will be added;
    } /// out of method, new created shared_ptr will be deleted, of course, reference count also be reduced;
    
  2. 参照渡し:

    void set(shared_ptr<Message>& msg) {
     this->msg = msg; /// reference count will be added, because reference is just an alias.
     }
    
  3. ポインターで渡す:

    void set(shared_ptr<Message>* msg) {
      this->ptr_msg = msg; /// reference count will not be added;
    }
    
0
Dylan Chen

すべてのコードには何らかの意味がなければなりません。アプリケーションで値everywhereで共有ポインタを渡す場合、これは"他の場所で何が起こっているかわからないため、生の安全性を優先します"を意味します。これは、コードを参照できる他のプログラマーにとって、私が良い自信を示すものではありません。

とにかく、関数がconst参照を取得し、「不明」な場合でも、関数の先頭に共有ポインターのコピーを作成して、ポインターに強力な参照を追加できます。これは、デザインに関するヒントと見なすこともできます(「ポインターは他の場所で変更できます」)。

はい、IMO、デフォルトは「constリファレンスによるパス」である必要があります。

0
Philippe