unique_ptr引数をコンストラクタまたは関数に渡すにはどうすればよいですか?


400

C ++ 11でセマンティクスを移動するのは初めてですがunique_ptr、コンストラクターまたは関数でパラメーターを処理する方法がよくわかりません。このクラスが自分自身を参照していると考えてください。

#include <memory>

class Base
{
  public:

    typedef unique_ptr<Base> UPtr;

    Base(){}
    Base(Base::UPtr n):next(std::move(n)){}

    virtual ~Base(){}

    void setNext(Base::UPtr n)
    {
      next = std::move(n);
    }

  protected :

    Base::UPtr next;

};

これは私がunique_ptr引数を取る関数をどのように書くべきですか?

そしてstd::move、呼び出しコードで使用する必要がありますか?

Base::UPtr b1;
Base::UPtr b2(new Base());

b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?


1
空のポインターでb1-> setNextを呼び出しているため、セグメンテーション違反ではありませんか?
balki 2013年

回答:


836

以下は、一意のポインタを引数として取る可能な方法と、それらに関連する意味です。

(A)値

Base(std::unique_ptr<Base> n)
  : next(std::move(n)) {}

ユーザーがこれを呼び出すには、次のいずれかを実行する必要があります。

Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr<Base>(new Base(...));

一意のポインターを値で取得するということは、問題の関数/オブジェクト/その他にポインターの所有権を転送することを意味します。newBase構築後、空であるnextBaseことが保証されます。あなたはオブジェクトを所有しておらず、そのオブジェクトへのポインタさえもありません。なくなった。

パラメータを値で取得するため、これは確実です。std::move実際にはないには何も移動。それは単なる派手なキャストです。へのr値参照であるをstd::move(nextBase)返します。それだけです。Base&&nextBase

Base::Base(std::unique_ptr<Base> n)引数はr値参照ではなく値で取得されるため、C ++は自動的に一時変数を作成します。これは、作成std::unique_ptr<Base>からBase&&、我々はを通じて機能を与えたことstd::move(nextBase)ます。実際に値を関数の引数に移動するのは、この一時的な構造ですnextBasen

(B)非const l値参照

Base(std::unique_ptr<Base> &n)
  : next(std::move(n)) {}

これは、実際のl値(名前付き変数)で呼び出す必要があります。次のように一時的に呼び出すことはできません。

Base newBase(std::unique_ptr<Base>(new Base)); //Illegal in this case.

これの意味は、非const参照の他の使用の意味と同じです。関数は、そうでない場合があります。は、ポインターの所有権を要求があります。このコードを考えると:

Base newBase(nextBase);

nextBase空であるという保証はありません。それあり空です。そうでないかもしれません。それは本当に何に依存しますBase::Base(std::unique_ptr<Base> &n)たいます。そのため、関数のシグネチャから何が起きるかは明らかではありません。実装(または関連ドキュメント)を読む必要があります。

そのため、これをインターフェースとしてお勧めしません。

(C)const l値参照

Base(std::unique_ptr<Base> const &n);

から移動できないため、実装は示していませんconst&。を渡すことによりconst&、関数Baseはポインターを介してにアクセスできるが、どこにも格納ことを意味します。それの所有権を主張することはできません。

これは便利です。必ずしも特定のケースに当てはまるわけではありませんが、誰かにポインタを渡すことができ、それができないことを知っていることは常に良いことです(C ++の規則に違反せずにキャストしないようにせずにconst)所有権を主張。保管できません。彼らはそれを他の人に渡すことができますが、それらの他の人は同じルールに従う必要があります。

(D)r値参照

Base(std::unique_ptr<Base> &&n)
  : next(std::move(n)) {}

これは、「非const l値参照」の場合とほぼ同じです。違いは2つあります。

  1. 一時的に渡すことができます:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
  2. 非一時的な引数を渡すときに使用する必要ありstd::moveます。

後者が本当に問題です。この行が表示された場合:

Base newBase(std::move(nextBase));

この行が完了すると、nextBase空になるはずであると合理的に期待しています。から移動する必要がありました。結局のところ、あなたはstd::moveそこに座って、動きが起こったことを告げています。

問題は、そうなっていないことです。からの移動は保証されていませ。移動された可能性がありますが、ソースコードを見るだけでわかります。関数のシグネチャだけではわかりません。

