C++ コア ガイドライン:スマート ポインタの受け渡し

スマート ポインターを渡すことは、めったに取り上げられない重要なトピックです。 std::shared_ptr と std::unique_ptr を渡すための 6 つの規則があるため、これは C++ コア ガイドラインで終わります。

6 つのルールは、ソフトウェア開発のインポート ドライ (繰り返さないでください) の原則に違反しています。最終的に、ソフトウェア開発者としての私たちの生活をずっと楽にしてくれる 4 つのルールしかありません。ルールは次のとおりです。

  • R.32:unique_ptr<widget> を取る 関数が widget の所有権を前提としていることを表すパラメータ
  • R.33:unique_ptr<widget>& を取る 関数が widget を再配置することを表すパラメーター
  • R.34:shared_ptr<widget> を取る 関数が一部の所有者であることを表すパラメーター
  • R.35:shared_ptr<widget>& を取る 関数が共有ポインタを再配置する可能性があることを表すパラメータ
  • R.36:const shared_ptr<widget>& を取る オブジェクトへの参照カウントを保持する可能性があることを表すパラメータ???
  • R.37:エイリアス化されたスマート ポインターから取得したポインターまたは参照を渡さない

std::unique_ptr の最初の 2 つのルールから始めましょう。

R.32:unique_ptr<widget> を取る 関数が widget の所有権を前提としていることを表すパラメータ

関数がウィジェットの所有権を取得する必要がある場合は、std::unique_ptr をコピーして取得する必要があります。その結果、呼び出し元は std::unique_ptr を移動してコードを実行する必要があります。

#include <memory>
#include <utility>

struct Widget{
 Widget(int){}
};

void sink(std::unique_ptr<Widget> uniqPtr){
 // do something with uniqPtr
}

int main(){
 auto uniqPtr = std::make_unique<Widget>(1998);
 
 sink(std::move(uniqPtr)); // (1)
 sink(uniqPtr); // (2) ERROR
}

呼び出し (1) は問題ありませんが、std::unique_ptr をコピーできないため、呼び出し (2) が壊れます。関数がウィジェットのみを使用する場合は、ポインターまたは参照によってパラメーターを取得する必要があります。ポインターと参照の違いは、ポインターが null ポインターになる可能性があることです。

void useWidget(Widget* wid);
void useWidget(Widget& wid);

R. 33:unique_ptr<widget>&を取る 関数が widget を再配置することを表すパラメーター

関数がウィジェットを再配置したい場合があります。この使用例では、非 const 参照によって std::unique_ptr を渡す必要があります。

#include <memory>
#include <utility>

struct Widget{
 Widget(int){}
};

void reseat(std::unique_ptr<Widget>& uniqPtr){
 uniqPtr.reset(new Widget(2003)); // (0)
 // do something with uniqPtr
}

int main(){
 auto uniqPtr = std::make_unique<Widget>(1998);
 
 reseat(std::move(uniqPtr)); // (1) ERROR
 reseat(uniqPtr); // (2) 
}

ここで、右辺値を非 const 左辺値参照にバインドできないため、呼び出し (1) は失敗します。これは (2) のコピーには当てはまりません。左辺値は、左辺値参照にバインドできます。ところで。呼び出し (0) は、新しい Widget (2003) を構築するだけでなく、古い Widget (1998) も破棄します。

std::shared_ptr の次の 3 つのルールは文字通り繰り返しです。

R.34:shared_ptr<widget> 関数が部分所有者であることを表すパラメーター、R.35:shared_ptr<widget>& を取る 関数が共有ポインターを再配置する可能性があることを表すパラメーター、および R.36:Take a const shared_ptr<widget>& オブジェクトへの参照カウントを保持する可能性があることを表すパラメータ???

これが、対処しなければならない 3 つの関数シグネチャです。

void share(std::shared_ptr<Widget> shaWid);
void reseat(std::shard_ptr<Widget>& shadWid);
void mayShare(const std::shared_ptr<Widget>& shaWid);

各関数シグネチャを個別に見てみましょう。これは機能の観点から何を意味しますか?

  • void share(std::shared_ptr shaWid) :関数本体の存続期間中、私はウィジェットの共有所有者です。関数本体の先頭で、参照カウンターを増やします。関数の最後で、参照カウンターを減らします。したがって、私が使用している限り、ウィジェットは存続します。
  • void reseat(std::shared_ptr&shaWid) :参照カウンターを変更しないため、私はウィジェットの共有所有者ではありません。関数の実行中にウィジェットが存続することは保証されていませんが、リソースを再配置することはできます。 const でない左辺値参照は、より似ています:リソースを借りて、再配置できます。
  • void mayShare(const std::shared_ptr&shaWid) :リソースを借りるだけです。リソースの有効期間を延長したり、リソースを再装着したりできますか?正直なところ、std::shared_ptr を使用しても付加価値がないため、代わりにポインター (Widget*) または参照 (Widget&) をパラメーターとして使用する必要があります。

R.37:ポインターまたはエイリアス化されたスマート ポインターから取得された参照

ルールを明確にするために、短いコード スニペットを提示させてください。

void oldFunc(Widget* wid){
 // do something with wid
}

void shared(std::shared_ptr<Widget>& shaPtr){ // (2)
 
 oldFunc(*shaPtr); // (3)
 
 // do something with shaPtr
 
 }

auto globShared = std::make_shared<Widget>(2011); // (1)


...

shared(globShared); 

globShared (1) は、グローバルに共有されるポインターです。関数 shared は参照ごとに引数を取ります (2)。したがって、shaPtr の参照カウンターは増加せず、関数共有は Widget(2011) の寿命を延ばしません。問題は(3)から始まります。 oldFunc はウィジェットへのポインターを受け入れます。したがって、oldFunc は、Widget が実行中に存続するという保証はありません。 oldFunc はウィジェットのみを借用します。

治療法は非常に簡単です。関数 oldFunc を呼び出す前に、globShared の参照カウントが増加することを確認する必要があります。つまり、std::shared_ptr:

のコピーを作成する必要があります。
  • std::shared_ptr をコピーして関数 shared:に渡します。
     void shared(std::shared_ptr<Widget> shaPtr){
     
     oldFunc(*shaPtr);
     
     // do something with shaPtr
     
     } 
    
  • shared 関数で shaPtr のコピーを作成します:
     void shared(std::shared_ptr<Widget>& shaPtr){
     
     auto keepAlive = shaPtr; 
     oldFunc(*shaPtr);
     
     // do something with keepAlive or shaPtr
     
     } 
    

同じ理由が std::unique_ptr にも当てはまりますが、std::unique_ptr をコピーすることはできないため、単純な解決策は考えていません。 std::unique_ptr のクローンを作成して、新しい std::unique_ptr を作成することをお勧めします。

次は?

これは、C++ コア ガイドラインのリソース管理に関する 4 つの投稿の最後です。 C++ コア ガイドラインには、式とステートメントに関する 50 を超える規則があります。次回の投稿で詳しく見ていきます。