私は、C++ 11の新機能のいくつかを調べてきましたが、気付いたのは、T&& var
のように、変数を宣言する際のダブルアンパサンドです。
はじめに、この獣は何と呼ばれていますか?このような句読点をGoogleで検索できるようにしてください。
それは正確には何ですか意味ですか?
一見すると、それは二重参照であるように見えます(Cスタイルの二重ポインタT** var
のように)、しかし私はそのためのユースケースを考えるのに苦労しています。
これは 右辺値の参照 (標準化提案書)を宣言しています。
これがrvalue references の紹介です。
これは、Microsoftの標準ライブラリ 開発者 による、右辺値参照の素晴らしい詳細な考察です。
注意: MSDNのリンク記事( "Rvalue References:VC10のC++ 0xの機能、第2部")はRvalue参照の非常に明確な紹介ですが、Rvalue参照についての記述は具体的には、右辺値参照は左辺値にバインドできることをさまざまな点で述べていますが、これは以前は真実でしたが変更されました(例:int x; int)。 && rrx = x; GCCではコンパイルできなくなりました) - drewbarbs '14年7月13日16時12分
C++ 03参照(現在はC++ 11では左辺値参照と呼ばれています)との最大の違いは、constでなくてもテンポラリのように右辺値にバインドできることです。したがって、この構文は現在有効です。
T&& r = T();
右辺値参照は主に次のものを提供します。
セマンティクスを移動する 。移動コンストラクタと移動代入演算子を定義できるようになりました。これは通常のconst-lvalue参照の代わりにrvalue参照を取ります。移動はコピーのように機能しますが、ソースを変更しないことが義務付けられていません。実際には、通常、移動されたリソースを所有しなくなるようにソースを変更します。これは、特に標準ライブラリの実装において、余分なコピーを排除するのに最適です。
たとえば、コピーコンストラクタは次のようになります。
foo(foo const& other)
{
this->length = other.length;
this->ptr = new int[other.length];
copy(other.ptr, other.ptr + other.length, this->ptr);
}
このコンストラクタが一時的なものを渡された場合、一時的なものが破壊されるだけであることがわかっているので、コピーは不要です。一時的にすでに割り当てられているリソースを利用しないのはなぜですか。 C++ 03では、一時的に渡されたと判断できないため、コピーを防止する方法はありません。 C++ 11では、moveコンストラクタをオーバーロードできます。
foo(foo&& other)
{
this->length = other.length;
this->ptr = other.ptr;
other.length = 0;
other.ptr = nullptr;
}
大きな違いに注意してください。moveコンストラクタは実際にその引数を変更します。これは一時的なものを構築中のオブジェクトに効果的に「移動」させ、それによって不要なコピーを排除します。
Moveコンストラクタは、一時的な関数、およびstd::move
関数を使用して明示的に右辺値参照に変換される非定数左辺値参照に使用されます(変換のみを実行します)。次のコードはどちらもf1
とf2
のためにmoveコンストラクタを呼び出します。
foo f1((foo())); // Move a temporary into f1; temporary becomes "empty"
foo f2 = std::move(f1); // Move f1 into f2; f1 is now "empty"
完全転送 。右辺値参照により、テンプレート関数の引数を正しく転送することができます。たとえば、このファクトリ関数を取ります。
template <typename T, typename A1>
std::unique_ptr<T> factory(A1& a1)
{
return std::unique_ptr<T>(new T(a1));
}
factory<foo>(5)
を呼び出した場合、引数はint&
であると推定されます。これは、foo
のコンストラクタがint
を取る場合でも、リテラル5にバインドされません。そうですね、代わりにA1 const&
を使用することもできますが、foo
が非const参照によってコンストラクター引数を取る場合はどうなりますか?本当に一般的なファクトリ関数を作るためには、A1&
とA1 const&
でfactoryをオーバーロードしなければなりません。 factoryが1つのパラメータタイプを取る場合はそれでもいいかもしれませんが、追加の各パラメータタイプは必要なオーバーロードセットを2倍にするでしょう。それは非常に早くメンテナンス不可能です。
右辺値参照は、標準ライブラリが左辺値/右辺値参照を正しく転送できるstd::forward
関数を定義できるようにすることで、この問題を解決します。 std::forward
の仕組みの詳細については、 この優れた答え を参照してください。
これにより、ファクトリ関数を次のように定義できます。
template <typename T, typename A1>
std::unique_ptr<T> factory(A1&& a1)
{
return std::unique_ptr<T>(new T(std::forward<A1>(a1)));
}
これで、引数の右辺値/左辺値は、T
のコンストラクタに渡されたときに保持されるようになりました。つまり、factoryが右辺値で呼び出されると、T
のコンストラクタは右辺値で呼び出されます。 factoryが左辺値で呼び出されると、T
のコンストラクタは左辺値で呼び出されます。改善されたファクトリー機能は、1つの特別な規則のために機能します。
関数パラメーター・タイプが
T&&
の形式で、T
がテンプレート・パラメーターで、関数の引数がタイプA
の左辺値の場合、テンプレート引数の推論にはタイプA&
が使用されます。
したがって、我々はそのようにファクトリを使うことができます:
auto p1 = factory<foo>(foo()); // calls foo(foo&&)
auto p2 = factory<foo>(*p1); // calls foo(foo const&)
重要な右辺値参照プロパティ :
float f = 0f; int&& i = f;
は、floatが暗黙的にintに変換可能であるため整形式です。参照は、変換の結果である一時的なものになります。std::move
呼び出しが必要なのかを理解するのに重要です。foo&& r = foo(); foo f = std::move(r);
これは右辺値参照を表します。明示的に生成されない限り、右辺値参照は一時オブジェクトにのみバインドされます。これらは、特定の状況下でオブジェクトをはるかに効率的にするため、および完全転送として知られる機能を提供するために使用されます。これはテンプレートコードを非常に単純化します。
C++ 03では、変更不可能な左辺値と右辺値のコピーを区別することはできません。
std::string s;
std::string another(s); // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(const std::string&);
C++ 0xでは、これは当てはまりません。
std::string s;
std::string another(s); // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(std::string&&);
これらのコンストラクタの背後にある実装を検討してください。前者の場合、文字列は値のセマンティクスを保持するためにコピーを実行する必要があり、これには新しいヒープ割り当てが含まれます。しかし、2番目のケースでは、コンストラクターに渡されたオブジェクトがすぐに破棄の対象になることを事前に知っているので、それをそのまま残しておく必要はありません。このシナリオでは、内部ポインタを効果的に交換するだけで、まったくコピーを実行できないため、実質的に効率的です。移動セマンティクスは、内部的に参照されているリソースのコピーが高価または禁止されているすべてのクラスに役立ちます。 std::unique_ptr
-のケースを考えてみましょう。unique_ptr
は一時的なものと一時的でないものを区別できるので、std::unique_ptr
をコピーすることはできずに移動させることができます。 C++ 03のstd::auto_ptr
はできません。
今度は、右辺値参照の他の用途 - 完全転送を検討します。参照を参照に結び付けるという問題について考えてみましょう。
std::string s;
std::string& ref = s;
(std::string&)& anotherref = ref; // usually expressed via template
C++ 03がこれについて言っていることを思い出すことはできませんが、C++ 0xでは、右辺値参照を扱うときに結果として得られる型は重要です。 Tが参照型であるT型への右辺値参照は、T型の参照になります。
(std::string&)&& ref // ref is std::string&
(const std::string&)&& ref // ref is const std::string&
(std::string&&)&& ref // ref is std::string&&
(const std::string&&)&& ref // ref is const std::string&&
最も単純なテンプレート関数minとmaxを考えてみましょう。 C++ 03では、constとnon-constの4つの組み合わせすべてを手動でオーバーロードする必要があります。 C++ 0xでは、それはただ1つのオーバーロードです。可変テンプレートと組み合わせることで、これは完璧な転送を可能にします。
template<typename A, typename B> auto min(A&& aref, B&& bref) {
// for example, if you pass a const std::string& as first argument,
// then A becomes const std::string& and by extension, aref becomes
// const std::string&, completely maintaining it's type information.
if (std::forward<A>(aref) < std::forward<B>(bref))
return std::forward<A>(aref);
else
return std::forward<B>(bref);
}
返品タイプの控除は、手放しで行ったことを思い出せないため、やめましたが、その最小値は、左辺値、右辺値、定数左辺値の任意の組み合わせを受け入れることができます。
T&&
の用語は、deduction型(完全な転送など)で使用される場合、転送参照。 「ユニバーサルリファレンス」という用語は、Scott Meyers この記事 によって造られましたが、後に変更されました。
これは、r値またはl値のいずれかである可能性があるためです。
例は次のとおりです。
// template
template<class T> foo(T&& t) { ... }
// auto
auto&& t = ...;
// typedef
typedef ... T;
T&& t = ...;
// decltype
decltype(...)&& t = ...;
詳細については、次の回答を参照してください。 ユニバーサルリファレンスの構文
右辺値参照は、いくつかの例外を除いて、通常の参照X&とよく似た動作をする型です。最も重要なのは、関数のオーバーロード解決に関しては、左辺値は古いスタイルの左辺値参照を優先するのに対し、右辺値は新しい右辺値参照を優先することです。
void foo(X& x); // lvalue reference overload
void foo(X&& x); // rvalue reference overload
X x;
X foobar();
foo(x); // argument is lvalue: calls foo(X&)
foo(foobar()); // argument is rvalue: calls foo(X&&)
それで右辺値は何ですか?左辺値ではない何でも。左辺値は、記憶場所を参照し、&演算子を介してその記憶場所のアドレスを取得できるようにする式です。
例を使って右辺値が達成することを最初に理解するのは、ほとんど簡単です。
class Sample {
int *ptr; // large block of memory
int size;
public:
Sample(int sz=0) : ptr{sz != 0 ? new int[sz] : nullptr}, size{sz}
{}
// copy constructor that takes lvalue
Sample(const Sample& s) : ptr{s.size != 0 ? new int[s.size] :\
nullptr}, size{s.size}
{
std::cout << "copy constructor called on lvalue\n";
}
// move constructor that take rvalue
Sample(Sample&& s)
{ // steal s's resources
ptr = s.ptr;
size = s.size;
s.ptr = nullptr; // destructive write
s.size = 0;
cout << "Move constructor called on rvalue." << std::endl;
}
// normal copy assignment operator taking lvalue
Sample& operator=(const Sample& s)
{
if(this != &s) {
delete [] ptr; // free current pointer
ptr = new int[s.size];
size = s.size;
}
cout << "Copy Assignment called on lvalue." << std::endl;
return *this;
}
// overloaded move assignment operator taking rvalue
Sample& operator=(Sample&& lhs)
{
if(this != &s) {
delete [] ptr; //don't let ptr be orphaned
ptr = lhs.ptr; //but now "steal" lhs, don't clone it.
size = lhs.size;
lhs.ptr = nullptr; // lhs's new "stolen" state
lhs.size = 0;
}
cout << "Move Assignment called on rvalue" << std::endl;
return *this;
}
//...snip
};
コンストラクタと代入演算子は、右辺値参照を取るバージョンでオーバーロードされています。右辺値参照を使用すると、関数がコンパイル時に(オーバーロードの解決を介して)条件「条件付きで左辺値または右辺値で呼び出されていますか?」で分岐できます。 これにより、リソースをコピーするのではなく、リソースを移動する上記のより効率的なコンストラクタと代入演算子を作成できました。
コンパイラーはコンパイル時に自動的に分岐し(左辺値と右辺値のどちらで呼び出されるかに応じて)、移動コンストラクターと移動代入演算子のどちらを呼び出すかを選択します。
要約すると:右辺値参照は移動意味論(および下の記事リンクで議論される完璧な転送)を可能にします。
実用的で理解しやすい1つの例は、クラステンプレート std :: unique_ptr です。 unique_ptrはその基礎となる生のポインタの排他的な所有権を維持しているので、unique_ptrをコピーすることはできません。それは彼らの独占的所有権の不変性に違反するでしょう。そのため、コピーコンストラクタはありません。しかし、彼らは移動コンストラクタを持っています:
template<class T> class unique_ptr {
//...snip
unique_ptr(unique_ptr&& __u) noexcept; // move constructor
};
std::unique_ptr<int[] pt1{new int[10]};
std::unique_ptr<int[]> ptr2{ptr1};// compile error: no copy ctor.
// So we must first cast ptr1 to an rvalue
std::unique_ptr<int[]> ptr2{std::move(ptr1)};
std::unique_ptr<int[]> TakeOwnershipAndAlter(std::unique_ptr<int[]> param,\
int size)
{
for (auto i = 0; i < size; ++i) {
param[i] += 10;
}
return param; // implicitly calls unique_ptr(unique_ptr&&)
}
// Now use function
unique_ptr<int[]> ptr{new int[10]};
// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(\
static_cast<unique_ptr<int[]>&&>(ptr), 10);
cout << "output:\n";
for(auto i = 0; i< 10; ++i) {
cout << new_owner[i] << ", ";
}
output:
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
static_cast<unique_ptr<int[]>&&>(ptr)
は通常 std :: move を使って行われます。
// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(std::move(ptr),0);
(右辺値がどのように完璧な転送を可能にするか、そしてそれが何を意味するのかなどの)すべてを説明する優れた記事はThomas Beckerの C++ Rvalue References Explained です。この記事は彼の記事に大きく依存していました。
より短い紹介は Rvalue参照への簡単な紹介 Stroutrup、et alによる。アル