誰かがオブジェクトをコピーし、その後クラスのデータメンバーに移動することを決めたコードを見ました。これは、移動のすべてのポイントがコピーを避けることであると思ったという点で私を混乱させました。以下に例を示します。
struct S
{
S(std::string str) : data(std::move(str))
{}
};
私の質問は次のとおりです。
str
への右辺値参照を取得しないのですか?std::string
?私があなたの質問に答える前に、あなたが間違っているように思われる1つのこと:C++ 11で値をとることは常にコピーを意味するわけではありません。右辺値が渡された場合、それはコピーされずにmoved(実行可能な移動コンストラクターが存在する場合)になります。そしてstd::string
には移動コンストラクターがあります。
C++ 03とは異なり、C++ 11では、以下で説明する理由により、値によってパラメーターを取得することはしばしば慣用的です。パラメーターを受け入れる方法に関するより一般的な一連のガイドラインについては、 このStackOverflowのQ&A も参照してください。
なぜ
str
への右辺値参照を取得しないのですか?
次のような左辺値を渡すことが不可能になるためです。
std::string s = "Hello";
S obj(s); // s is an lvalue, this won't compile!
S
に右辺値を受け入れるコンストラクターのみがある場合、上記はコンパイルされません。
コピーは高価ではありません。特に
std::string
?
右辺値を渡すと、movedがstr
に移動し、最終的にはdata
に移動します。コピーは実行されません。一方、左辺値を渡すと、その左辺値はstr
にcopiedされ、data
に移動されます。
まとめると、右辺値に対して2つの動き、左辺値に対して1つのコピーと1つの動きです。
作者がコピーを作成してから移動を決定する理由は何でしょうか?
まず、前述したように、最初のものは常にコピーではありません。これは、答えは次のとおりです。「効率的であるため(std::string
オブジェクトは安価であり、シンプルです)。
移動は安価であるという仮定の下で(ここではSSOを無視します)、この設計の全体的な効率を考慮すると、移動は実質的に無視できます。その場合、左辺値のコピーが1つあり(const
への左辺値参照を受け入れた場合と同様)、右辺値のコピーはありません(一方、左辺値参照を受け入れた場合はコピーがあります) const
)。
つまり、値による取得は、左辺値が指定されている場合はconst
への左辺値参照による取得と同等であり、右辺値が指定されている場合はより適切です。
追伸:コンテキストを提供するために、 これはQ&Aです OPが参照していると思います。
これが良いパターンである理由を理解するために、C++ 03とC++ 11の両方の選択肢を検討する必要があります。
std::string const&
を取得するC++ 03メソッドがあります。
struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
この場合、always実行される単一のコピーがあります。生のC文字列から構築する場合、std::string
が構築され、再度コピーされます:2つの割り当て。
std::string
への参照を取得し、それをローカルstd::string
にスワップするC++ 03メソッドがあります。
struct S
{
std::string data;
S(std::string& str)
{
std::swap(data, str);
}
};
これは「移動セマンティクス」のC++ 03バージョンであり、swap
はしばしば非常に安価に最適化できます(move
と同様)。また、コンテキストで分析する必要があります。
S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal
そして、一時的ではないstd::string
を強制的に作成し、それを破棄します。 (一時的なstd::string
は非const参照にバインドできません)。ただし、割り当ては1回だけです。 C++ 11バージョンは&&
を取り、std::move
または一時的に呼び出す必要があります。これは、呼び出し元explicitlyの外部にコピーを作成する必要があります呼び出し、そのコピーを関数またはコンストラクターに移動します。
struct S
{
std::string data;
S(std::string&& str): data(std::move(str))
{}
};
使用する:
S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal
次に、コピーとmove
の両方をサポートする完全なC++ 11バージョンを実行できます。
struct S
{
std::string data;
S(std::string const& str) : data(str) {} // lvalue const, copy
S(std::string && str) : data(std::move(str)) {} // rvalue, move
};
次に、これがどのように使用されるかを調べることができます。
S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data
std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data
std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data
この2つのオーバーロード手法が、上記の2つのC++ 03スタイルと少なくとも同じくらい効率的であることは明らかです。この2オーバーロードバージョンを「最適な」バージョンと呼びます。
ここで、コピーによるバージョンを調べます。
struct S2 {
std::string data;
S2( std::string arg ):data(std::move(x)) {}
};
これらの各シナリオで:
S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data
std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data
std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data
このサイドバイサイドを「最も最適な」バージョンと比較する場合、move
!を1つ追加します。余分なcopy
を一度もしません。
したがって、move
が安価であると仮定すると、このバージョンでは、最適なバージョンとほぼ同じパフォーマンスが得られますが、コードは2倍少なくなります。
また、2〜10個の引数を取る場合、コードの削減は指数関数的です。1個の引数で2倍、2個で4x、3個で8x、4個で1024x、10個の引数で1024xです。
これで、完全な転送とSFINAEを介してこれを回避できます。10個の引数を取る単一のコンストラクタまたは関数テンプレートを記述し、SFINAEを実行して引数が適切な型であることを確認してから、必要に応じてローカル状態。これにより、プログラムサイズの千倍の問題が防止されますが、このテンプレートから生成された関数の山がまだある可能性があります。 (テンプレート関数のインスタンス生成は関数を生成します)
また、生成された関数の多くは実行可能コードのサイズが大きくなることを意味し、それ自体がパフォーマンスを低下させる可能性があります。
いくつかのmove
sのコストで、コードが短くなり、パフォーマンスがほぼ同じになり、コードを理解しやすくなります。
これが機能するのは、関数(この場合はコンストラクター)が呼び出されたときに、その引数のローカルコピーが必要になることがわかっているためです。アイデアは、コピーを作成することがわかっている場合は、引数リストにコピーしてコピーを作成していることを呼び出し元に知らせる必要があるということです。その後、彼らはコピーを提供するという事実を中心に最適化することができます(たとえば、議論に移ります)。
「値による取得」手法のもう1つの利点は、移動コンストラクターがnoexceptであることが多いということです。これは、値によって取得して引数から移動する関数が多くの場合noexceptであり、throw
sを本体から移動し、 (場合によっては、直接の構築によって回避するか、アイテムとmove
を引数に構築して、スローが発生する場所を制御できます。メソッドnothrowを作成することは価値があります。
これはおそらく意図的なものであり、 コピーとスワップのイディオム に似ています。基本的に、文字列はコンストラクターの前にコピーされるため、一時的な文字列strのみをスワップ(移動)するため、コンストラクター自体は例外セーフです。
移動用のコンストラクターとコピー用のコンストラクターを作成することで、自分自身を繰り返したくありません。
S(std::string&& str) : data(std::move(str)) {}
S(const std::string& str) : data(str) {}
特に複数の引数がある場合、これは非常に定型的なコードです。あなたのソリューションは、不必要な移動のコストでその重複を避けます。 (ただし、移動操作は非常に安価である必要があります。)
競合するイディオムは、完全な転送を使用することです。
template <typename T>
S(T&& str) : data(std::forward<T>(str)) {}
テンプレートマジックは、渡されたパラメーターに応じて移動またはコピーを選択します。基本的に、両方のコンストラクターが手動で作成された最初のバージョンに拡張されます。背景情報については、Scott Meyerの ユニバーサルリファレンス に関する投稿を参照してください。
パフォーマンスの観点から、完全な転送バージョンは不要な移動を回避するため、バージョンよりも優れています。ただし、バージョンの方が読み書きが簡単であると主張できます。とにかく、パフォーマンスへの影響の可能性はほとんどの状況で重要ではないので、最終的にはスタイルの問題のようです。