std::unique_ptr
クラス テンプレートのインスタンスによってメモリが管理されるオブジェクトにポインターを渡すさまざまな実行可能なモードを述べてみましょう。それは古いクラス テンプレートにも適用されますstd::auto_ptr
(一意のポインターが行うすべての使用を許可すると思いますが、さらに、を呼び出す必要なく、右辺値が期待される場所で変更可能な左辺値が受け入れられますstd::move
) std::shared_ptr
。
議論の具体例として、次の単純なリストタイプを検討します
struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }
このようなリストのインスタンス (他のインスタンスと部分を共有したり、循環したりすることはできません) は、最初のlist
ポインターを保持する人によって完全に所有されます。格納するリストが決して空にならないことをクライアント コードが認識している場合、クライアント コードnode
は、list
. のデストラクタをnode
定義する必要はありません。そのフィールドのデストラクタは自動的に呼び出されるため、初期ポインタまたはノードの有効期間が終了すると、リスト全体がスマート ポインタ デストラクタによって再帰的に削除されます。
この再帰型は、プレーン データへのスマート ポインターの場合にはあまり目立たないいくつかのケースについて説明する機会を与えてくれます。また、関数自体が (再帰的に) クライアント コードの例を提供することもあります。の typedeflist
はもちろん に偏っていますが、定義は useまたはにunique_ptr
変更することができますが、以下に述べる内容をあまり変更する必要はありません (特に、デストラクタを記述する必要なく保証される例外の安全性に関して)。auto_ptr
shared_ptr
スマート ポインターを渡すモード
モード 0: スマート ポインターの代わりにポインターまたは参照引数を渡す
関数が所有権に関係ない場合は、これが推奨される方法です。スマート ポインターをまったく使用しないでください。この場合、関数は、指しているオブジェクトの所有者や、所有権がどのような方法で管理されているかを気にする必要がないため、生のポインターを渡すことは完全に安全であり、最も柔軟な形式です。所有権に関係なく、クライアントは常に生のポインターを生成します (メソッドを呼び出すかget
、アドレス演算子から&
)。
たとえば、そのようなリストの長さを計算する関数には、list
引数ではなく、生のポインターを与える必要があります。
size_t length(const node* p)
{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; }
変数を保持するクライアントは、list head
この関数を として呼び出すことができますが、代わりに空でないリストを表すlength(head.get())
を格納することを選択したクライアントは、 を呼び出すことができます。node n
length(&n)
ポインターが非 null であることが保証されている場合 (リストが空である可能性があるため、ここではそうではありません)、ポインターではなく参照を渡すことを好むかもしれません。const
関数がノードのコンテンツを追加または削除せずにノードのコンテンツを更新する必要がある場合は、 non- へのポインター/参照である可能性があります (後者には所有権が含まれます)。
モード 0 のカテゴリに該当する興味深いケースは、リストの (深い) コピーを作成することです。これを行う関数はもちろん、作成したコピーの所有権を譲渡する必要がありますが、コピーしているリストの所有権には関係ありません。したがって、次のように定義できます。
list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }
このコードは、なぜそれがコンパイルされるのかという疑問のために、よく見る価値があります (イニシャライザ リスト内の への再帰呼び出しの結果は、のフィールドを初期化するときに、別名copy
のムーブ コンストラクタ内の右辺値参照引数にバインドされます)。 generated )、およびなぜそれが例外セーフであるかについての質問に対して (再帰的な割り当てプロセス中にメモリが不足し、throwsの呼び出しが発生した場合、その時点で、部分的に構築されたリストへのポインターが一時的な型に匿名で保持されます初期化子リスト用に作成され、そのデストラクタはその部分的なリストをクリーンアップします)。ちなみに、(私が最初に行ったように)2番目を次のように置き換えたいという誘惑に抵抗する必要があります。unique_ptr<node>
list
next
node
new
std::bad_alloc
list
nullptr
p
、これは結局、その時点で null であることがわかっています。null であることがわかっている場合でも、(生の) ポインターから constant へのスマート ポインターを構築することはできません。
モード 1: スマート ポインターを値で渡す
スマート ポインター値を引数として受け取る関数は、指しているオブジェクトをすぐに取得します。呼び出し元が保持していたスマート ポインター (名前付き変数または無名一時変数のいずれか) は、関数の入り口で引数値にコピーされ、呼び出し元のポインターが null になりました (一時的な場合、コピーは省略された可能性がありますが、いずれにせよ、呼び出し元は指定されたオブジェクトへのアクセスを失いました)。このモード呼び出しを現金で呼び出したいと思います。呼び出し元は、呼び出したサービスに対して前払いし、呼び出し後に所有権について幻想を持つことはできません。これを明確にするために、言語規則では、呼び出し元が引数をラップする必要があります。std::move
スマート ポインターが変数に保持されている場合 (技術的には、引数が左辺値の場合)。この場合 (ただし、以下のモード 3 の場合は除きます)、この関数はその名前が示すように、値を変数から一時変数に移動し、変数を null のままにします。
呼び出された関数がポイント先のオブジェクトの所有権を無条件に取得する (盗む) 場合、このモードを使用するstd::unique_ptr
かstd::auto_ptr
、所有権と共にポインターを渡すのに適した方法です。これにより、メモリ リークのリスクが回避されます。それにもかかわらず、以下のモード 3 がモード 1 よりも優先されない (ほんの少しだけ) 状況はごくわずかだと思います。このため、このモードの使用例は提供しません。(ただし、reversed
以下のモード 3 の例を参照してください。ここでは、モード 1 も少なくとも同様に機能することが示されています。) 関数がこのポインターだけでなくより多くの引数を取る場合、モードを回避するための技術的な理由がさらにある可能性があります。 1 (std::unique_ptr
またはを使用std::auto_ptr
): 実際の移動操作はポインタ変数の受け渡し中に行われるためp
式によって、が他の引数を評価している間 (評価の順序が指定されていない) 有用な値を保持してstd::move(p)
いると仮定することはできず、p
微妙なエラーにつながる可能性があります。対照的に、モード 3 を使用すると、関数呼び出しの前に移動が行われないことが保証されるp
ため、他の引数は を介して安全に値にアクセスできますp
。
で使用するとstd::shared_ptr
、このモードは興味深い点で、単一の関数定義を使用して、関数で使用される新しい共有コピーを作成しながら、呼び出し元がポインターの共有コピーを保持するかどうかを選択できます(これは、左辺値が引数が提供される; 呼び出しで使用される共有ポインターのコピー コンストラクターが参照カウントを増やす)、またはポインターを保持したり参照カウントに触れたりせずに関数にポインターのコピーを与えるだけである (これは、右辺値引数が提供された場合に発生する可能性があります)。の呼び出しでラップされた左辺値std::move
)。例えば
void f(std::shared_ptr<X> x) // call by shared cash
{ container.insert(std::move(x)); } // store shared pointer in container
void client()
{ std::shared_ptr<X> p = std::make_shared<X>(args);
f(p); // lvalue argument; store pointer in container but keep a copy
f(std::make_shared<X>(args)); // prvalue argument; fresh pointer is just stored away
f(std::move(p)); // xvalue argument; p is transferred to container and left null
}
void f(const std::shared_ptr<X>& x)
同じことは、(左辺値の場合) と(右辺値の場合) を別々に定義することで実現できます。void f(std::shared_ptr<X>&& x)
関数本体は、最初のバージョンがコピー セマンティクスを呼び出す (を使用する場合はコピーの構築/代入を使用するx
) という点だけが異なりますが、2 番目のバージョンはセマンティクスを移動します。 (std::move(x)
コード例のように、代わりに書き込みます)。そのため、共有ポインターの場合、コードの重複を避けるためにモード 1 が役立ちます。
モード 2: (変更可能な) 左辺値参照によってスマート ポインターを渡す
ここで、関数はスマート ポインターへの変更可能な参照を持つことだけを必要としますが、それで何をするかについては何も示しません。このメソッドcall by cardを呼び出したいと思います。呼び出し元は、クレジット カード番号を指定して支払いを保証します。参照は、指定されたオブジェクトの所有権を取得するために使用できますが、そうする必要はありません。このモードでは、変更可能な左辺値引数を提供する必要があります。これは、関数の目的の効果には、引数変数に有用な値を残すことが含まれる可能性があるという事実に対応しています。そのような関数に渡したい右辺値式を持つ呼び出し元は、呼び出しを行うことができるように名前付き変数に格納する必要があります。これは、言語が定数への暗黙的な変換しか提供しないためです。右辺値からの左辺値参照 (一時参照)。( によって処理される反対の状況とは異なり、からへのスマート ポインター型でstd::move
のキャストは不可能です。それにもかかわらず、この変換は、本当に必要な場合は単純なテンプレート関数によって取得できます。https://stackoverflow.com/a/24868376を参照してください)。 /1436796 )。呼び出された関数が引数を盗んで無条件にオブジェクトの所有権を取得しようとする場合、左辺値引数を提供する義務は間違ったシグナルを与えています: 変数は呼び出し後に有用な値を持たなくなります。したがって、関数内で同じ可能性を提供しますが、呼び出し元に右辺値を提供するように要求するモード 3 は、そのような使用法に優先する必要があります。Y&&
Y&
Y
ただし、モード 2 には有効な使用例があります。つまり、ポインターを変更する可能性のある関数、または所有権を伴う方法で指されたオブジェクトです。たとえば、ノードを の前に付ける関数は、list
そのような使用例を提供します。
void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }
ここで、呼び出し元に を強制的に使用させるのは明らかに望ましくありませんstd::move
。呼び出し後もスマート ポインターは明確に定義された空でないリストを保持していますが、以前とは異なります。
prepend
ここでも、空きメモリが不足して呼び出しが失敗した場合に何が起こるかを観察するのは興味深いことです。次に、new
呼び出しがスローされstd::bad_alloc
ます。この時点では nonode
を割り当てることができなかったため、渡された rvalue 参照 (モード 3)std::move(l)
はまだ盗まれていないことが確実です。これは、割り当てに失敗した のnext
フィールドを構築するために行われるからです。node
したがって、エラーがスローされたとき、元のスマート ポインターl
は元のリストを保持しています。そのリストは、スマート ポインター デストラクタによって適切に破棄されるかl
、十分に早い節のおかげで生き残る必要がある場合はcatch
、元のリストを保持します。
これは建設的な例でした。この質問にウィンクすると、指定された値を含む最初のノードがあれば、それを削除するというより破壊的な例を示すこともできます。
void remove_first(int x, list& l)
{ list* p = &l;
while ((*p).get()!=nullptr and (*p)->entry!=x)
p = &(*p)->next;
if ((*p).get()!=nullptr)
(*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next);
}
ここでも、正確さは非常に微妙です。特に、最後のステートメントでは、削除されるノード内に保持されているポインターは、そのノードを (暗黙的に) 破棄する前に(*p)->next
(によって保持されている古い値を破棄するときに)リンクが解除されます (によってrelease
、ポインターは返されますが、元の nullになります)。その時点で 1つのノードのみが破棄されます。(コメントで言及されている別の形式では、このタイミングはインスタンスの move-assignment 演算子の実装の内部に委ねられます。標準では 20.7.1.2.3;2 とあり、この演算子は「あたかも"を呼び出しているため、ここでもタイミングは安全です。) reset
p
std::unique_ptr
list
reset(u.release())
prepend
andは、常に空でないリストremove_first
のローカル変数を格納するクライアントからは呼び出せないことに注意してください。これは、指定された実装がそのような場合に機能しないためです。node
モード 3: (変更可能な) 右辺値参照によってスマート ポインターを渡す
これは、単にポインターの所有権を取得する場合に使用する優先モードです。このメソッド呼び出しを小切手で呼び出したいと思います: 呼び出し元は、小切手に署名することによって、現金を提供するかのように所有権の放棄を受け入れる必要がありますが、実際の引き出しは、呼び出された関数が実際にポインターを盗むまで延期されます (モード 2 を使用する場合とまったく同じように) )。「チェックの署名」とは、具体的には、呼び出し元がstd::move
左辺値の場合に (モード 1 のように) 引数をラップする必要があることを意味します (右辺値の場合、「所有権を放棄する」部分は明らかであり、別のコードは必要ありません)。
技術的には、モード 3 はモード 2 とまったく同じように動作するため、呼び出された関数は所有権を引き受ける必要がないことに注意してください。ただし、(通常の使用で) 所有権の譲渡に不確実性がある場合は、モード 3 よりもモード 2 を使用することをお勧めします。これにより、モード 3 を使用することで、発信者が所有権を放棄していることを暗示的に示すことができます。モード 1 の引数の受け渡しだけが、実際には呼び出し側に所有権の強制的な喪失を通知していると反論する人もいるかもしれません。しかし、クライアントが呼び出された関数の意図について疑問を持っている場合、クライアントは呼び出されている関数の仕様を知っているはずであり、それによって疑いが取り除かれます。
list
モード 3 の引数渡しを使用する型を含む典型的な例を見つけるのは驚くほど困難です。b
リストを別のリストの最後に移動することa
は典型的な例です。ただし、a
(操作の結果を保持して保持する) モード 2 を使用して渡す方が適切です。
void append (list& a, list&& b)
{ list* p=&a;
while ((*p).get()!=nullptr) // find end of list a
p=&(*p)->next;
*p = std::move(b); // attach b; the variable b relinquishes ownership here
}
モード 3 引数の受け渡しの純粋な例は次のとおりです。これは、リスト (およびその所有権) を受け取り、逆の順序で同一のノードを含むリストを返します。
list reversed (list&& l) noexcept // pilfering reversal of list
{ list p(l.release()); // move list into temporary for traversal
list result(nullptr);
while (p.get()!=nullptr)
{ // permute: result --> p->next --> p --> (cycle to result)
result.swap(p->next);
result.swap(p);
}
return result;
}
この関数はl = reversed(std::move(l));
、リストを反転してそれ自体にするために のように呼び出すことができますが、反転したリストは別の方法で使用することもできます。
ここでは、効率のために引数がすぐにローカル変数に移動されます (引数l
を の代わりに直接使用することもできますp
が、毎回それにアクセスすると、余分なレベルの間接化が必要になります)。したがって、モード 1 引数の受け渡しとの違いは最小限です。実際、そのモードを使用すると、引数はローカル変数として直接機能する可能性があるため、最初の移動を回避できます。これは、参照によって渡された引数がローカル変数を初期化するためだけに機能する場合、代わりに値によって渡し、パラメーターをローカル変数として使用するという一般原則の単なる例です。
モード 3 を使用することは、モード 3 を使用してスマート ポインターの所有権を転送するすべての提供されたライブラリ関数によって証明されるように、標準によって提唱されているようですstd::shared_ptr<T>(auto_ptr<T>&& p)
。そのコンストラクターは (in でstd::tr1
) 変更可能な左辺値参照 (コピー コンストラクターと同様) を取得するために使用されたため、 inのように左辺値auto_ptr<T>&
で呼び出すことができ、その後null にリセットされました。引数の受け渡しがモード 2 から 3 に変更されたため、この古いコードを書き直す必要があり、その後も引き続き機能します。委員会がここでモード 2 を好まなかったことは理解していますが、モード 1 に変更するオプションがありました。auto_ptr<T>
p
std::shared_ptr<T> q(p)
p
std::shared_ptr<T> q(std::move(p))
std::shared_ptr<T>(auto_ptr<T> p)
代わりに、古いコードが変更なしで機能することを保証できたはずです。これは、(一意のポインターとは異なり) 自動ポインターは値に対して暗黙的に逆参照できるためです (ポインター オブジェクト自体はプロセスで null にリセットされます)。どうやら、委員会はモード 1 よりもモード 3 を提唱することを非常に好んだため、すでに非推奨の使用法であってもモード 1 を使用するのではなく、既存のコードを積極的に壊すことを選択したようです。
モード 1 よりもモード 3 を好む場合
多くの場合、モード 1 は完全に使用可能であり、上記の例のように、所有権がスマート ポインターをローカル変数に移動する形式をとる場合は、モード 3 よりも優先される可能性がありますreversed
。ただし、より一般的なケースでモード 3 を好む理由が 2 つあります。
一時ポインタを作成して古いポインタを nix するよりも、参照を渡す方がわずかに効率的です (キャッシュの処理はやや面倒です)。シナリオによっては、ポインターが実際に盗まれる前に、別の関数に変更されずに数回渡されることがあります。このような受け渡しには通常、書き込みが必要ですがstd::move
(モード 2 が使用されない限り)、これは単なるキャストであり、実際には何も行わない (特に逆参照を行わない) ため、コストはゼロであることに注意してください。
関数呼び出しの開始と、それ (または含まれている呼び出し) が実際にポイント先のオブジェクトを別のデータ構造に移動するポイントとの間に何かが例外をスローすることが考えられるはずです (この例外は、関数自体の内部でまだキャッチされていません)。 )、モード 1 を使用する場合、スマート ポインターによって参照されるオブジェクトは、catch
節が例外を処理できるようになる前に破棄されます (スタックの巻き戻し中に関数パラメーターが破棄されたため) が、モード 3 を使用する場合はそうではありません。後者は、そのような場合、呼び出し元には、(例外をキャッチすることによって) オブジェクトのデータを回復するオプションがあります。ここでのモード 1はメモリ リークを引き起こしませんが、プログラムの回復不能なデータ損失につながる可能性があることに注意してください。これも望ましくない場合があります。
スマート ポインターを返す: 常に値渡し
呼び出し元が使用するために作成されたオブジェクトを指していると思われるスマート ポインターを返すことについて一言で締めくくります。これは、ポインタを関数に渡す場合と実際には比較できるケースではありませんが、完全を期すために、そのような場合は常に値で返すことを主張したいと思います(ステートメントでは使用しないでください)。 多分ちょうど nix されたばかりのポインターへの参照を取得したいと思う人はいません。std::move
return