C++ コア ガイドライン:スマート ポインタのルール

スマート ポインターが C++11 の最も重要な機能であると言う C++ の専門家はたくさんいました。今日は、C++ のスマート ポインターについて書きます。

C++ コア ガイドラインには、スマート ポインターに関する 13 の規則があります。それらの半分は、所有者のセマンティックを扱います。そのうちの半分は、共有ポインタを関数に渡すにはどうすればよいですか?

ルールの概要は次のとおりです。

  • R.20:unique_ptr を使用 または shared_ptr 所有権を表す
  • R.21:unique_ptr を優先 shared_ptr以上 所有権を共有する必要がない場合
  • R.22:make_shared() を使用 shared_ptr にする
  • R.23:make_unique() を使用 unique_ptr にする
  • R.24:std::weak_ptr を使用 shared_ptr の循環を断ち切る
  • R.30:ライフタイム セマンティクスを明示的に表現するためだけにスマート ポインターをパラメーターとして使用する
  • R.31:std 以外の場合 スマート ポインター、std の基本パターンに従います
  • 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:エイリアス化されたスマート ポインターから取得したポインターまたは参照を渡さない

最初の 5 つのルール (R.20 - R.24 ) は非常に明白です。私はそれらについていくつかの記事を書きました。ルールを言い換えて、以前の投稿を参照させてください。

std::unique_ptr はそのリソースの排他的所有者です。したがって、コピーすることはできず、移動のみを行うことができます。対照的に、std::shared_pointer は所有権を共有します。共有ポインターをコピーまたはコピー割り当てすると、参照カウンターが自動的に増加します。共有ポインタを削除またはリセットすると、参照カウンタが減少します。参照カウンターがゼロになると、基になるリソースが削除されます。この管理オーバーヘッドのため、可能であれば std::unique_ptr を使用する必要があります (R.21 ).

このオーバーヘッドは、std::shared_ptr を作成する場合に特に当てはまります。 std::shared_ptr を作成するには、リソースと参照カウンターの割り当てが必要です。これは、合計すると非常に高価な作業です。したがって、ファクトリ関数 std::make_shared (R.22 )。 std::make_shared は 1 つの割り当てのみを行います。これにより、std::shared_ptr のパフォーマンスが大幅に向上します。投稿「共有ポインターのメモリとパフォーマンスのオーバーヘッド」の比較では、生のポインターと、ファクトリ関数 std::make_shared および std::make_unique を含む共有ポインターの作成と削除の違い。

std::make_shared で std::shared_ptr を作成し、std::make_unique で std::unique_ptr を作成する重要な理由がもう 1 つあります。メモリ リークはありません (R.22 および R.23) )。 1 つの式で std::shared_ptr または std::unique_ptr の 2 つの呼び出しを使用すると、例外が発生した場合にメモリ リークが発生する可能性があります。この問題の詳細については、前回の記事「C++ Core Guidelines:Rules for Allocating and Deallocating (R.13)」を参照してください。

正直なところ、std::weak_ptr はスマート ポインターではありません。 std::weak_ptr は所有者ではなく、その std::shared_ptr からリソースのみを貸与します。そのインターフェースは非常に限られています。 std::weak_ptr でメソッド ロックを使用することにより、std::weak_ptr を std::shared_ptr に持ち上げることができます。もちろん、質問があります:なぜ std::weak_ptr が必要なのですか? std::weak_ptr は std::shared_ptr の循環を断ち切るのに役立ちます (R.24) .これらのサイクルが理由です。 std::shared_ptr はそのリソースを自動的に解放しません。あるいは逆に言えば。共有ポインタのサイクルがある場合、メモリ リークが発生します。 std::weak_ptr の詳細と、それらを使用して std::shared_ptr でメモリ リークを克服する方法については、以前の投稿 std::weak_ptr を参照してください。

これで、スマート ポインターの概要が完成しました。これは、多かれ少なかれスマート ポインターに関する一般的な知識です。これは、残りのルールには当てはまりません。彼らは次の質問に対処します:関数への共有ポインタをどのように渡す必要がありますか?

R.30:明示的に有効期間を表すためだけにスマート ポインターをパラメーターとして使用するセマンティクス

このルールは少しトリッキーです。スマート ポインターをパラメーターとして関数に渡し、この関数でスマート ポインターの基になるリソースのみを使用すると、問題が発生します。この場合、スマート ポインターの有効期間セマンティックを使用しないため、ポインターまたは参照を関数パラメーターとして使用する必要があります。

スマート ポインターの非常に洗練された有効期間管理の例を挙げましょう。

// lifetimeSemantic.cpp

#include <iostream>
#include <memory>

void asSmartPointerGood(std::shared_ptr<int>& shr){
 std::cout << "shr.use_count(): " << shr.use_count() << std::endl; // (3)
 shr.reset(new int(2011)); // (5)
 std::cout << "shr.use_count(): " << shr.use_count() << std::endl; // (4)
}

void asSmartPointerBad(std::shared_ptr<int>& shr){
 // doSomethingWith(*shr);
 *shr += 19;
}

int main(){
 
 std::cout << std::endl;
 
 auto firSha = std::make_shared<int>(1998);
 auto secSha = firSha;
 std::cout << "firSha.use_count(): " << firSha.use_count() << std::endl; // (1)
 
 std::cout << std::endl;
 
 asSmartPointerGood(firSha); // (2)
 
 std::cout << std::endl;
 
 std::cout << "*firSha: " << *firSha << std::endl;
 std::cout << "firSha.use_count(): " << firSha.use_count() << std::endl;
 
 std::cout << std::endl;
 
 std::cout << "*secSha: " << *secSha << std::endl;
 std::cout << "secSha.use_count(): " << secSha.use_count() << std::endl;
 
 std::cout << std::endl;
 
 asSmartPointerBad(secSha); // (6)
 std::cout << "*secSha: " << *secSha << std::endl;
 
 std::cout << std::endl;
 
}

std::shared_ptr の適切なケースから始めます。行 (1) の参照カウンターは 2 です。これは、共有ポインター firSha を使用して初期化された secSha をコピーしたためです。関数 asSmartPointerGood (2) の呼び出しを詳しく見てみましょう。まず(3)shrの参照回数が2で、(4)で1になる。行 (5) で何が起こったのですか? shr を新しいリソース new int(2011) にリセットします。その結果、共有ポインター firSha と secSha の両方が、すぐに異なるリソースの共有所有者になります。スクリーンショットで動作を確認できます。

共有ポインターでリセットを呼び出すと、内部で魔法が起こります。

  • 引数なしでリセットを呼び出すと、参照カウンターが 1 つ減ります。
  • 引数を指定してリセットを呼び出し、参照カウンターが少なくとも 2 の場合、異なるリソースを所有する 2 つの独立した共有ポインターを取得します。これは、共有ポインタの一種のディープ コピーです。
  • 引数の有無にかかわらずリセットを呼び出し、参照カウンターが 0 になると、リソースは解放されます。

共有ポインタの基になるリソースにのみ関心がある場合、この魔法は必要ありません。したがって、ポインターまたは参照は関数 asSmartPointerBad (6) の正しい種類のパラメーターです。

詳細情報

また、Bartek F. による最近の投稿も参照してください。weak_ptr が完全なメモリ クリーンアップを妨げる状況について:How a weak_ptr may prevent full memory cleanup of a managed object.

次は?

関数にスマート ポインターを渡すには、6 つのルールが残っています。というわけで、次の投稿で何を書きますか。