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