C ++ 0xに関するScott Meyersとのソフトウェアエンジニアリングのラジオポッドキャストインタビューを聞いたところです。新機能のほとんどは私にとって理にかなっており、1つを除いて、実際にC ++ 0xに興奮しています。私はまだ移動セマンティクスを取得しません...それは正確には何ですか?
C ++ 0xに関するScott Meyersとのソフトウェアエンジニアリングのラジオポッドキャストインタビューを聞いたところです。新機能のほとんどは私にとって理にかなっており、1つを除いて、実際にC ++ 0xに興奮しています。私はまだ移動セマンティクスを取得しません...それは正確には何ですか?
回答:
例のコードで移動のセマンティクスを理解するのが最も簡単です。ヒープに割り当てられたメモリブロックへのポインタのみを保持する非常に単純な文字列クラスから始めましょう。
#include <cstring>
#include <algorithm>
class string
{
char* data;
public:
string(const char* p)
{
size_t size = std::strlen(p) + 1;
data = new char[size];
std::memcpy(data, p, size);
}
私たちは自分でメモリを管理することを選択したので、3つのルールに従う必要があります。ここでは、代入演算子の記述を延期し、デストラクタとコピーコンストラクタのみを実装します。
~string()
{
delete[] data;
}
string(const string& that)
{
size_t size = std::strlen(that.data) + 1;
data = new char[size];
std::memcpy(data, that.data, size);
}
コピーコンストラクタは、文字列オブジェクトをコピーすることの意味を定義します。パラメータconst string& that
は文字列型のすべての式にバインドされます。これにより、次の例でコピーを作成できます。
string a(x); // Line 1
string b(x + y); // Line 2
string c(some_function_returning_a_string()); // Line 3
ここで、移動のセマンティクスに関する重要な洞察が得られます。x
このディープコピーが本当に必要なのは、コピーする最初の行だけであることに注意してください。x
後で検査する必要があり、x
何らかの変更があった場合は非常に驚かれるからです。私がx
3回(この文章を含めると4回)言ったことがあり、毎回まったく同じオブジェクトを意味していることに気づきましたか?x
「左辺値」などの式を呼び出します。
行2と3の引数は左辺値ではなく右辺値です。これは、基礎となる文字列オブジェクトに名前がないため、クライアントが後でそれらを再度検査する方法がないためです。右辺値は、次のセミコロン(より正確には、右辺値を字句的に含む完全式の終わり)で破棄される一時オブジェクトを示します。b
and の初期化中に、c
ソース文字列を使用して必要なことをすべて実行でき、クライアントが違いを通知できなかったため、これは重要です。
C ++ 0xには、「右辺値参照」と呼ばれる新しいメカニズムが導入されています。これにより、関数のオーバーロードを介して右辺値引数を検出できます。必要なのは、右辺値参照パラメーターを使用してコンストラクターを作成することだけです。そのコンストラクタ内では行うことができます私たちが望むものを限り、私たちがそれを残すように、ソースといくつかの有効な状態:
string(string&& that) // string&& is an rvalue reference to a string
{
data = that.data;
that.data = nullptr;
}
ここで何をしましたか?ヒープデータを深くコピーする代わりに、ポインタをコピーしてから元のポインタをnullに設定しました(ソースオブジェクトのデストラクタからの「delete []」が「盗んだばかりのデータ」を解放しないようにするため)。実際には、元々ソース文字列に属していたデータが「盗まれ」ました。繰り返しになりますが、重要な洞察は、どのような状況でも、ソースが変更されたことをクライアントが検出できないことです。ここでは実際にはコピーを行わないため、このコンストラクターを「移動コンストラクター」と呼びます。その仕事は、リソースをコピーするのではなく、あるオブジェクトから別のオブジェクトに移動することです。
おめでとうございます。これで移動セマンティクスの基本が理解できました。代入演算子を実装して続けましょう。コピーとスワップのイディオムに慣れていない場合は、例外の安全性に関連する素晴らしいC ++イディオムであるため、学んで戻ってきてください。
string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}
};
ええ、それだけですか?「右辺値参照はどこにありますか?」あなたが尋ねるかもしれません。「ここでは必要ありません!」私の答えです:)
パラメータthat
を値で渡すためthat
、他の文字列オブジェクトと同じように初期化する必要があることに注意してください。正確にどのthat
ように初期化されますか?C ++ 98の昔、答えは「コピーコンストラクター」でした。C ++ 0xでは、コンパイラーは、代入演算子への引数が左辺値または右辺値のどちらであるかに基づいて、コピーコンストラクターと移動コンストラクターのどちらかを選択します。
したがって、と言ったa = b
場合、コピーコンストラクターが初期化されthat
(式b
が左辺値であるため)、代入演算子はコンテンツを新しく作成されたディープコピーと入れ替えます。これが、コピーとスワップのイディオムのまさにその定義です。コピーを作成し、内容をコピーと交換し、スコープを離れることによってコピーを削除します。ここで新しいことはありません。
しかし、と言ったa = x + y
場合、(式が右辺値であるため)移動コンストラクターが初期化されるthat
ため、x + y
深いコピーは含まれず、効率的な移動のみが行われます。
that
はまだ引数から独立したオブジェクトですが、ヒープデータをコピーする必要がなく、移動しただけなので、その構築は簡単でした。x + y
は右辺値であるため、コピーする必要はありませんでした。また、右辺値で示された文字列オブジェクトから移動してもかまいません。
要約すると、コピーコンストラクターはディープコピーを作成します。これは、ソースを変更しないでおく必要があるためです。一方、移動コンストラクタは、ポインタをコピーして、ソースのポインタをnullに設定するだけです。クライアントがオブジェクトを再度検査する方法がないため、この方法でソースオブジェクトを「無効化」しても問題ありません。
この例が重要なポイントになったと思います。参照を評価し、セマンティクスを移動する方法は他にもたくさんあります。詳細については、私の補足回答をご覧ください。
that.data = 0
と、キャラクターが非常に早く(一時的に死ぬとき)、また2回破壊されるためです。あなたはそれを共有するのではなく、データを盗もうとしています!
delete[]
nullptr上の@ Virus721 は、C ++標準で何もしないと定義されています。
私の最初の答えは、セマンティクスを移動するための非常に単純化された紹介でした。そして、それを単純に保つために、多くの詳細は意図的に省略されました。ただし、セマンティクスを移動する方法は他にもたくさんあり、ギャップを埋めるには2番目の答えが必要だと思いました。最初の答えはすでにかなり古く、完全に異なるテキストに単純に置き換えるのは適切ではないと感じました。それはまだ最初の紹介としてうまく機能すると思います。しかし、さらに深く掘り下げたい場合は、以下をお読みください。
Stephan T. Lavavejは貴重なフィードバックを提供するために時間をかけました。ステファン、ありがとうございました!
移動セマンティクスにより、オブジェクトは、特定の条件下で、他のオブジェクトの外部リソースの所有権を取得できます。これは2つの点で重要です。
高価なコピーを安価な動きに変える。例については、最初の回答を参照してください。オブジェクトが少なくとも1つの外部リソースを管理しない場合(直接またはそのメンバーオブジェクトを介して間接的に)、移動セマンティクスはコピーセマンティクスよりも優れていることに注意してください。その場合、オブジェクトのコピーと移動はまったく同じことを意味します。
class cannot_benefit_from_move_semantics
{
int a; // moving an int means copying an int
float b; // moving a float means copying a float
double c; // moving a double means copying a double
char d[64]; // moving a char array means copying a char array
// ...
};
安全な「移動専用」タイプの実装。つまり、コピーには意味がなく、移動には意味があるタイプです。例としては、ロック、ファイルハンドル、一意の所有権セマンティクスを持つスマートポインターなどがあります。注:この回答ではstd::auto_ptr
、std::unique_ptr
C ++ 11で置き換えられた非推奨のC ++ 98標準ライブラリテンプレートについて説明します。中級のC ++プログラマーは、おそらくにある程度精通しておりstd::auto_ptr
、表示される「移動のセマンティクス」のため、C ++ 11での移動のセマンティクスについて説明するための良い出発点のようです。YMMV。
C ++ 98標準ライブラリは、という独自の所有権セマンティクスを持つスマートポインタを提供しますstd::auto_ptr<T>
。に慣れていない場合auto_ptr
、その目的は、例外が発生した場合でも、動的に割り当てられたオブジェクトが常に解放されることを保証することです。
{
std::auto_ptr<Shape> a(new Triangle);
// ...
// arbitrary code, could throw exceptions
// ...
} // <--- when a goes out of scope, the triangle is deleted automatically
異常なのauto_ptr
は、その「コピー」動作です。
auto_ptr<Shape> a(new Triangle);
+---------------+
| triangle data |
+---------------+
^
|
|
|
+-----|---+
| +-|-+ |
a | p | | | |
| +---+ |
+---------+
auto_ptr<Shape> b(a);
+---------------+
| triangle data |
+---------------+
^
|
+----------------------+
|
+---------+ +-----|---+
| +---+ | | +-|-+ |
a | p | | | b | p | | | |
| +---+ | | +---+ |
+---------+ +---------+
注どのようにの初期化b
ではa
ないではない三角形をコピーし、代わりに三角形の所有権を転送a
しますb
。また、「a
に移動 b
」または「三角形がからに移動」と言います。三角形自体は常にメモリ内の同じ場所にあるため、これは混乱を招くように聞こえるかもしれません。a
b
オブジェクトを移動するとは、オブジェクトが管理しているリソースの所有権を別のオブジェクトに移すことを意味します。
のコピーコンストラクタはauto_ptr
おそらく次のようになります(やや簡略化されています)。
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
危険なことauto_ptr
は、構文的にコピーのように見えるものが実際には移動であるということです。移動元でメンバー関数を呼び出そうとすると、auto_ptr
未定義の動作が呼び出されるため、移動元のを使用しないように注意する必要がありますauto_ptr
。
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
しかしauto_ptr
、常に危険なわけではありません。ファクトリ関数は、次の場合の完全に適切な使用例ですauto_ptr
。
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
どちらの例も同じ構文パターンに従っていることに注意してください。
auto_ptr<Shape> variable(expression);
double area = expression->area();
それでも、1つは未定義の動作を呼び出しますが、もう1つは呼び出しません。では、式a
との違いは何make_triangle()
ですか?どちらも同じタイプではないですか?確かにそうですが、価値観は異なります。
もちろん、表現の間にいくつかの深遠な違いが存在しなければならないa
意味auto_ptr
変数を、そして表現make_triangle()
返す関数の呼び出し表すauto_ptr
ので、新鮮な一時的な作成、値によってauto_ptr
オブジェクトが呼び出されるたびに。a
一例である左辺値に対して、make_triangle()
一例である右辺値。
などの左辺値からの移動a
は危険です。後でa
定義されていない動作を呼び出してを介してメンバー関数を呼び出そうとする可能性があるためです。一方、などの右辺値からの移動make_triangle()
は完全に安全です。これは、コピーコンストラクターの処理が完了すると、一時変数を再び使用することができないためです。この一時的なものを表す表現はありません。単純にmake_triangle()
もう一度書くと、別の一時ファイルが作成されます。実際、moved-from一時ファイルはすでに次の行にあります。
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
文字l
とr
には、割り当ての左側と右側に歴史的な起源があることに注意してください。これは、C ++ではもはや当てはまりません。これは、代入の左側に表示できない左辺値(配列または代入演算子のないユーザー定義型など)があり、右辺値(クラス型のすべての右辺値)があるためです。代入演算子付き)。
クラスタイプの右辺値は、評価によって一時オブジェクトが作成される式です。通常の状況では、同じスコープ内の他の式は同じ一時オブジェクトを示しません。
左辺値から移動することは潜在的に危険であるが、右辺値から移動することは無害であることを現在理解しています。C ++でlvalue引数とrvalue引数を区別する言語サポートがある場合、lvaluesからの移動を完全に禁止するか、少なくとも呼び出しサイトでlvaluesからの移動を明示的にすることで、誤って移動することがなくなります。
この問題に対するC ++ 11の答えは、右辺値参照です。右辺値参照は、右辺値にのみバインドされる新しい種類の参照であり、構文はX&&
です。古き良き参照X&
は現在、左辺値参照として知られています。(これは参照への参照でX&&
はないことに注意してください。C++にはそのようなものはありません。)
我々は投げる場合はconst
ミックスに、我々はすでに参照の4種類があります。X
彼らはどのような種類の表現にバインドできますか?
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& yes
const X& yes yes yes yes
X&& yes
const X&& yes yes
実際には、忘れることができますconst X&&
。右辺値からの読み取りに制限されていることはあまり役に立ちません。
右辺値参照
X&&
は、右辺値にのみバインドする新しい種類の参照です。
右辺値参照はいくつかのバージョンを通過しました。バージョン2.1以降、からへの暗黙的な変換がある場合、右辺値参照X&&
は異なるタイプのすべての値カテゴリにもバインドされます。その場合、タイプの一時変数が作成され、右辺値参照がその一時変数にバインドされます。Y
Y
X
X
void some_function(std::string&& r);
some_function("hello world");
上記の例で"hello world"
は、はタイプの左辺値ですconst char[12]
。const char[12]
through const char*
からへの暗黙的な変換があるためstd::string
、タイプの一時std::string
が作成され、r
その一時にバインドされます。これは、右辺値(式)と一時値(オブジェクト)の区別が少しぼやけている場合の1つです。
X&&
パラメータを持つ関数の便利な例は、移動コンストラクタ X::X(X&& source)
です。その目的は、管理対象リソースの所有権をソースから現在のオブジェクトに転送することです。
C ++ 11では、右辺値参照を利用するにstd::auto_ptr<T>
置き換えられましstd::unique_ptr<T>
た。の簡易バージョンを開発して説明しunique_ptr
ます。まず、生のポインタをカプセル化し、演算子->
and をオーバーロードする*
ので、クラスはポインタのように感じます。
template<typename T>
class unique_ptr
{
T* ptr;
public:
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
コンストラクタはオブジェクトの所有権を取得し、デストラクタはそれを削除します。
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
興味深い部分、移動コンストラクターが登場します。
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
この移動コンストラクターは、auto_ptr
コピーコンストラクターが行ったこととまったく同じですが、右辺値でのみ提供できます。
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
a
左辺値であるため、2行目はコンパイルに失敗しますが、パラメーターunique_ptr&& source
は右辺値にのみバインドできます。これがまさに私たちが欲しかったものです。危険な動きは決して暗示されるべきではありません。make_triangle()
右辺値であるため、3行目は問題なくコンパイルされます。moveコンストラクタは、所有権を一時からに転送しc
ます。繰り返しますが、これはまさに私たちが望んでいたことです。
移動コンストラクターは、管理対象リソースの所有権を現在のオブジェクトに転送します。
最後に欠けているのは、移動割り当て演算子です。その仕事は、古いリソースを解放し、その引数から新しいリソースを取得することです。
unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource
ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
}
};
この移動割り当て演算子の実装が、デストラクタと移動コンストラクタの両方のロジックを複製していることに注意してください。コピーアンドスワップのイディオムを知っていますか?移動と交換のイディオムとして、移動セマンティクスにも適用できます。
unique_ptr& operator=(unique_ptr source) // note the missing reference
{
std::swap(ptr, source.ptr);
return *this;
}
};
これがsource
の型の変数にunique_ptr
なり、moveコンストラクターによって初期化されます。つまり、引数はパラメーターに移動されます。moveコンストラクタ自体に右辺値参照パラメータがあるため、引数は右辺値である必要があります。制御フローはの閉じ括弧に到達するとoperator=
、source
自動的に古いリソースを解放し、スコープ外になります。
移動割り当て演算子は、管理対象リソースの所有権を現在のオブジェクトに転送し、古いリソースを解放します。移動と交換のイディオムは、実装を簡素化します。
場合によっては、左辺値から移動したいことがあります。つまり、場合によっては、コンパイラーが左辺値を右辺値であるかのように処理して、安全でない可能性がある場合でも移動コンストラクターを呼び出すことができるようにする必要があります。この目的のために、C ++ 11はstd::move
、ヘッダー内で呼び出される標準ライブラリ関数テンプレートを提供しています<utility>
。std::move
左辺値を右辺値にキャストするだけなので、この名前は少し残念です。それ自体は何も動かしません。それは単に移動を可能にします。多分それはstd::cast_to_rvalue
or std::enable_move
と名付けられるべきでしたが、今ではその名前で行き詰っています。
次に、左辺値から明示的に移動する方法を示します。
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
3行目以降はa
、三角形を所有していないことに注意してください。でだから、大丈夫です明示的に書いてstd::move(a)
、私たちは私たちの意図が明らかにした:「親愛なるコンストラクタを、あなたがやりたいa
初期化するためにc
、私は気にしないでくださいa
。もうであなたの方法を持ってお気軽にa
。」
std::move(some_lvalue)
左辺値を右辺値にキャストし、後続の移動を可能にします。
std::move(a)
は右辺値ですが、その評価では一時オブジェクトが作成されないことに注意してください。この難問により、委員会は第3の価値カテゴリーを導入せざるを得なくなりました。それは伝統的な意味では右辺値ではないにもかかわらず、右辺値参照にバインドすることができるものを、と呼ばれてはxValue(値を期限切れ)。従来の右辺値はprvalues(純粋な右辺値)に名前が変更されました。
prvaluesとxvaluesはどちらも右辺値です。X値とl値はどちらもglvalue(一般化されたl値)です。関係を図で理解することは簡単です:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
xvalueのみが本当に新しいことに注意してください。残りは、名前の変更とグループ化によるものです。
C ++ 98の右辺値は、C ++ 11ではprvaluesとして知られています。前の段落で出現したすべての「rvalue」を「prvalue」で精神的に置き換えます。
これまで、ローカル変数と関数パラメーターへの動きを見てきました。しかし、反対方向への移動も可能です。関数が値によって返される場合、呼び出しサイトの一部のオブジェクト(おそらくローカル変数または一時的ですが、任意の種類のオブジェクトである可能性があります)はreturn
、移動コンストラクターへの引数としてステートメントの後の式で初期化されます。
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| temporary is moved into c
|
v
unique_ptr<Shape> c(make_triangle());
おそらく驚くべきことに、自動オブジェクト(として宣言されていないローカル変数static
)を暗黙的に関数の外に移動することもできます。
unique_ptr<Shape> make_square()
{
unique_ptr<Shape> result(new Square);
return result; // note the missing std::move
}
moveコンストラクターが左辺値result
を引数として受け入れるのはなぜですか?のスコープはresult
間もなく終了し、スタックの巻き戻し中に破棄されます。result
どういうわけかそれが変わったと後で文句を言う人は誰もいないでしょう。制御フローが呼び出し元に戻ったとき、result
もう存在しません!そのため、C ++ 11には、を記述しなくても関数から自動オブジェクトを返すことができる特別なルールがありますstd::move
。実際には、「名前付き戻り値の最適化」(NRVO)を阻害するため、を使用して関数から自動オブジェクトを移動することはできませんstd::move
。
std::move
自動オブジェクトを関数から移動するために使用しないでください。
どちらのファクトリ関数でも、戻り値の型は値であり、右辺値参照ではないことに注意してください。右辺値参照は依然として参照であり、いつものように、自動オブジェクトへの参照を返すことはできません。次のようにコンパイラーにコードを受け入れさせると、呼び出し元はぶら下がり参照になります。
unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS!
{
unique_ptr<Shape> very_bad_idea(new Square);
return std::move(very_bad_idea); // WRONG!
}
右辺値参照によって自動オブジェクトを返さないでください。移動は
std::move
、によってではなく、右辺値を右辺値参照にバインドするだけではなく、移動コンストラクタによって排他的に実行されます。
遅かれ早かれ、次のようなコードを記述します。
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
基本的に、コンパイラーはそれparameter
が左辺値であると文句を言うでしょう。その型を見ると、右辺値参照が表示されますが、右辺値参照は、単に「右辺値にバインドされている参照」を意味します。参照自体が右辺値であるという意味ではありません!実際、parameter
は名前の付いた通常の変数です。parameter
コンストラクターの本体内で何度でも使用でき、常に同じオブジェクトを示します。暗黙的にそこから移動することは危険であり、したがって言語はそれを禁止します。
名前付き右辺値参照は、他の変数と同様に左辺値です。
解決策は、手動で移動を有効にすることです。
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(std::move(parameter)) // note the std::move
{}
};
parameter
これは、の初期化後はもう使用されていないと主張できますmember
。std::move
返り値のように黙って挿入する特別なルールがないのはなぜですか?おそらくコンパイラの実装者にとって負担が大きすぎるためでしょう。たとえば、コンストラクタ本体が別の翻訳単位にある場合はどうなりますか?対照的に、戻り値のルールは、シンボルテーブルをチェックして、return
キーワードの後の識別子が自動オブジェクトを表すかどうかを判断するだけです。
parameter
by値を渡すこともできます。のような移動のみのタイプの場合unique_ptr
、確立されたイディオムはまだないようです。個人的には、インターフェースで混乱を少なくするため、値で渡すことを好みます。
C ++ 98は、3つの特別なメンバー関数をオンデマンドで暗黙的に宣言します。つまり、コピーコンストラクター、コピー代入演算子、およびデストラクタです。
X::X(const X&); // copy constructor
X& X::operator=(const X&); // copy assignment operator
X::~X(); // destructor
右辺値参照はいくつかのバージョンを通過しました。バージョン3.0以降、C ++ 11は必要に応じて2つの特別なメンバー関数を宣言します。移動コンストラクターと移動代入演算子です。VC10もVC11もまだバージョン3.0に準拠していないため、自分で実装する必要があることに注意してください。
X::X(X&&); // move constructor
X& X::operator=(X&&); // move assignment operator
これら2つの新しい特別なメンバー関数は、特別なメンバー関数が手動で宣言されていない場合にのみ暗黙的に宣言されます。また、独自の移動コンストラクターまたは移動割り当て演算子を宣言する場合、コピーコンストラクターもコピー割り当て演算子も暗黙的に宣言されません。
これらのルールは実際にはどういう意味ですか?
アンマネージリソースなしでクラスを作成する場合、5つの特別なメンバー関数を自分で宣言する必要はなく、正しいコピーセマンティクスと移動セマンティクスを無料で取得できます。それ以外の場合は、特別なメンバー関数を自分で実装する必要があります。もちろん、クラスが移動セマンティクスの恩恵を受けない場合は、特別な移動操作を実装する必要はありません。
コピー代入演算子と移動代入演算子は、値によって引数を取る単一の統合代入演算子に融合できることに注意してください。
X& X::operator=(X source) // unified assignment operator
{
swap(source); // see my first answer for an explanation
return *this;
}
このようにして、実装する特別なメンバー関数の数は5から4に減ります。例外安全性と効率性の間にはトレードオフがありますが、私はこの問題の専門家ではありません。
次の関数テンプレートを検討してください。
template<typename T>
void foo(T&&);
T&&
一見すると右辺値参照のように見えるため、右辺値にのみバインドすることを期待するかもしれません。T&&
結局のところ、左辺値にもバインドされます。
foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&
引数の型の右辺値である場合はX
、T
あることを推測されX
、したがって、T&&
手段X&&
。これは誰もが期待することです。しかし、引数が型の左辺値である場合、X
特別な規則により、であるT
と推定されるためX&
、のT&&
ような意味になりX& &&
ます。しかし、C ++には参照への参照の概念がないため、型X& &&
はに縮小されX&
ます。これは最初は混乱して役に立たないように聞こえるかもしれませんが、参照の折りたたみは完全な転送に不可欠です(これについてはここでは説明しません)。
T &&は右辺値参照ではなく、転送参照です。また、左辺値にバインドします。この場合
T
、T&&
どちらも左辺値参照です。
関数テンプレートを右辺値に制限したい場合は、SFINAEをタイプ特性と組み合わせることができます。
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
参照の折りたたみを理解したので、次はそのstd::move
実装方法です。
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
ご覧のとおりmove
、転送参照のおかげであらゆる種類のパラメーターを受け入れT&&
、右辺値参照を返します。std::remove_reference<T>::type
それ以外の場合は、種類の左辺値のためのためのメタ関数呼び出しが必要でX
、戻り値の型は次のようになりX& &&
に崩壊することになります、X&
。のでt
(という名前の右辺値参照は左辺値であることを覚えておいてください)常に左辺値ですが、私たちが結合したいt
右辺値参照に、我々は明示的にキャストする必要がありt
、正しい戻り値の型に。右辺値参照を返す関数の呼び出し自体がx値です。これで、xvalueがどこから来たかがわかります;)
などの右辺値参照を返す関数の呼び出しは
std::move
x値です。
この例でt
は、は自動オブジェクトではなく、呼び出し元から渡されたオブジェクトを示すため、右辺値参照で返すのは問題ありません。
移動セマンティクスは右辺値参照に基づいています。
右辺値は一時的なオブジェクトであり、式の最後で破棄されます。現在のC ++では、右辺const
値は参照にのみバインドされます。C ++ 1xは、右辺値オブジェクトへの参照である、const
右辺値以外の参照を許可しT&&
ます。
右辺値は式の最後で死ぬので、そのデータを盗むことができます。別のオブジェクトにコピーする代わりに、そのデータをそこに移動します。
class X {
public:
X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
: data_()
{
// since 'x' is an rvalue object, we can steal its data
this->swap(std::move(rhs));
// this will leave rhs with the empty data
}
void swap(X&& rhs);
// ...
};
// ...
X f();
X x = f(); // f() returns result as rvalue, so this calls move-ctor
上記のコードでは、古いコンパイラとの結果がf()
されるコピーにx
使用してX
のコピーコンストラクタを。コンパイラがmoveセマンティクスをサポートしX
、moveコンストラクタがある場合は、代わりにそれが呼び出されます。そのrhs
引数は右辺値なので、もう必要ないことがわかっており、その値を盗むことができます。
値がされるように移動から戻った無名の一時的なからf()
にx
(のデータは一方でx
、空に初期化しX
、割り当て後に破棄されますこれは、一時的に移動されます)。
this->swap(std::move(rhs));
名前付き右辺値参照は左辺値であるため、これは
rhs
ある左辺値は、のコンテキストでX::X(X&& rhs)
。std::move(rhs)
右辺値を取得するために呼び出す必要がありますが、これはちょっと答えを無効にします。
実質的なオブジェクトを返す関数があるとします。
Matrix multiply(const Matrix &a, const Matrix &b);
このようなコードを書くと:
Matrix r = multiply(a, b);
次に、通常のC ++コンパイラは、の結果用の一時オブジェクトを作成しmultiply()
、コピーコンストラクターを呼び出して初期化しr
、一時的な戻り値を破棄します。C ++ 0xの移動セマンティクスでは、「移動コンストラクター」を呼び出してr
その内容をコピーすることで初期化し、一時的な値を破棄せずに破棄できます。
これは、(Matrix
上記の例のように)コピーされるオブジェクトがヒープに追加のメモリを割り当ててその内部表現を保存する場合に特に重要です。コピーコンストラクターは、内部表現の完全なコピーを作成するか、参照カウントとコピーオンライトセマンティクスを内部で使用する必要があります。移動コンストラクタは、ヒープメモリをそのままにし、Matrix
オブジェクト内のポインタをコピーするだけです。
移動のセマンティクスの詳細な説明に本当に興味がある場合は、それらに関する元の論文「C ++言語に移動のセマンティクスサポートを追加する提案」を読むことを強くお勧めします。
それは非常にアクセスしやすく、読みやすく、彼らが提供する利点の優れた例になります。WG21のWebサイトでは、移動のセマンティクスに関する最新の最新の論文が他にもありますが、これはトップレベルの視点から物事に取り組み、粗末な言語の詳細にあまり触れないため、おそらく最も簡単なものです。
移動セマンティクスは、ソース値が不要になったときにリソースをコピーするのではなく、転送することです。
C ++ 03では、オブジェクトがコピーされることが多く、コードが値を再び使用する前に破棄または割り当てられます。たとえば、RVOが起動しない限り、関数から値で戻ると、返される値は呼び出し元のスタックフレームにコピーされ、その後スコープ外になり破棄されます。これは多くの例の1つにすぎません。ソースオブジェクトが一時的な場合の値渡し、sort
アイテムの再配置、超過したvector
場合の再割り当てなどのアルゴリズムを参照してくださいcapacity()
。
このようなコピー/破棄のペアが高価な場合、通常はオブジェクトが重いリソースを所有していることが原因です。たとえば、vector<string>
はstring
、それぞれ独自の動的メモリを持つオブジェクトの配列を含む動的に割り当てられたメモリブロックを所有している場合があります。このようなオブジェクトのコピーはコストがかかります。ソース内の動的に割り当てられたブロックごとに新しいメモリを割り当て、すべての値をコピーする必要があります。 次に、コピーしたメモリの割り当てをすべて解除する必要があります。ただし、大きく移動するvector<string>
ことは、(動的メモリブロックを参照する)いくつかのポインターを宛先にコピーして、ソースでそれらをゼロにすることを意味します。
簡単な(実用的な)用語で:
オブジェクトのコピーとは、「静的」メンバーをコピーしnew
、動的オブジェクトのオペレーターを呼び出すことを意味します。正しい?
class A
{
int i, *p;
public:
A(const A& a) : i(a.i), p(new int(*a.p)) {}
~A() { delete p; }
};
ただし、オブジェクトを移動する(実際の観点では繰り返します)とは、動的オブジェクトのポインターをコピーすることだけを意味し、新しいオブジェクトを作成することを意味しません。
しかし、それは危険ではありませんか?もちろん、動的オブジェクトを2回破棄することもできます(セグメンテーション違反)。したがって、それを回避するには、ソースポインターを2回破壊するのを避けるために、ソースポインターを「無効化」する必要があります。
class A
{
int i, *p;
public:
// Movement of an object inside a copy constructor.
A(const A& a) : i(a.i), p(a.p)
{
a.p = nullptr; // pointer invalidated.
}
~A() { delete p; }
// Deleting NULL, 0 or nullptr (address 0x0) is safe.
};
わかりましたが、オブジェクトを移動すると、ソースオブジェクトが役に立たなくなります。もちろん、特定の状況では非常に便利です。最も明白なのは、匿名オブジェクト(時間、右辺値オブジェクトなど)を使用して関数を呼び出すときです。別の名前で呼び出すことができます。
void heavyFunction(HeavyType());
その場合、匿名オブジェクトが作成され、次に関数パラメーターにコピーされ、その後削除されます。したがって、匿名オブジェクトは必要なく、時間とメモリを節約できるので、ここではオブジェクトを移動する方が適切です。
これは、「右辺値」参照の概念につながります。それらはC ++ 11にのみ存在し、受信したオブジェクトが匿名かどうかを検出します。「lvalue」が代入可能なエンティティー(=
演算子の左側)であることはすでにご存じだと思います。したがって、lvalueとして機能するには、オブジェクトへの名前付き参照が必要です。右辺値は正反対で、名前付き参照のないオブジェクトです。そのため、匿名オブジェクトと右辺値は同義語です。そう:
class A
{
int i, *p;
public:
// Copy
A(const A& a) : i(a.i), p(new int(*a.p)) {}
// Movement (&& means "rvalue reference to")
A(A&& a) : i(a.i), p(a.p)
{
a.p = nullptr;
}
~A() { delete p; }
};
この場合、型のオブジェクトをA
「コピー」する必要がある場合、コンパイラーは、渡されたオブジェクトに名前が付けられているかどうかに応じて、左辺値参照または右辺値参照を作成します。そうでない場合は、moveコンストラクターが呼び出され、オブジェクトが一時的であることがわかっているため、動的オブジェクトをコピーする代わりに移動して、スペースとメモリを節約できます。
「静的」オブジェクトは常にコピーされることを覚えておくことが重要です。静的オブジェクト(ヒープ内ではなくスタック内のオブジェクト)を「移動」する方法はありません。したがって、オブジェクトに動的メンバーが(直接または間接的に)ない場合の「移動」/「コピー」の区別は関係ありません。
オブジェクトが複雑で、デストラクタがライブラリの関数の呼び出し、他のグローバル関数の呼び出し、または何であれ、その他の二次的な影響を持っている場合は、フラグを使って動きを通知する方がよいでしょう。
class Heavy
{
bool b_moved;
// staff
public:
A(const A& a) { /* definition */ }
A(A&& a) : // initialization list
{
a.b_moved = true;
}
~A() { if (!b_moved) /* destruct object */ }
};
したがって、コードは短く(nullptr
各動的メンバーに割り当てを行う必要がない)、より一般的です。
その他の一般的な質問:どのような違いであるA&&
とはconst A&&
?もちろん、最初のケースではオブジェクトを変更でき、2番目のケースでは変更できませんが、実用的な意味はありますか?2番目のケースでは、オブジェクトを変更できないため、オブジェクトを無効にする方法がなく(変更可能なフラグなどを使用する場合を除く)、コピーコンストラクターと実際的な違いはありません。
そして、完璧な転送とは何ですか?「右辺値参照」は「呼び出し元のスコープ」内の名前付きオブジェクトへの参照であることを理解することが重要です。ただし、実際のスコープでは、右辺値参照はオブジェクトの名前であるため、名前付きオブジェクトとして機能します。右辺値参照を別の関数に渡すと、名前付きオブジェクトが渡されるので、オブジェクトは一時オブジェクトのように受け取られません。
void some_function(A&& a)
{
other_function(a);
}
オブジェクトa
はの実際のパラメータにコピーされますother_function
。オブジェクトをa
引き続き一時オブジェクトとして扱いたい場合は、次のstd::move
関数を使用する必要があります。
other_function(std::move(a));
この行でstd::move
は、はa
右辺値にキャストし、other_function
名前のないオブジェクトとしてオブジェクトを受け取ります。もちろん、other_function
が名前のないオブジェクトを処理する特定のオーバーロードを持たない場合、この区別は重要ではありません。
それは完璧な転送ですか?そうではありませんが、非常に近いです。完全転送は、テンプレートを操作する場合にのみ役立ちます。つまり、オブジェクトを別の関数に渡す必要がある場合、名前付きオブジェクトを受け取った場合、オブジェクトは名前付きオブジェクトとして渡され、そうでない場合は、名前のないオブジェクトのように渡したい:
template<typename T>
void some_function(T&& a)
{
other_function(std::forward<T>(a));
}
これは、C ++ 11でによって実装された、完全転送を使用するプロトタイプ関数のシグネチャですstd::forward
。この関数は、テンプレートのインスタンス化のいくつかのルールを利用します。
`A& && == A&`
`A&& && == A&&`
したがって、T
がA
(T = A&)への左辺値参照である場合、a
(A&&& => A&)も参照します。場合は、T
へ右辺値参照されA
、a
また、(A && && => A &&)。どちらの場合も、a
は実際のスコープ内の名前付きオブジェクトですがT
、呼び出し元のスコープの観点から見た「参照タイプ」の情報が含まれています。この情報(T
)はテンプレートパラメータとしてに渡されforward
、「a」はのタイプに応じて移動されT
ます。
コピーの意味が正しいことを知っていますか?これは、コピー可能な型があることを意味します。ユーザー定義型の場合、これを定義するには、コピーコンストラクター&代入演算子を明示的に記述するか、コンパイラーが暗黙的に生成します。これはコピーを行います。
移動セマンティクスは基本的に、const以外のr値参照(&&(はい2つのアンパサンド)を使用した新しいタイプの参照)を取るコンストラクターを持つユーザー定義型です。これは移動コンストラクターと呼ばれ、割り当て演算子と同じです。したがって、moveコンストラクタは何をするのでしょうか。そのソース引数からメモリをコピーする代わりに、ソースから宛先にメモリを「移動」します。
いつそれをしたいですか?std :: vectorは例です。一時的なstd :: vectorを作成し、関数からそれを返すとします。
std::vector<foo> get_foos();
std :: vectorがmoveコンストラクターを持っている場合、関数が戻ると、コピーコンストラクターからオーバーヘッドが発生します。std:: vectorは、コピーするのではなく、ポインターを設定して動的に割り当てられる 'move'だけです。新しいインスタンスへのメモリ。これは、std :: auto_ptrを使用した所有権譲渡のセマンティクスのようなものです。
移動セマンティクスの必要性を説明するために、移動セマンティクスなしのこの例を考えてみましょう:
次の関数は、型のオブジェクトを受け取りT
、同じ型のオブジェクトを返しますT
。
T f(T o) { return o; }
//^^^ new object constructed
上記の関数は値による呼び出しを使用します。つまり、この関数が呼び出された場合、関数で使用するためにオブジェクトを構築する必要があります。
関数も値によって戻るため、戻り値用に別の新しいオブジェクトが作成されます。
T b = f(a);
//^ new object constructed
2つの新しいオブジェクトが作成されました。そのうちの1つは、関数の実行中にのみ使用される一時オブジェクトです。
戻り値から新しいオブジェクトが作成されると、コピーコンストラクターが呼び出され、一時オブジェクトの内容が新しいオブジェクトにコピーされます。関数が完了すると、関数で使用された一時オブジェクトはスコープ外になり、破棄されます。
ここで、コピーコンストラクタの機能について考えてみましょう。
最初にオブジェクトを初期化し、次にすべての関連データを古いオブジェクトから新しいオブジェクトにコピーする必要があります。
クラスによっては、おそらく非常に多くのデータを含むコンテナであり、それは多くの時間とメモリ使用量を表す可能性があります
// Copy constructor
T::T(T &old) {
copy_data(m_a, old.m_a);
copy_data(m_b, old.m_b);
copy_data(m_c, old.m_c);
}
移動セマンティクスを使用すると、コピーするのではなくデータを移動するだけで、この作業のほとんどを不快にしないようにすることができます。
// Move constructor
T::T(T &&old) noexcept {
m_a = std::move(old.m_a);
m_b = std::move(old.m_b);
m_c = std::move(old.m_c);
}
データを移動するには、データを新しいオブジェクトに再度関連付ける必要があります。そして、コピーはまったく行われません。
これは、rvalue
リファレンスを使用して実行されます。リファレンスはかなりのように動作し、一つの重要な違いが参照:右辺値参照を移動させることができると左辺値はできません。rvalue
lvalue
強力な例外保証を可能にするために、ユーザー定義の移動コンストラクターは例外をスローしてはなりません。実際、標準のコンテナーは通常、コンテナー要素を再配置する必要があるときに、移動とコピーのどちらを選択するかをstd :: move_if_noexceptに依存しています。コピーコンストラクターと移動コンストラクターの両方が指定されている場合、引数が右辺値(無名の一時変数などのprvalueまたはstd :: moveの結果などのxvalue)の場合、オーバーロード解決は移動コンストラクターを選択し、次の場合はコピーコンストラクターを選択します。引数は左辺値(名前付きオブジェクト、または左辺値参照を返す関数/演算子)です。コピーコンストラクターのみが提供されている場合、すべての引数カテゴリーがそれを選択し(convalueへの参照が必要である限り、右辺値はconst参照にバインドできるため)、移動が利用できない場合に、移動のフォールバックをコピーします。多くの場合、移動コンストラクターは、観察可能な副作用が生じる場合でも最適化されます。コピーの省略を参照してください。コンストラクターは、右辺値参照をパラメーターとして取る場合、「移動コンストラクター」と呼ばれます。何かを移動する義務はありません。クラスにリソースを移動する必要はありません。パラメータがaである許容される(ただし、多分賢くない)場合と同様に、「移動コンストラクタ」はリソースを移動できない場合があります。 const右辺値参照(const T &&)。
私はそれを正しく理解するためにこれを書いています。
移動オブジェクトは、ラージオブジェクトの不要なコピーを回避するために作成されました。Bjarne Stroustrupの著書「C ++プログラミング言語」では、デフォルトで不要なコピーが行われる2つの例を使用しています。1つは2つのラージオブジェクトのスワッピング、もう1つはメソッドからのラージオブジェクトの戻りです。
通常、2つの大きなオブジェクトを交換するには、最初のオブジェクトを一時オブジェクトにコピーし、2番目のオブジェクトを最初のオブジェクトにコピーし、一時オブジェクトを2番目のオブジェクトにコピーします。組み込み型の場合、これは非常に高速ですが、大きなオブジェクトの場合、これらの3つのコピーにはかなりの時間がかかる可能性があります。「移動割り当て」を使用すると、プログラマーはデフォルトのコピー動作をオーバーライドし、代わりにオブジェクトへの参照をスワップすることができます。つまり、コピーがまったくなく、スワップ操作がはるかに高速になります。移動割り当ては、std :: move()メソッドを呼び出すことで呼び出すことができます。
デフォルトでメソッドからオブジェクトを返すには、ローカルオブジェクトとその関連データのコピーを、呼び出し元がアクセスできる場所に作成する必要があります(ローカルオブジェクトは呼び出し元にアクセスできず、メソッドが終了すると消えます)。組み込み型が返される場合、この操作は非常に高速ですが、大きなオブジェクトが返される場合、これには時間がかかる可能性があります。moveコンストラクターを使用すると、プログラマーはこのデフォルトの動作をオーバーライドし、代わりにローカルオブジェクトに関連付けられたヒープデータを呼び出し元に返すことにより、ローカルオブジェクトに関連付けられたヒープデータを「再利用」できます。したがって、コピーは必要ありません。
ローカルオブジェクト(つまり、スタック上のオブジェクト)の作成を許可しない言語では、すべてのオブジェクトがヒープに割り当てられ、常に参照によってアクセスされるため、この種の問題は発生しません。
x
してy
、することができますだけでなく、「オブジェクトへのスワップの参照」。オブジェクトに他のデータを参照するポインタが含まれている可能性があります。それらのポインタは交換できますが、移動演算子は何かを交換する必要はありません。それらはdestデータをそこに保存するのではなく、移動されたオブジェクトからデータを一掃するかもしれません。
swap()
移動セマンティクスなしで書くことができます。 「移動割り当ては、std :: move()メソッドを呼び出すことで呼び出すことができます。」- 使用する必要がある場合がありますstd::move()
-実際には何も移動しませんが-引数が移動可能であることをstd::forward<>()
コンパイラに通知するだけです。
ここだ答えビャーネ・ストロヴストルップの著書「C ++プログラミング言語」からは。ビデオを見たくない場合は、以下のテキストをご覧ください。
このスニペットを考えてみましょう。operator +から戻るには、結果をローカル変数res
から呼び出し元がアクセスできる場所にコピーする必要があります。
Vector operator+(const Vector& a, const Vector& b)
{
if (a.size()!=b.size())
throw Vector_siz e_mismatch{};
Vector res(a.size());
for (int i=0; i!=a.size(); ++i)
res[i]=a[i]+b[i];
return res;
}
私たちは本当にコピーを望んでいませんでした。関数から結果を取り出したかっただけです。そのため、ベクターをコピーするのではなく移動する必要があります。次のようにmoveコンストラクタを定義できます。
class Vector {
// ...
Vector(const Vector& a); // copy constructor
Vector& operator=(const Vector& a); // copy assignment
Vector(Vector&& a); // move constructor
Vector& operator=(Vector&& a); // move assignment
};
Vector::Vector(Vector&& a)
:elem{a.elem}, // "grab the elements" from a
sz{a.sz}
{
a.elem = nullptr; // now a has no elements
a.sz = 0;
}
&&は「右辺値参照」を意味し、右辺値をバインドできる参照です。「右辺値」は、「左辺値」を補完することを目的としています。「左辺値」は、「割り当ての左側に表示される可能性があるもの」を大まかに意味します。したがって、右辺値は、関数呼び出しによって返される整数や、res
Vectorのoperator +()のローカル変数など、大まかに「割り当てることができない値」を意味します。
今、ステートメントreturn res;
はコピーされません!