std::unique_ptr

std::unique_ptr は、RAII イディオムに従って、そのリソースの有効期間を自動的かつ排他的に管理します。 std::unique_ptr は、メモリやパフォーマンスのオーバーヘッドなしで機能するため、最初に選択する必要があります。

std::unique_ptr の使用法を示す前に、特徴的な箇条書きをいくつか紹介します。

std::unique_ptr

  • リソースの有無にかかわらずインスタンス化できます。
  • 単一のオブジェクトのライフサイクルを管理しますが、オブジェクトの配列です.
  • 基礎となるリソースのインターフェースを透過的に提供します。
  • 独自の削除関数でパラメーター化できます。
  • 移動できます (移動セマンティック)。
  • ヘルパー関数 std::make_unique で作成できます。

使い方

std::unique_ptr の重要な問題は、基礎となるリソースをいつ削除するかです。これは、 std::unique_ptr が範囲外になるか、新しいリソースを取得したときに正確に発生します。 2 つの使用例を次に示します。

 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
// uniquePtr.cpp

#include <iostream>
#include <memory>
#include <utility>

struct MyInt{

 MyInt(int i):i_(i){}

 ~MyInt(){
 std::cout << "Good bye from " << i_ << std::endl;
 }

 int i_;

};


int main(){

 std::cout << std::endl;

 std::unique_ptr<MyInt> uniquePtr1{ new MyInt(1998) };

 std::cout << "uniquePtr1.get(): " << uniquePtr1.get() << std::endl;

 std::unique_ptr<MyInt> uniquePtr2;
 uniquePtr2= std::move(uniquePtr1);
 std::cout << "uniquePtr1.get(): " << uniquePtr1.get() << std::endl;
 std::cout << "uniquePtr2.get(): " << uniquePtr2.get() << std::endl;

 std::cout << std::endl;


 {
 std::unique_ptr<MyInt> localPtr{ new MyInt(2003) };
 }

 std::cout << std::endl;

 uniquePtr2.reset(new MyInt(2011));
 MyInt* myInt= uniquePtr2.release();
 delete myInt;

 std::cout << std::endl;

 std::unique_ptr<MyInt> uniquePtr3{ new MyInt(2017) };
 std::unique_ptr<MyInt> uniquePtr4{ new MyInt(2022) };

 std::cout << "uniquePtr3.get(): " << uniquePtr3.get() << std::endl;
 std::cout << "uniquePtr4.get(): " << uniquePtr4.get() << std::endl;

 std::swap(uniquePtr3, uniquePtr4);

 std::cout << "uniquePtr3.get(): " << uniquePtr3.get() << std::endl;
 std::cout << "uniquePtr4.get(): " << uniquePtr4.get() << std::endl;

 std::cout << std::endl;

}

クラス MyInt (7 行目から 17 行目) は、数値の単純なラッパーです。 MyInt のライフサイクルを観察するために、11 ~ 13 行目のデストラクタを調整しました。

24 行目で std::unique_ptr を作成し、27 行目でそのリソース (new MyInt(1998)) のアドレスを返します。その後、uniquePtr1 を uniquePtr2 に移動します (29 行目)。したがって、uniquePtr2 はリソースの所有者です。これは、30 行目と 31 行目のプログラムの出力を示しています。37 行目のローカル std::unique_ptr は、スコープの終わりでその有効範囲に達しています。したがって、localPtr のデストラクタ、つまりリソー​​ス (new MyInt(2003)) のデストラクタが実行されます。これがスクリーンショットです。

最も興味深いのは 42 行目から 44 行目です。最初に、uniquePtr1 に新しいリソースを割り当てます。したがって、MyInt(1998) のデストラクタが実行されます。 43 行目のリソースが解放された後、デストラクタを明示的に呼び出すことができます。

プログラムの残りの部分は、非常に簡単に取得できます。 48 ~ 58 行で 2 つの std::unique_ptr を作成し、それらのリソースを交換します。 std::unique_ptr はコピー セマンティックをサポートしないため、std::swap は内部でムーブ セマンティックを使用します。 main 関数が終了すると、uniquePtr3 と uniquePtr4 は範囲外になり、それらのデストラクタが自動的に実行されます。

それが全体像でした。 std::unique_ptr の詳細をいくつか掘り下げてみましょう。

オブジェクトと配列の有効期間の扱い

std::unique_ptr は配列に特化しています。アクセスは完全に透過的です。つまり、std::unique_ptr がオブジェクトの有効期間を管理する場合、オブジェクト アクセスの演算子はオーバーロードされます (operator* および operator->)。 std::unique_ptr が配列の有効期間を管理する場合、インデックス演算子 operator[] がオーバーロードされます。したがって、オペレーターの呼び出しは、基礎となるリソースに完全に透過的に転送されます。

 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
// uniquePtrArray.cpp

#include <iomanip>
#include <iostream>
#include <memory>

class MyStruct{
public:
 MyStruct(){
 std::cout << std::setw(15) << std::left << (void*) this << " Hello " << std::endl;
 }
 ~MyStruct(){
 std::cout << std::setw(15) << std::left << (void*)this << " Good Bye " << std::endl;
 }
};