推奨事項

  • (A)値渡し:関数がの所有権を主張することを意味する場合は、unique_ptr値渡しにします。
  • (C)const l値参照:関数unique_ptrがその関数の実行期間中に単にを使用することを意味する場合、それをで取得しconst&ます。または、a を使用するのではなく、a &またはconst&を指す実際の型に渡しunique_ptrます。
  • (D)r値参照:関数が(内部コードパスに応じて)所有権を主張する場合としない場合がある場合は、でそれを取得し&&ます。しかし、可能な限りこれを行わないことを強くお勧めします。

unique_ptrを操作する方法

をコピーすることはできませんunique_ptr。移動のみできます。これを行う適切な方法は、std::move標準ライブラリ関数を使用することです。

unique_ptrby値を取得すると、そこから自由に移動できます。しかし、実際にはのために動きは起こりませんstd::move。次のステートメントを見てください。

std::unique_ptr<Base> newPtr(std::move(oldPtr));

これは実際には2つのステートメントです。

std::unique_ptr<Base> &&temporary = std::move(oldPtr);
std::unique_ptr<Base> newPtr(temporary);

(注:非一時的なr値の参照は実際にはr値ではないため、上記のコードは技術的にコンパイルされません。これはデモ目的でのみここにあります)。

temporaryちょうどr値の基準ですoldPtr。動きが発生する場所のコンストラクタnewPtrあります。unique_ptrのmoveコンストラクター(&&それ自体を取得するコンストラクター)が実際の動作を行います。

あなたが持っている場合はunique_ptr値を、あなたはどこかにそれを保存したい、あなたがしなければならない使用std::moveのストレージを行うこと。


5
@Nicol:ただしstd::move、戻り値には名前を付けません。名前付き右辺値参照は左辺値であることを忘れないでください。ideone.com/VlEM3
R.

31
私は基本的にこの回答に同意しますが、いくつかの発言があります。(1)const lvalueへの参照を渡すための有効なユースケースはないと思います:呼び出し先がそれで実行できるすべてのこと、それはconst(ベア)ポインタへの参照でも行うことができます。所有権がaを通じて保持されていることを知ることは、そのビジネスのどれでもありませんunique_ptr。おそらくいくつかの他の発信者は、同じ機能を必要とするが、保持しているshared_ptrと呼ばれる機能があれば左辺値参照による代わりに](2)呼び出しが役立つ可能性が修正されたリンクリストから、(リスト所有)のノードの追加や削除、例えば、ポインタを。
Marc van Leeuwen、2014年

8
...(3)右辺値参照で渡すよりも、値で渡すことを優先するあなたの議論は理にかなっていますが、標準自体は常にunique_ptr右辺値参照で値を渡します(たとえば、それらをに変換する場合shared_ptr)。その理由は、呼び出し元にまったく同じ権限を与える一方で、一時的なポインタへの移動は行われず、少し効率的である可能性があります(右辺値またはでラップされた左辺値を渡すことstd::moveができますが、ネイキッド左辺値は渡せません)。
Marc van Leeuwen

19
Marcの発言を繰り返し、Sutterを引用すると、「パラメーターとしてconst unique_ptr&を使用しないでください。代わりにwidget *を使用してください」
Jon

17
渡しの問題を発見しまし -移動は引数の初期化中に行われます。これは、他の引数評価に関して順序付けされていません(もちろん、initializer_listを除く)。一方、右辺値参照を受け入れると、関数呼び出しの後で、したがって他の引数の評価後に移動が発生するように強く指示されます。したがって、所有権が取得されるときは常に、右辺値参照を受け入れることをお勧めします。
Ben Voigt 2015

57

std::unique_ptrクラステンプレートのインスタンスによってメモリが管理されているオブジェクトにポインタを渡すさまざまな実行可能なモードについて説明します。これは、古いstd::auto_ptrクラステンプレートにも適用されます(これにより、一意のポインターが行うすべての使用が許可されると考えられますが、それに加えて、変更可能な左辺値は、右辺値が期待される場所で受け入れられ、を呼び出す必要はありませんstd::movestd::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_ptrshared_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)、そしてなぜそれが例外セーフであるかについての質問に対して(再帰的な割り当てプロセス中にメモリが不足し、newthrowsの呼び出しがあったstd::bad_alloc場合、その時点で部分的に構築されたリストへのポインタは匿名で型の一時に保持されますlist初期化リスト用に作成され、そのデストラクタがその部分的なリストをクリーンアップします)。ちなみに、2つ目を(最初に行ったように)置き換える代わりにnullptrp結局、その時点ではnullであることがわかっています。nullであることがわかっている場合でも、(raw)ポインターからconstantへのスマートポインターを構築することはできません。

