コピー代入演算子がリソースをリサイクルできる型の場合、コピーとの交換がコピー代入演算子を実装する最良の方法になることはほとんどありません。たとえば、次を見てくださいstd::vector
。
このクラスは、動的にサイズ変更されたバッファーを管理し、a capacity
(バッファーが保持できる最大長) と a size
(現在の長さ) の両方を維持します。vector
コピー代入演算子が実装されている場合、swap
何があっても新しいバッファが常に割り当てられrhs.size() != 0
ます。
ただし、 の場合lhs.capacity() >= rhs.size()
、新しいバッファをまったく割り当てる必要はありません。rhs
からまでの要素を単純に割り当て/構築できますlhs
。要素の型が自明にコピー可能な場合、これはmemcpy
. これは、バッファの割り当てと割り当て解除よりもはるかに高速です。
の同じ問題std::string
。
および/またはであるデータ メンバーがあるMyType
場合の同じ問題。MyType
std::vector
std::string
スワップを使用したコピー割り当ての実装を検討する必要があるのは 2 回だけです。
このswap
方法 (rhs が左辺値である場合の必須のコピー作成を含む) は、それほど非効率的ではないことがわかっています。
強力な例外安全性を保証するには、コピー代入演算子が常に必要になることがわかっています。
2 について確信が持てない場合、つまり、コピー代入演算子が強力な例外安全性の保証を必要とする場合があると思われる場合は、スワップに関して代入を実装しないでください。次のいずれかを提供すれば、クライアントは同じ保証を簡単に達成できます。
- noexcept スワップ。
- noexcept 移動代入演算子。
例えば:
template <class T>
T&
strong_assign(T& x, T y)
{
using std::swap;
swap(x, y);
return x;
}
また:
template <class T>
T&
strong_assign(T& x, T y)
{
x = std::move(y);
return x;
}
現在、スワップを使用してコピー割り当てを実装することが理にかなっているタイプがいくつかあります。ただし、これらのタイプは例外であり、規則ではありません。
の上:
void push_back(const value_type& val);
void push_back(value_type&& val);
どこを想像してくださいvector<big_legacy_type>
:
class big_legacy_type
{
public:
big_legacy_type(const big_legacy_type&); // expensive
// no move members ...
};
もし私たちが持っていたのは:
void push_back(value_type val);
次にpush_back
、左辺値big_legacy_type
を a にing すると、十分なvector
場合でも、1 つではなく 2 つのコピーが必要になります。capacity
それは、パフォーマンスに関しては惨事になるでしょう。
アップデート
これは、C++11 準拠のプラットフォームで実行できる HelloWorld です。
#include <vector>
#include <random>
#include <chrono>
#include <iostream>
class X
{
std::vector<int> v_;
public:
explicit X(unsigned s) : v_(s) {}
#if SLOW_DOWN
X(const X&) = default;
X(X&&) = default;
X& operator=(X x)
{
v_.swap(x.v_);
return *this;
}
#endif
};
std::mt19937_64 eng;
std::uniform_int_distribution<unsigned> size(0, 1000);
std::chrono::high_resolution_clock::duration
test(X& x, const X& y)
{
auto t0 = std::chrono::high_resolution_clock::now();
x = y;
auto t1 = std::chrono::high_resolution_clock::now();
return t1-t0;
}
int
main()
{
const int N = 1000000;
typedef std::chrono::duration<double, std::nano> nano;
nano ns(0);
for (int i = 0; i < N; ++i)
{
X x1(size(eng));
X x2(size(eng));
ns += test(x1, x2);
}
ns /= N;
std::cout << ns.count() << "ns\n";
}
X
のコピー代入演算子を次の 2 つの方法でコーディングしました。
- 暗黙的に、これは
vector
のコピー代入演算子を呼び出すことと同じです。
- copy/swap イディオムを使用して、暗示的にマクロの下に
SLOW_DOWN
. という名前を付けることを考えSLEEP_FOR_AWHILE
ましたが、バッテリー駆動のデバイスを使用している場合、この方法は実際にはスリープステートメントよりもはるかに悪いです。
vector<int>
このテストでは、0 から 1000 の間のランダムなサイズの s をいくつか作成し、それらを 100 万回割り当てます。それぞれの時間を計測し、時間を合計してから、浮動小数点ナノ秒単位で平均時間を見つけて出力します。高解像度クロックを 2 回連続して呼び出しても 100 ナノ秒未満の値が返されない場合は、ベクトルの長さを大きくすることをお勧めします。
ここに私の結果があります:
$ clang++ -std=c++11 -stdlib=libc++ -O3 test.cpp
$ a.out
428.348ns
$ a.out
438.5ns
$ a.out
431.465ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DSLOW_DOWN test.cpp
$ a.out
617.045ns
$ a.out
616.964ns
$ a.out
618.808ns
この簡単なテストでは、コピー/スワップ イディオムで 43% のパフォーマンス ヒットが見られます。YMMV。
上記のテストでは、平均して、lhs の半分の時間で十分な容量があります。これを極端に考えると、次のようになります。
- lhs には常に十分な容量があります。
- lhs には常に十分な容量があります。
その場合、コピー/スワップ イディオムに対するデフォルトのコピー割り当てのパフォーマンス上の利点は、約 560% から 0% まで変化します。copy/swap イディオムは決して高速ではなく、劇的に遅くなる可能性があります (このテストの場合)。
スピードが欲しいですか?測定。