std::shared_ptr

std::shared_ptr はリソースを共有します。共有参照カウンターは、所有者の数をカウントします。 std::shared_ptr をコピーすると、参照カウントが 1 つ増えます。 std::shared_ptr を破棄すると、参照カウントが 1 つ減少します。参照カウントがゼロになると、リソースは自動的に解放されます。

std::shared_ptr の詳細を扱う前に、同じページに移動して基本を説明します。

基本

std::shared_ptr をコピーすると、参照カウントが 1 つ増えます。その後、両方のスマート ポインターが同じリソースを使用します。このシナリオを描きました。

shared1 のおかげで shared2 が初期化されました。最終的に、参照カウントは 2 になり、両方のスマート ポインターが同じリソースを持ちます。

アプリケーション

このプログラムは、スマート ポインターの一般的な使用法を示しています。リソースのライフサイクルを視覚的に把握するために、MyInt のコンストラクターとデストラクタに短いメッセージを入れました (8 行目から 16 行目)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// sharedPtr.cpp

#include <iostream>
#include <memory>

using std::shared_ptr;

struct MyInt{
 MyInt(int v):val(v){
 std::cout << " Hello: " << val << std::endl;
 }
 ~MyInt(){
 std::cout << " Good Bye: " << val << std::endl;
 }
 int val;
};

int main(){

 std::cout << std::endl;

 shared_ptr<MyInt> sharPtr(new MyInt(1998));
 std::cout << " My value: " << sharPtr->val << std::endl;
 std::cout << "sharedPtr.use_count(): " << sharPtr.use_count() << std::endl;


 {
 shared_ptr<MyInt> locSharPtr(sharPtr);
 std::cout << "locSharPtr.use_count(): " << locSharPtr.use_count() << std::endl;
 }
 std::cout << "sharPtr.use_count(): "<< sharPtr.use_count() << std::endl;

 shared_ptr<MyInt> globSharPtr= sharPtr;
 std::cout << "sharPtr.use_count(): "<< sharPtr.use_count() << std::endl;
 globSharPtr.reset();
 std::cout << "sharPtr.use_count(): "<< sharPtr.use_count() << std::endl;

 sharPtr= shared_ptr<MyInt>(new MyInt(2011));

 std::cout << std::endl;
 
}

これがプログラムのスクリーンショットです。

22 行目に MyInt(1998) を作成します。これは、スマート ポインターが処理するリソースです。 sharPtr->val を使用することで、リソースに直接アクセスできます (23 行目)。プログラムの出力には、参照カウンターの数が表示されます。 24 行目で 1 から始まり、28 行目でローカル コピーの shartPtr によって 2 になり、ブロック (27 ~ 40 行目) の後で 1 に戻ります。 33 行目のリセット呼び出しとしてのコピー割り当ては、参照カウンターを変更します。 38 行目の sharPtr=shared_ptr(new MyInt(2011)) という式は、より興味深いものです。最初に、リソース MyInt(2011) が作成され、sharPtr に割り当てられます。その結果、sharPtr のデストラクタが呼び出されます。 sharedPtr はリソース new MyInt(1998) の排他的所有者でした (22 行目)。最後のリソース new MyInt(2011) はメインの最後で破棄されます。

プログラムは難しすぎてはいけません。これで、さらに深く掘り下げることができます。

コントロール ブロック

std::shared_ptr の共有は、リソースや参照カウンター以上のものです。それらはリソースと制御ブロックを共有します。制御ブロックには 2 つのカウンターがあり、最終的にはさらに多くのデータがあります。カウンター2つ?制御ブロックには、std::shared_ptr および std::shared_ptr を参照する std::weak_ptr のカウンターがあります。 std::weak_ptr について話すのはこれが初めてです。彼らの仕事は、循環参照を壊すことです。循環参照については別の記事を書きます。概要をもう一度。

制御ブロックには

  • std::shared_ptr のカウンター
  • std::weak_ptr のカウンター
  • 特別なデリータやアロケータなどの最終的なデータ

