web-dev-qa-db-ja.com

関数からunique_ptrを返す

unique_ptr<T>はコピー構築を許可しません、代わりにそれは移動意味論をサポートします。それでも、関数からunique_ptr<T>を返し、その戻り値を変数に割り当てることができます。

#include <iostream>
#include <memory>

using namespace std;

unique_ptr<int> foo()
{
  unique_ptr<int> p( new int(10) );

  return p;                   // 1
  //return move( p );         // 2
}

int main()
{
  unique_ptr<int> p = foo();

  cout << *p << endl;
  return 0;
}

上記のコードは、意図したとおりにコンパイルおよび動作します。では、1行がコピーコンストラクタを呼び出さず、コンパイラエラーになるのはどうしてですか。代わりに2行を使用しなければならない場合は意味があります(2行を使用しても同様に機能しますが、必須ではありません)。

戻り値は関数が終了するとすぐに破棄される一時オブジェクトであるため、C++ 0xではunique_ptrに対するこの例外が許可されています。これにより、返されるポインタの一意性が保証されます。私はこれがどのように実装されているかについて興味がありますか、それはコンパイラの中で特別に扱われていますか、あるいは言語仕様の中でこれが利用する他の節がありますか?

306
Praetorian

これが利用する言語仕様の中に他の節がありますか?

はい、12.8§34と§35を参照してください。

特定の基準が満たされると、実装はクラスオブジェクトのコピー/移動の構築を省略することができます[...]コピー/移動操作のこの選択は、コピー選択と呼ばれます。クラス戻り型の関数内のreturn文に許可される[...]、式が不揮発性自動オブジェクトの名前の場合関数と同じcv非修飾型の場合戻り型[...]

コピー操作の省略の基準が満たされ、コピー対象のオブジェクトが左辺値で指定されている場合、コピーのコンストラクターを選択するためのオーバーロード解決が最初に実行されますオブジェクトが右辺値で指定された場合と同様


最悪の場合、つまりC++ 11、C++ 14、およびC++ 17での選択がない場合、returnステートメント内の名前付きの値が扱われるため、ここでは値による戻りがデフォルトの選択であるという点をもう1つ追加します。右辺値として。たとえば、次の関数は-fno-elide-constructorsフラグを付けてコンパイルします。

std::unique_ptr<int> get_unique() {
  auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1
  return ptr; // <- 2, moved into the to be returned unique_ptr
}

...

auto int_uptr = get_unique(); // <- 3

フラグがコンパイル時に設定されていると、この関数内で2つの動作(1と2)が発生し、その後1つの動作(3)が発生します。

195
fredoverflow

これはstd::unique_ptrに固有のものではありませんが、移動可能なすべてのクラスに適用されます。あなたは価値で戻ってきているので、それは言語の規則によって保証されています。コンパイラはコピーの削除を試み、コピーを削除できない場合はmoveコンストラクタを呼び出し、移動できない場合はcopyコンストラクタを呼び出し、コピーできない場合はコンパイルに失敗します。

引数としてstd::unique_ptrを受け入れる関数があるとしたら、pを渡すことはできません。 moveコンストラクタを明示的に呼び出す必要がありますが、この場合はbar()の呼び出し後に変数pを使用しないでください。

void bar(std::unique_ptr<int> p)
{
    // ...
}

int main()
{
    unique_ptr<int> p = foo();
    bar(p); // error, can't implicitly invoke move constructor on lvalue
    bar(std::move(p)); // OK but don't use p afterwards
    return 0;
}
91

unique_ptrは伝統的なコピーコンストラクタを持っていません。代わりに、右辺値参照を使用する「移動コンストラクタ」があります。

unique_ptr::unique_ptr(unique_ptr && src);

右辺値参照(二重アンパサンド)は右辺値にのみバインドされます。そのため、左辺値unique_ptrを関数に渡そうとするとエラーが発生します。一方、関数から返される値は右辺値として扱われるため、moveコンストラクタが自動的に呼び出されます。

ところで、これは正しく動作します:

bar(unique_ptr<int>(new int(44));

ここでの一時的なunique_ptrは右辺値です。

35

私はそれが完全にスコットマイヤーズの項目25で説明されていると思います Effective Modern C++ 。これは抜粋です。

標準のRVOの祝福の一部は、RVOの条件は満たされているが、コンパイラーがコピー排除を実行しないことを選択した場合、返されるオブジェクトは右辺値として扱われる必要があると言います。事実上、この規格では、RVOが許可されている場合、コピーの選択が行われるか、または返されるローカルオブジェクトにstd::moveが暗黙的に適用されることを要求しています。

ここで、RVO戻り値の最適化を表し、RVOの条件が次の場合はを表します。 metは、RVOを実行すると予想される関数内で宣言されているローカルオブジェクトを返すことを意味します。標準を参照する(ここでローカルオブジェクトには、return文によって作成された一時オブジェクトが含まれます)。抜粋からの最大の離脱はコピーの削除が行われるかstd::moveが暗黙的に返されるローカルオブジェクトに適用されることです。 Scottは、項目25で、std::moveは、コンパイラがコピーを回避しないことを選択した場合に暗黙的に適用され、プログラマが明示的にそうしない場合には暗黙的に適用されると述べています。

あなたの場合、コードはローカルオブジェクトpを返し、pの型は戻り型と同じであるため、コードは明らかにRVOの候補です。脱出。また、コンパイラがコピーを回避しないことを選択した場合は、何らかの理由でstd::move1行に追加されていました。

9
David Lee

私が他の答えで見なかったことの一つは、 明確にするために 別の答え 関数内で作成されたstd :: unique_ptrを返すことと、その関数に与えられたものとの間に違いがあることを==。

例は次のようになります。

class Test
{int i;};
std::unique_ptr<Test> foo1()
{
    std::unique_ptr<Test> res(new Test);
    return res;
}
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t)
{
    // return t;  // this will produce an error!
    return std::move(t);
}

//...
auto test1=foo1();
auto test2=foo2(std::unique_ptr<Test>(new Test));
2
v010dya