int main(){
 
 std::cout << std::endl;
 
 std::unique_ptr<int> uniqInt(new int(2011));
 std::cout << "*uniqInt: " << *uniqInt << std::endl;

 std::cout << std::endl;

 {
 std::unique_ptr<MyStruct[]> myUniqueArray{new MyStruct[5]};
 }

 std::cout << std::endl;

 {
 std::unique_ptr<MyStruct[]> myUniqueArray{new MyStruct[1]};
 MyStruct myStruct;
 myUniqueArray[0]=myStruct;
 }

 std::cout << std::endl;

 {
 std::unique_ptr<MyStruct[]> myUniqueArray{new MyStruct[1]};
 MyStruct myStruct;
 myStruct= myUniqueArray[0];
 }

 std::cout << std::endl;

}

22 行目で std::unique_ptr を逆参照し、そのリソースの値を取得します。

7 ~ 15 行目の MyStruct は、std::unique_ptr の配列のベースです。 MyStruct オブジェクトをインスタンス化すると、そのアドレスが取得されます。出力はデストラクタによって与えられます。これで、オブジェクトのライフ サイクルを簡単に観察できます。

26 行目から 28 行目で、MyStruct の 5 つのインスタンスを作成して破棄します。 32 行目から 36 行目はもっと興味深いです。ヒープ (33 行目) とスタック (34 行目) に MyStruct インスタンスを作成します。したがって、両方のオブジェクトが異なる範囲のアドレスを持っています。その後、ローカル オブジェクトを std::unique_pr に割り当てます (35 行目)。 40 行目から 54 行目も同様の戦略に従います。ここで、ローカル オブジェクトに myUniqueArray の最初の要素を割り当てます。 35 行目と 43 行目の std::unique_ptr へのインデックス アクセスは、おなじみの配列へのインデックス アクセスのように感じます。

ユーザー提供のデリータ

std::unique_ptr はユーザー提供のデリータを持つことができます:std::unique_ptr uniqPtr(new int(2011), intDeleter)。デリータはタイプの一部です。関数、関数オブジェクト、ラムダ関数などの呼び出し可能オブジェクトを使用できます。デリータに状態がない場合、std::unique_ptr のサイズは変更されません。デリータが、値によってコンテキストを取得する状態またはラムダ関数を持つ関数オブジェクトである場合、オーバーヘッドなしの原則はもはや成り立たなくなります。 std::shared_ptr に関する投稿で、deleter について書きます。

std::auto_ptr の置き換え

古典的な C++ には既に std::auto_ptr があります。その仕事は std::unique_ptr の仕事に似ています。 std::auto_ptr は、基になるリソースの有効期間を排他的に管理します。しかし、 std::auto_ptr は非常に奇妙です。 std::auto_ptr をコピーすると、そのリソースが移動されます。つまり、コピー セマンティックを使用した操作は、フード ムーブ セマンティックの下で実行されます。これが std::auto_ptr が推奨されない理由であり、代わりに std::unique_ptr を使用する必要があります。 std::unique_ptr は移動のみ可能で、コピーはできません。 std::unique_ptr で std::move を明示的に呼び出す必要があります。

この図は、std::auto_ptr と std::unique_ptr の違いを示しています。

次のコード スニペットを実行すると、

std::auto_ptr<int> auto1(new int(5));
std::auto_ptr<int> auto2(auto1); 

std::auto_ptr auto1 はそのリソースを失います。

std::unique_ptr はコピーできません。したがって、ムーブ セマンティックを使用する必要があります。

std::unique_ptr<int> uniqueo1(new int(5));
std::unique_ptr<int> unique2(std::move(unique1));

std::unique_ptr は STL のコンテナーに移動でき、内部でコピー セマンティックを使用しない場合は、後で STL のアルゴリズムで使用できます。

正確には。 std::auto_ptr のコピーは未定義の動作です。 std::unquiue_ptr の移動により、ソースは明確に定義された状態になりますが、正確に指定された状態にはなりません。しかし、描かれている行動はかなりありそうです。

ヘルパー関数 std::make_unique

C++11 には std::make_shared がありますが、std::make_unique はありません。これは C++14 で修正されています。 Microsoft Visual Studio 2015 は正式に C++11 をサポートしていますが、std::make_unique を使用できます。 std::make_unique のおかげで、new に手を加える必要はありません。

std::unique_ptr<int> uniqPtr1= std::make_unique<int>(2011);
auto uniqPtr2= std::make_unique<int>(2014);

std::make_unique を自動型推定と組み合わせて使用​​すると、入力が最小限に抑えられます。それは std::unique_ptr uniqPtr2 を証明します。

常に std::make_unique を使用する

std::make_unique を使用するもう 1 つの微妙な理由があります。 std::make_unique は常に正しいです。

使用する場合

func(std::make_unique<int>(2014), functionMayThrow());
func(std::unique_ptr<int>(new int(2011)), functionMayThrow());

および functionMayThrow がスローされた場合、次の可能な一連の呼び出しに対して new int(2011) でメモリ リークが発生します:

new int(2011)
functionMayThrow()
std::unique_ptr<int>(...)

次は?

次回はstd::shared_ptrについてです。したがって、この投稿は独占的所有権についてであり、次の投稿は共有所有権についてです。