モード1:スマートポインターを値で渡す

引数としてスマートポインター値を受け取る関数は、ポイントされたオブジェクトをすぐに取得します。呼び出し側が(名前付き変数または匿名一時変数のどちらで)保持していたスマートポインターは、関数の入り口と呼び出し元の引数の値にコピーされます。ポインタがnullになりました(一時的な場合はコピーが省略された可能性がありますが、いずれの場合も、呼び出し元はポイントされたオブジェクトへのアクセスを失います)。私はこのモードコールを現金で呼び出したいのです。呼び出し側は呼び出されたサービスに対して前払いし、呼び出し後の所有権についての幻想はありません。これを明確にするために、言語規則では、呼び出し元が引数をstd::moveスマートポインターが変数に保持されている場合(技術的には、引数が左辺値の場合); この場合(ただし、以下のモード3の場合を除く)、この関数はその名前が示すことを実行します。つまり、値を変数から一時変数に移動し、変数をnullのままにします。

呼び出された関数が指定されたオブジェクトの所有権を無条件に取得(盗み)する場合、このモードはstd::unique_ptrまたstd::auto_ptrはと共に使用され、所有権と一緒にポインターを渡すのに適した方法であり、メモリーリークのリスクを回避します。それにもかかわらず、以下のモード3がモード1よりも(少しでも)優先されない状況はほとんどないと思います。このため、このモードの使用例は提供しません。(ただし、reversed以下のモード3 の例を参照してください。ここでは、モード1が少なくとも同様に機能することが説明されています。)関数がこのポインターだけよりも多くの引数を取る場合、さらにモードを回避する技術的な理由がある可能性があります。 1std::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)、その後、pnullにリセットされています。引数の受け渡しでモード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::movereturn声明)。おそらくnixされたばかりのポインタへの参照を取得したくはありません。


1
モード0の+1-unique_ptrの代わりに基になるポインターを渡します。(unique_ptrを渡すことに関する質問であるため)トピックからわずかに外れていますが、それは単純で問題を回避します。
Machta 14

ここではモード1はメモリリークを引き起こしません」-つまり、モード3はメモリリークを引き起こしますが、これは正しくありません。unique_ptr移動元かどうかに関係なく、破棄または再利用されるたびに値を保持している場合は、値が削除されます。
rustyx 2016

@RustyX:あなたがその含意をどのように解釈しているかはわかりませんし、私があなたが含意していると思うことを言うつもりもありません。他の場所と同じようにを使用unique_ptrするとメモリリークが防止されるため(つまり、ある意味でその契約が満たされます)、ここでは(つまり、モード1を使用して)、特定の状況下ではさらに有害と見なされる可能性がある、つまり、モード3を使用して回避できた可能性のあるデータの損失(指定された値の破壊)
Marc van Leeuwen

4

はいunique_ptr、コンストラクターでby値を取得する場合に必要です。Explicityは素晴らしいことです。以来unique_ptr(プライベートコピーctorの)コピー不可である、あなたが書いたものをあなたにコンパイルエラーを与える必要があります。


3

編集:厳密に言えば、コードは機能しますが、この答えは間違っています。その下での議論はあまりにも有用なので、私はここに残します。この他の答えは、私がこれを最後に編集したときに与えられた最良の答えですどのようにして、unique_ptr引数をコンストラクターまたは関数に渡しますか?

の基本的な考え方::std::moveは、あなたを渡す人はunique_ptrそれを使って、unique_ptr彼らが渡すものは所有権を失うことを知っているという知識を表現するべきだということです。

つまりunique_ptr、メソッドでは、unique_ptrそれ自体ではなく、メソッドへの右辺値参照を使用する必要があります。とにかくこれは機能しません。プレーンなoldを渡すとunique_ptrコピーを作成する必要があり、これはのインターフェイスで明示的に禁止されているためunique_ptrです。興味深いことに、名前付き右辺値参照を使用すると、再び右辺値に戻るため、メソッド::std::move 内でも使用する必要があります。

つまり、2つのメソッドは次のようになります。

Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability

