该类型的拷贝赋值运算符可以回收的资源,并抄送交换是几乎从来没有实现拷贝赋值运算符的最佳途径。例如看std::vector
:
此类管理动态大小的缓冲区,并保持二者capacity
(该缓冲区可容纳最大长度),和一个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,换句话说,你想拷贝赋值运算符可能有时需要强异常安全保障,不交换方面实行分配。如果您提供以下其中一项,您的客户很容易达到相同的保证:
- A noexcept swap。
- 一个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
成vector
需要2份代替1,甚至当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
的拷贝赋值运算符两种方式:
- 隐式地,这相当于调用
vector
的复制赋值运算符。
- 随着复制/交换成语,暗示下的宏
SLOW_DOWN
。我想过命名它SLEEP_FOR_AWHILE
,但如果你使用电池供电的设备,这种方式实际上比睡眠声明差得多。
该测试构造了一些在0到1000之间的随机大小的vector<int>
,并为它们指定了一百万次。它计算每一个,总结时间,然后以浮点纳秒为单位查找平均时间并打印出来。如果连续两次调用高分辨率时钟不会返回小于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%的性能损失。因人而异。
上述测试平均在一半的时间内有足够的容量。如果我们采取这种方式:
- lhs在所有的时间都有足够的容量。
- lhs在任何时候都有足够的容量。
那么默认拷贝分配在复制/交换习惯用法上的性能优势从约560%变化到0%。复制/交换习惯从不会更快,并且可能会显着更慢(对于此测试)。
想要速度?测量。
当对象很重要移动时(例如,包含大量数据成员),按值表单可能更昂贵(两次移动而不是一次)。 – Angew
这可能部分是历史原因,因为'std :: vector'在C++ 03中已经有'T const&'重载了。可能有代码依赖于现有的超载(比如某人拿到了成员函数的地址)。还要注意,在标准中,不需要优化必须实现的代码行,因为它由编译器实现者编写一次,但几乎在其他地方使用。额外的开发成本可以忽略不计。 –
出于显而易见的原因,肯定需要的一种情况是复制/移动构造函数。 – celtschk