私はいくつかの例をまとめました。このすべてでGCC4.4.4を使用しました。
シンプルなケース、なし-std=c++0x
まず、それぞれを受け入れる2つのクラスを使用した非常に単純な例をまとめましたstd::string
。
#include <string>
#include <iostream>
struct A /* construct by reference */
{
std::string s_;
A (std::string const &s) : s_ (s)
{
std::cout << "A::<constructor>" << std::endl;
}
A (A const &a) : s_ (a.s_)
{
std::cout << "A::<copy constructor>" << std::endl;
}
~A ()
{
std::cout << "A::<destructor>" << std::endl;
}
};
struct B /* construct by value */
{
std::string s_;
B (std::string s) : s_ (s)
{
std::cout << "B::<constructor>" << std::endl;
}
B (B const &b) : s_ (b.s_)
{
std::cout << "B::<copy constructor>" << std::endl;
}
~B ()
{
std::cout << "B::<destructor>" << std::endl;
}
};
static A f () { return A ("string"); }
static A f2 () { A a ("string"); a.s_ = "abc"; return a; }
static B g () { return B ("string"); }
static B g2 () { B b ("string"); b.s_ = "abc"; return b; }
int main ()
{
A a (f ());
A a2 (f2 ());
B b (g ());
B b2 (g2 ());
return 0;
}
そのプログラムの出力はstdout
次のとおりです。
A::<constructor>
A::<constructor>
B::<constructor>
B::<constructor>
B::<destructor>
B::<destructor>
A::<destructor>
A::<destructor>
結論
A
GCCは、一時的またはB
離れた場所ですべてを最適化することができました。これは、 C++FAQ
と一致しています。基本的に、GCCは、値によって表示される関数が呼び出された場合でも、そのa, a2, b, b2
場で構築するコードを生成する可能性があります(そして生成する意思があります) 。これにより、GCCは、コードを調べることで存在が「推測」された可能性のある一時的なものの多くを回避できます。
次に確認したいstd::string
のは、上記の例で実際にコピーされる頻度です。std::string
よりよく観察して見ることができるものに置き換えましょう。
現実的なケース、なし-std=c++0x
#include <string>
#include <iostream>
struct S
{
std::string s_;
S (std::string const &s) : s_ (s)
{
std::cout << " S::<constructor>" << std::endl;
}
S (S const &s) : s_ (s.s_)
{
std::cout << " S::<copy constructor>" << std::endl;
}
~S ()
{
std::cout << " S::<destructor>" << std::endl;
}
};
struct A /* construct by reference */
{
S s_;
A (S const &s) : s_ (s) /* expecting one copy here */
{
std::cout << "A::<constructor>" << std::endl;
}
A (A const &a) : s_ (a.s_)
{
std::cout << "A::<copy constructor>" << std::endl;
}
~A ()
{
std::cout << "A::<destructor>" << std::endl;
}
};
struct B /* construct by value */
{
S s_;
B (S s) : s_ (s) /* expecting two copies here */
{
std::cout << "B::<constructor>" << std::endl;
}
B (B const &b) : s_ (b.s_)
{
std::cout << "B::<copy constructor>" << std::endl;
}
~B ()
{
std::cout << "B::<destructor>" << std::endl;
}
};
/* expecting a total of one copy of S here */
static A f () { S s ("string"); return A (s); }
/* expecting a total of one copy of S here */
static A f2 () { S s ("string"); s.s_ = "abc"; A a (s); a.s_.s_ = "a"; return a; }
/* expecting a total of two copies of S here */
static B g () { S s ("string"); return B (s); }
/* expecting a total of two copies of S here */
static B g2 () { S s ("string"); s.s_ = "abc"; B b (s); b.s_.s_ = "b"; return b; }
int main ()
{
A a (f ());
std::cout << "" << std::endl;
A a2 (f2 ());
std::cout << "" << std::endl;
B b (g ());
std::cout << "" << std::endl;
B b2 (g2 ());
std::cout << "" << std::endl;
return 0;
}
そして、残念ながら、出力は期待を満たしています。
S::<constructor>
S::<copy constructor>
A::<constructor>
S::<destructor>
S::<constructor>
S::<copy constructor>
A::<constructor>
S::<destructor>
S::<constructor>
S::<copy constructor>
S::<copy constructor>
B::<constructor>
S::<destructor>
S::<destructor>
S::<constructor>
S::<copy constructor>
S::<copy constructor>
B::<constructor>
S::<destructor>
S::<destructor>
B::<destructor>
S::<destructor>
B::<destructor>
S::<destructor>
A::<destructor>
S::<destructor>
A::<destructor>
S::<destructor>
結論
GCCは、のコンストラクターによって作成された一時的なものを最適化できませんでした。のデフォルトのコピーコンストラクタを使用しても、それは変わりませんでした。に変わるS
B
S
f, g
static A f () { return A (S ("string")); } // still one copy
static B g () { return B (S ("string")); } // reduced to one copy!
示された効果がありました。GCCは、コンストラクターへの引数を適切に構築することをいとわないようですが、メンバーをB
適切に構築することを躊躇しています。B
まだ一時的A
またはB
作成されていないことに注意してください。つまりa, a2, b, b2
、まだ適切に構築されているということです。涼しい。
ここで、新しい移動セマンティクスが2番目の例にどのように影響するかを調べてみましょう。
現実的なケース、-std=c++0x
次のコンストラクターをに追加することを検討してくださいS
S (S &&s) : s_ ()
{
std::swap (s_, s.s_);
std::cout << " S::<move constructor>" << std::endl;
}
そして、B
のコンストラクタをに変更します
B (S &&s) : s_ (std::move (s)) /* how many copies?? */
{
std::cout << "B::<constructor>" << std::endl;
}
この出力を取得します
S::<constructor>
S::<copy constructor>
A::<constructor>
S::<destructor>
S::<constructor>
S::<copy constructor>
A::<constructor>
S::<destructor>
S::<constructor>
S::<move constructor>
B::<constructor>
S::<destructor>
S::<constructor>
S::<move constructor>
B::<constructor>
S::<destructor>
B::<destructor>
S::<destructor>
B::<destructor>
S::<destructor>
A::<destructor>
S::<destructor>
A::<destructor>
S::<destructor>
したがって、パスバイ右辺値を使用して、 4つのコピーを2つの移動に置き換えることができました。
しかし、実際には壊れたプログラムを作成しました。
想起g, g2
static B g () { S s ("string"); return B (s); }
static B g2 () { S s ("string"); s.s_ = "abc"; B b (s); /* s is zombie now */ b.s_.s_ = "b"; return b; }
マークされた場所は問題を示しています。一時的ではないオブジェクトに対して移動が行われました。これは、右辺値参照が一時値にもバインドされる可能性があることを除いて、左辺値参照のように動作するためです。B
したがって、定数の左辺値参照をとるコンストラクターでのコンストラクターをオーバーロードすることを忘れてはなりません。
B (S const &s) : s_ (s)
{
std::cout << "B::<constructor2>" << std::endl;
}
次に、どちらの場合もシンボルが右辺値参照よりも定数参照に適しているため、両方g, g2
が「constructor2」を呼び出すことに気付くでしょう。次の2つの方法のいずれかでs
、コンパイラに移動を実行するように説得できます。g
static B g () { return B (S ("string")); }
static B g () { S s ("string"); return B (std::move (s)); }
結論
値による戻りを行います。このコードは、「私が提供する参照を埋める」コードよりも読みやすく、より高速で、おそらく例外安全性がさらに高くなります。
f
に変更することを検討してください
static void f (A &result) { A tmp; /* ... */ result = tmp; } /* or */
static void f (A &result) { /* ... */ result = A (S ("string")); }
それは、の割り当てがそれを提供する場合にのみ、強力な保証を満たします。A
にコピーするresult
ことはスキップできません。また、が構築されていないためtmp
、の代わりに構築することもできません。したがって、コピーが不要だった以前よりも遅くなります。C ++ 0xコンパイラとムーブ代入演算子はオーバーヘッドを削減しますが、それでも値による戻りよりも低速です。result
result
値によるリターンは、より簡単に強力な保証を提供します。オブジェクトは所定の位置に構築されます。その一部が失敗し、他の部分がすでに構築されている場合、通常の巻き戻しはクリーンアップされ、S
コンストラクターが自身のメンバーに関する基本保証とグローバルアイテムに関する強力な保証を満たしている限り、全体が返されます。値によるプロセスは、実際には強力な保証を提供します。
とにかく(スタックに)コピーする場合は、常に値を渡します
スピードが欲しいで説明したように?値渡し。。コンパイラーは、可能であれば、呼び出し元の引数を適切に構成するコードを生成し、コピーを削除します。これは、参照して手動でコピーする場合には実行できません。主な例:これを書かないでください(引用された記事から引用)
T& T::operator=(T const& x) // x is a reference to the source
{
T tmp(x); // copy construction of tmp does the hard work
swap(*this, tmp); // trade our resources for tmp's
return *this; // our (old) resources get destroyed with tmp
}
しかし、常にこれを好む
T& T::operator=(T x) // x is a copy of the source; hard work already done
{
swap(*this, x); // trade our resources for x's
return *this; // our (old) resources get destroyed with x
}
スタック以外のフレームの場所にコピーする場合は、C ++ 0xより前のconst参照を渡し、さらにC++0xより後の右辺値参照を渡します。
私たちはすでにこれを見ました。参照渡しは、値渡しよりもインプレース構築が不可能な場合に発生するコピーが少なくなります。また、C ++ 0xの移動セマンティクスは、多くのコピーをより少なく安価な移動に置き換える可能性があります。ただし、移動すると、移動元のオブジェクトからゾンビが作成されることに注意してください。移動はコピーではありません。上に示したように、右辺値参照を受け入れるコンストラクターを提供するだけで問題が発生する可能性があります。
スタック以外のフレームの場所にコピーして、を持っている場合はswap
、とにかく値を渡すことを検討してください(C ++ 0xより前)
swap
安価なデフォルトの構造を使用している場合は、それをaと組み合わせると、コピーするよりも効率的です。S
のコンストラクタを次のように考えます
S (std::string s) : s_ (/* is this cheap for your std::string? */)
{
s_.swap (s); /* then this may be faster than copying */
std::cout << " S::<constructor>" << std::endl;
}