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
定義する必要はそのフィールドのデストラクタが自動的に呼び出されるため、初期ポインタまたはノードの寿命が終了すると、リスト全体がスマートポインタデストラクタによって再帰的に削除されます。
この再帰型は、プレーンデータへのスマートポインターの場合に見えにくいいくつかのケースについて説明する機会を与えます。また、関数自体がクライアントコードの例を(再帰的に)提供することもあります。のtypedef list
はもちろんに偏って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
の移動のコンストラクタで右辺値参照引数に初期化子リストでバインドをunique_ptr<node>
別名、list
初期化時に、next
のフィールドを生成されますnode
)、そしてなぜそれが例外セーフであるかについての質問に対して(再帰的な割り当てプロセス中にメモリが不足し、new
throwsの呼び出しがあったstd::bad_alloc
場合、その時点で部分的に構築されたリストへのポインタは匿名で型の一時に保持されますlist
初期化リスト用に作成され、そのデストラクタがその部分的なリストをクリーンアップします)。ちなみに、2つ目を(最初に行ったように)置き換える代わりにnullptr
、p
結局、その時点ではnullであることがわかっています。nullであることがわかっている場合でも、(raw)ポインターから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:(変更可能な)左辺値参照でスマートポインターを渡す
ここでは、関数はスマートポインターへの変更可能な参照を持つ必要がありますが、関数で何が行われるかを示していません。このメソッドをカードで呼び出したいのですが、呼び出し側はクレジットカード番号を提供して支払いを保証します。参照は、ポイントされたオブジェクトの所有権を取得するために使用できますが、そうする必要はありません。このモードでは、変更可能な左辺値引数を指定する必要があります。これは、関数の望ましい効果には、引数変数に有用な値を残すことが含まれる場合があるという事実に対応しています。そのような関数に渡したい右辺値式を持つ呼び出し元は、言語が暗黙的な変換のみを提供するため、呼び出しを行うことができるように、それを名前付き変数に格納することを強制されます。定数右辺値からの左辺値参照(一時的なものを参照)。(で処理される反対の状況とは異なり、スマートポインタータイプを使用したto std::move
からのキャストは不可能です。それでも、この変換は、必要に応じて単純なテンプレート関数で取得できます。https://stackoverflow.com/a/24868376を参照してください。 / 1436796)。呼び出された関数が無条件にオブジェクトの所有権を取得し、引数を盗もうとする場合、左辺値引数を提供する義務は誤ったシグナルを与えています。変数は呼び出し後に有用な値を持ちません。したがって、モード3は関数内で同じ可能性を提供しますが、呼び出し元に右辺値を提供するように要求しますが、そのような使用法には推奨されます。Y&&
Y&
Y
ただし、モード2には有効な使用例があります。つまり、ポインターを変更する可能性のある関数、または所有権を含む方法でポイントされたオブジェクトです。たとえば、ノードの前にaを付ける関数list
は、そのような使用例を提供します。
void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }
呼び出し元にstd::move
スマートポインターがまだ明確に定義された空でないリストを所有しているため、呼び出し元が以前とは異なるものであるため、ここでは呼び出し元にを強制することは明らかに望ましくありません。
繰り返しになりprepend
ますが、空きメモリがないために呼び出しが失敗した場合に何が起こるかを観察することは興味深いことです。次に、new
呼び出しがスローされstd::bad_alloc
ます。この時点では、node
割り当てられなかったため、から渡された右辺値参照(モード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
ポインタを返すが、元のヌルになる)前に reset
(それが保持している古い値を破棄したときに、そのノードを破壊する(暗黙的に)p
ことを保証します) 1と唯一のノードがその時に破壊されます。(コメントで言及されている代替形式では、このタイミングはstd::unique_ptr
インスタンスの移動割り当て演算子の実装の内部に任されますlist
。規格では、この演算子は「あたかもreset(u.release())
"を呼び出すと、ここでもタイミングが安全になるはずです。)
ことを注意prepend
してremove_first
ローカル保存クライアントによって呼び出すことはできませんnode
特定の実装では、このような場合のために働くことができなかったので、当然のように常に非空のリストのための変数を、と。
モード3:(変更可能な)右辺値参照によってスマートポインターを渡す
これは、単にポインタの所有権を取得するときに使用する優先モードです。チェックによってこのメソッド呼び出しを呼び出したい:呼び出し元は、小切手に署名することによって、現金を提供するかのように所有権を放棄することを受け入れる必要がありますが、実際の引き出しは、呼び出された関数が実際にポインターを盗むまで延期されます(モード2を使用する場合とまったく同じです) )。「チェックの署名」とは、具体的std::move
には、それが左辺値の場合(モード1のように)呼び出し元が引数をラップする必要があることを意味します(右辺値の場合、「所有権の放棄」の部分は明白であり、個別のコードは必要ありません)。
技術的には、モード3はモード2とまったく同じように動作するため、呼び出された関数が所有権を持つ必要はありません。しかし、私は(通常の使用で)所有権移転についての不確実性がある場合は、モード2、モード3を使用すると、暗黙のうちに彼らがいることを発信者への信号であるように、モード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)
です。(で使用したコンストラクタはstd::tr1
)変更可能取る左辺値(ちょうど同じ参照auto_ptr<T>&
コピーコンストラクタ)を、したがってと呼ばれることができるauto_ptr<T>
左辺値p
のようにstd::shared_ptr<T> q(p)
、その後、p
nullにリセットされています。引数の受け渡しでモード2から3に変更されたため、この古いコードを書き換える必要std::shared_ptr<T> q(std::move(p))
があり、引き続き機能します。委員会はここではモード2を好まなかったと理解していますが、次のように定義することで、モード1に変更するオプションがありました。std::shared_ptr<T>(auto_ptr<T> p)
代わりに、(一意のポインターとは異なり)自動ポインターを暗黙的に値に逆参照できるため(ポインターオブジェクト自体がプロセスでnullにリセットされるため)、古いコードを変更せずに機能させることができます。どうやら委員会はモード1よりもモード3を支持することを非常に好んだため、すでに非推奨の使用法であっても、モード1を使用するのではなく、既存のコードを積極的に中断することを選択しました。
モード1よりもモード3を優先する場合
モード1は多くの場合完全に使用可能であり、所有権の仮定がreversed
上記の例のようにスマートポインターをローカル変数に移動するという形を取る場合は、モード3よりも優先される場合があります。ただし、より一般的なケースでモード3を選択する理由は2つあります。
参照を渡す方が、一時的なものを作成して古いポインタを取り除くよりも少し効率的です(キャッシュの処理はやや面倒です)。シナリオによっては、ポインタが実際に盗まれる前に、変更されずに数回、別の関数に渡される場合があります。このような受け渡しには通常、書き込みが必要ですstd::move
(モード2が使用されている場合を除く)。ただし、これは実際には何も実行しないキャスト(特に逆参照なし)であるため、コストはかかりません。
関数呼び出しの開始と、そのオブジェクト(または含まれている呼び出し)が実際にポイントされたオブジェクトを別のデータ構造に移動するポイントとの間に何かが例外をスローすることが考えられる場合(この例外は、関数自体の内部でまだ捕捉されていません) )、モード1を使用すると、スマートポインターによって参照されるオブジェクトは、catch
句が例外を処理する前に破棄されます(関数のパラメーターがスタックの巻き戻し中に破棄されたため)。ただし、モード3を使用している場合はそうではありません。後者は、呼び出し元は、そのような場合(例外をキャッチすることにより)オブジェクトのデータを回復するオプションを持っています。ここでモード1を指定してもメモリリークは発生しませんが、プログラムのデータが回復不能に失われる可能性があり、これも望ましくない場合があります。
スマートポインターを返す:常に値渡し
おそらく、呼び出し元が使用するために作成されたオブジェクトを指す、スマートポインターを返すことについて一言で締めくくります。これは本当に機能へのポインタを渡すと同等のケースではありませんが、完全を期すために、私はそのような場合であることを主張したいと思います常に値で返す(および使用しない std::move
でreturn
声明)。おそらくnixされたばかりのポインタへの参照を取得したくはありません。