void setNext(Base::UPtr &&n) { next = ::std::move(n); }

次に、メソッドを使用する人々はこれを行います:

Base::UPtr objptr{ new Base; }
Base::UPtr objptr2{ new Base; }
Base fred(::std::move(objptr)); // objptr now loses ownership
fred.setNext(::std::move(objptr2)); // objptr2 now loses ownership

ご覧のように、::std::moveは、ポインタが最も関連性があり、知っておくと便利な時点でポインタが所有権を失うことを示しています。これが目に見えない形で起こった場合、クラスを使用している人々objptrが、すぐには明らかな理由もなく突然所有権を失うのは非常に混乱します。


2
名前付き右辺値参照は左辺値です。
R.マルティーニョフェルナンデス

あなたはそれがそうだと確信してBase fred(::std::move(objptr));Base::UPtr fred(::std::move(objptr));ますか?
codablank1 2011年

1
私の以前のコメントに追加するには:このコードはコンパイルされません。std::moveコンストラクタとメソッドの両方の実装で使用する必要があります。また、値で渡す場合でも、呼び出し元はstd::move左辺値を渡すために使用する必要があります。主な違いは、値渡しのインターフェースにより、所有権が失われることを明確にすることです。別の回答については、Nicol Bolasのコメントを参照してください。
R.マルティーニョフェルナンデス

@ codablank1:はい。右辺値参照を取るbaseのコンストラクタとメソッドの使用方法を示しています。
全知

@ R.MartinhoFernandes:ああ、面白い。それは理にかなっていると思います。私はあなたが間違っていることを期待していましたが、実際のテストはあなたが正しいことを証明しました。今修正されました。
52:52に全知

0
Base(Base::UPtr n):next(std::move(n)) {}

はるかに良いはずです

Base(Base::UPtr&& n):next(std::forward<Base::UPtr>(n)) {}

そして

void setNext(Base::UPtr n)

する必要があります

void setNext(Base::UPtr&& n)

同じ体で。

そして...何がevt入っているのhandle()??


3
std::forwardここで使用してもメリットBase::UPtr&&はありません。は常に右辺値参照型でありstd::move、右辺値として渡します。すでに正しく転送されています。
R.マルティーニョフェルナンデス

7
私は強く反対します。関数がunique_ptrby値を取る場合、移動コンストラクターが新しい値で呼び出されたことが保証されます(または単に一時変数が与えられたことが保証されます)。これによりunique_ptr、ユーザーが持っている変数が確実に空になります。あなたがでそれを取る場合は&&代わりにあなたのコードは、移動操作を呼び出した場合、それだけで空になります。あなたのやり方では、ユーザーが移動されていない変数である可能性があります。これにより、ユーザーはstd::move疑わしくて混乱しやすくなります。を使用std::moveすると、常に何かが移動したことが保証されます。
Nicol

@NicolBolas:その通りです。答えは機能しますが、あなたの観察は完全に正しいので、私は私の答えを削除します。
全面的11/11/13

0

トップ投票の答えに。私は右辺値参照で渡すことを好みます。

右辺値参照による受け渡しの問題の原因を理解しています。しかし、この問題を2つの側面に分けましょう。

  • 発信者向け:

私は、コードを記述する必要がありますBase newBase(std::move(<lvalue>))Base newBase(<rvalue>)

  • 呼び出し先:

ライブラリの作成者は、所有権を所有する場合、unique_ptrを初期化メンバーに実際に移動することを保証する必要があります。

それで全部です。

右辺値参照で渡すと、1つの「移動」命令しか呼び出されませんが、値で渡すと、2つになります。

そうです、ライブラリの作者がこれについて専門家でない場合、彼はunique_ptrを初期化メンバーに移動しないかもしれませんが、それはあなたではなく作者の問題です。値渡しまたは右辺値参照で渡されるものは何でも、コードは同じです!

ライブラリを作成している場合は、ライブラリを保証する必要があることがわかったので、それを実行してください。右辺値参照で渡すことは、値よりも優れた選択肢です。ライブラリを使用するクライアントは、同じコードを書くだけです。

さて、あなたの質問のために。unique_ptr引数をコンストラクタまたは関数に渡すにはどうすればよいですか?

あなたは何が最良の選択か知っています。

http://scottmeyers.blogspot.com/2014/07/should-move-only-types-ever-be-passed.html

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.