1. ホーム
  2. c++

[解決済み] C++ - std::shared_ptr または boost::shared_ptr への参照の渡し方

2022-05-15 10:21:02

質問

もし、ある関数が 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> を参照ではなく値で渡す正当な理由が見当たりません。値で渡すと、コピーによって参照カウントが一時的に増加し、関数スコープを終了するときに減少するだけです。 私は何かを見落としているのでしょうか?

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

どのように解決するのですか?

明確な 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() がヌルポインタのために吹き飛ばされないとどうしてわかるのでしょうか?

それはすべて、コードの「...」セクションに何があるかによるのです。最初の「...」セクションで何かを呼び出すと、(コードの別の部分のどこかで)その副作用として 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;
}

(この例ではコンソールに"send"するだけです).

今度は、前のメッセージを記憶するための機能を追加したいと思います。次のような動作が必要です: 最近送信されたメッセージを含む変数が存在しなければなりませんが、メッセージが現在送信されている間は、前のメッセージは存在してはなりません (変数は送信前にリセットされるべきです)。そこで、新しい変数を宣言します。

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_messageshared_ptr :

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


に変更できるのは明らかです。

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

これがもたらすパフォーマンスの向上について考えてみましょう! (私たちはあるチャンネルで典型的な大きなメッセージを送信しようとしているので、パフォーマンスの向上は測定不可能なほど小さくなることは気にしないでください)。

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

Maintainer 氏はこのことに驚きましたが、防御的なチェックを send_message に防御的なチェックを加え、この問題の発生を阻止しようとしました。

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

しかし、もちろんそれでもクラッシュしてしまいます。 msg は決して null ではないからです。 send_message が呼び出されたとき、決して null にはなりません。

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

簡単な解決策としては、関数が shared_ptr がずっと非NULLであり続けることに依存したい場合、簡単な解決策は、関数が自分自身の真の shared_ptr への参照に依存するのではなく、既存の shared_ptr .

欠点は、コピーされた shared_ptr は自由ではありません。quot;lock-free" の実装でさえ、スレッド保証を守るためにインターロックされたオペレーションを使わなければなりません。そのため、プログラム中に shared_ptrshared_ptr & . しかし、これはすべてのプログラムに対して安全に行える変更ではありません。プログラムの論理的な意味を変えてしまうのです。

同じようなバグは std::string の代わりに std::shared_ptr<std::string> の代わりに、そして

previous_message = 0;

をクリアにするために、私たちは言いました。

previous_message.clear();

そうすると、症状は未定義の動作ではなく、空のメッセージを誤って送信してしまうことになります。非常に大きな文字列の余分なコピーの代償は、例えば shared_ptr をコピーするコストよりもはるかに大きいかもしれないので、トレードオフは異なるかもしれません。