これが良いパターンである理由を理解するには、C++03 と C++11 の両方で代替案を調べる必要があります。
を取得する C++03 メソッドがありstd::string const&
ます。
struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
この場合、常に単一のコピーが実行されます。生の C 文字列から構築する場合、astd::string
が構築され、再度コピーされます: 2 つの割り当て。
std::string
への参照を取得し、それを local にスワップするC++03 メソッドがありますstd::string
。
struct S
{
std::string data;
S(std::string& str)
{
std::swap(data, str);
}
};
これは「移動セマンティクス」の C++03 バージョンであり、swap
多くの場合、非常に安価に実行できるように最適化できます (a によく似ていますmove
)。また、コンテキストで分析する必要があります。
S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal
非一時的な を形成することを強制しstd::string
、それを破棄します。(テンポラリstd::string
は非 const 参照にバインドできません)。ただし、割り当ては 1 つだけです。C++11 バージョンは を取り、それを、または一時的&&
に呼び出す必要があります。これには、呼び出し元が呼び出しの外で明示的にコピーを作成し、そのコピーを関数またはコンストラクターに移動する必要があります。std::move
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
次に、コピーと の両方をサポートする完全な C++11 バージョンを実行できますmove
。
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
これを「最適な」バージョンと並べて比較すると、ちょうど 1 つの追加が行われmove
ます。余分なことは一度もありませんcopy
。
したがって、これが安価であると仮定するとmove
、このバージョンは最も最適なバージョンとほぼ同じパフォーマンスを実現しますが、コードは 2 倍少なくなります。
また、たとえば 2 ~ 10 個の引数を使用する場合、コードの削減は指数関数的です。引数が 1 の場合は 2 倍、2 の場合は 4 倍、3 の場合は 8 倍、4 の場合は 16 倍、引数が 10 の場合は 1024 倍になります。
これで、完全転送と SFINAE を介してこれを回避できます。これにより、10 個の引数を受け取る単一のコンストラクターまたは関数テンプレートを作成し、SFINAE を実行して引数が適切な型であることを確認し、それらを必要に応じてローカル状態。これにより、プログラム サイズの問題が数千倍に増加するのを防ぐことができますが、このテンプレートから大量の関数が生成される可能性があります。(テンプレート関数のインスタンス化は関数を生成します)
また、多数の関数が生成されるということは、実行可能なコードのサイズが大きくなることを意味し、それ自体がパフォーマンスを低下させる可能性があります。
数move
秒のコストで、より短いコードとほぼ同じパフォーマンスが得られ、多くの場合、コードを理解しやすくなります。
これが機能するのは、関数 (この場合はコンストラクター) が呼び出されたときに、その引数のローカル コピーが必要になることがわかっているからです。コピーを作成することがわかっている場合は、コピーを作成していることを呼び出し元に引数リストに入れて知らせる必要があります。次に、彼らは私たちにコピーを提供しようとしているという事実を中心に最適化できます(たとえば、私たちの議論に移ることによって)。
「値による取得」手法のもう 1 つの利点は、ムーブ コンストラクターが noexcept であることが多いことです。つまり、値で取得して引数の外に移動する関数は、多くの場合、noexcept である可能性があり、すべてthrow
の s を本体から呼び出しスコープに移動します。 (直接構築することで回避できる場合もあれば、アイテムを構築move
して引数に入れ、スローが発生する場所を制御することもできます) メソッドを非スローにすることは、多くの場合、価値があります。