std::shared_ptr をそのリソースと一緒に作成する場合、2 つの割り当てが必要です。 1 つはリソース用で、もう 1 つは制御ブロック用です。 std::make_shared は 2 つの割り当てのうち 1 つを作成するため、より高速で (スマート ポインターのメモリとパフォーマンスのオーバーヘッドを参照)、安全です。 std::shared_ptr(new int(2011)) に対するこの安全性の保証はありません。 std::shared_ptr(new int(2011)) でスマート ポインターを作成すると、割り当ての 1 つが失敗し、メモリ リークが発生する可能性があります。

std::shared_ptr は、特別なデリータによってパラメータ化できます。この投稿の次のセクションでは、まさにそれが起こります。

デリーター

std::shared_ptr のデリータは、型のコンポーネントではない std::unique_ptr のデリータと反対です。したがって、std::vector> に異なるデリータを使用して std::shared_ptr を非常に簡単にプッシュできます。特別なデリータは制御ブロックに格納されます。

次の例では、既に解放されたメモリの量を記録する特別な std::shared_ptr を作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// sharedPtrDeleter.cpp

#include <iostream>
#include <memory>
#include <random>
#include <typeinfo>

template <typename T>
class Deleter{
public:
 void operator()(T *ptr){
 ++Deleter::count;
 delete ptr;
 }
 void getInfo(){
 std::string typeId{typeid(T).name()};
 size_t sz= Deleter::count * sizeof(T);
 std::cout << "Deleted " << Deleter::count << " objects of type: " << typeId << std::endl;
 std::cout <<"Freed size in bytes: " << sz << "." << std::endl;
 std::cout << std::endl;
 }
private:
 static int count;
};

template <typename T>
int Deleter<T>::count=0;

typedef Deleter<int> IntDeleter;
typedef Deleter<double> DoubleDeleter;

void createRandomNumbers(){

 std::random_device seed;

 std::mt19937 engine(seed());

 std::uniform_int_distribution<int> thousand(1,1000);
 int ranNumber= thousand(engine);
 for ( int i=0 ; i <= ranNumber; ++i) std::shared_ptr<int>(new int(i),IntDeleter());

}

int main(){

 std::cout << std::endl;

 {
 std::shared_ptr<int> sharedPtr1( new int,IntDeleter() );
 std::shared_ptr<int> sharedPtr2( new int,IntDeleter() );
 auto intDeleter= std::get_deleter<IntDeleter>(sharedPtr1);
 intDeleter->getInfo();
 sharedPtr2.reset();
 intDeleter->getInfo();

 }
 createRandomNumbers();
 IntDeleter().getInfo();

 {
 std::unique_ptr<double,DoubleDeleter > uniquePtr( new double, DoubleDeleter() );
 std::unique_ptr<double,DoubleDeleter > uniquePtr1( new double, DoubleDeleter() );
 std::shared_ptr<double> sharedPtr( new double, DoubleDeleter() );

 std::shared_ptr<double> sharedPtr4(std::move(uniquePtr));
 std::shared_ptr<double> sharedPtr5= std::move(uniquePtr1);
 DoubleDeleter().getInfo();
 }

 DoubleDeleter().getInfo();

}

8 行目から 27 行目の Deleter は特殊な Deleter です。デリータは、型 T によってパラメータ化されます。それは、呼び出し演算子 (11 ~ 14 行目) が使用された頻度を静的変数カウント (23 行目) でカウントします。 Deleter はすべての情報を getInfo で返します (15 ~ 21 行目)。関数 createRandomNumbers (行 32 ~ 42) は、特別なデリータ intDeleter() によってパラメータ化された 1 ~ 1000 の std::shared_ptr (行 40) を作成します。

intDeleter->getInfo() の最初の使用は、リソースが解放されていないことを示しています。これは、53 行目の sharedPtr2.reset() の呼び出しで変更されます。4 バイトの int 変数が解放されています。 57 行目の createRandomNumbers() 呼び出しは、74 個の std::shared_ptr を作成します。もちろん、std::unique_ptr (60 ~ 68 行目) にはデリータを使用できます。 double オブジェクトのメモリは、68 行目のブロックの終了後に解放されます。

次は?

std::shared_ptr には他にもたくさんの機能があります。 std:.shared_ptr を既存のオブジェクトに作成できます。 std::shared_ptr には最小限のマルチスレッド保証があります。しかし、1つの質問はまだ答えられていません。あなたの関数は、値または参照によって std::shared_ptr を取る必要がありますか?次の投稿で答えを見つけてください。