C++ 复制和交换总是最好的解决方案吗?
我认为中推荐的习惯用法是为赋值操作符实现强异常安全性的推荐/最佳/唯一方法。在我看来,这种方法也有缺点 考虑以下使用复制和交换的简化类向量:C++ 复制和交换总是最好的解决方案吗?,c++,C++,我认为中推荐的习惯用法是为赋值操作符实现强异常安全性的推荐/最佳/唯一方法。在我看来,这种方法也有缺点 考虑以下使用复制和交换的简化类向量: class IntVec { size_t size; int* vec; public: IntVec() : size(0), vec(0) {} IntVec(IntVec const& other) : size(other.size), vec(size? new int[siz
class IntVec {
size_t size;
int* vec;
public:
IntVec()
: size(0),
vec(0)
{}
IntVec(IntVec const& other)
: size(other.size),
vec(size? new int[size] : 0)
{
std::copy(other.vec, other.vec + size, vec);
}
void swap(IntVec& other) {
using std::swap;
swap(size, other.size);
swap(vec, other.vec);
}
IntVec& operator=(IntVec that) {
swap(that);
return *this;
}
//~IntVec() and other functions ...
}
通过复制构造函数实现分配可能是有效的,可以保证异常安全,但也可能导致不必要的分配,甚至可能导致不必要的内存不足错误
考虑将700MB
IntVec
分配给具有的机器上的1GBIntVec
的情况。实现重用空间存在两个问题
- 如果您正在分配
非常大的向量=非常小的向量代码>将不会释放额外内存。这可能是你想要的,也可能不是
- 对于整数,一切都可以,但是对于复制操作可能引发异常的对象呢?您将得到一个混乱的数组,其中部分拷贝已经完成,并且被截断。如果复制操作失败(交换习惯用法就是这样做的),最好保持目标不动
顺便说一句,一般来说,在很少的情况下,你可以找到任何看起来“总是最好的解决方案”的东西。如果您正在寻找解决方案,那么编程将不是正确的选择。是的,内存不足是一个潜在的问题 您已经知道复制和交换解决了什么问题。这就是如何“撤消”失败的分配
如果分配在流程的某个阶段失败,那么使用更有效的方法是无法返回的。而且,如果一个对象写得不好,一个失败的赋值甚至可能使该对象损坏,并且程序将在对象销毁期间崩溃。要解决您的特定问题,请修改“复制交换”以清除“复制交换” 这可以通过以下方式实现:
Foo& operator=( Foo const& o ) {
using std::swap;
if (this == &o) return *this; // efficient self assign does nothing
swap( *this, Foo{} ); // generic clear
Foo tmp = o; // copy to a temporary
swap( *this, tmp ); // swap temporary state into us
return *this;
}
Foo& operator=( Foo && o ) {
using std::swap;
if (this == &o) return *this; // efficient self assign does nothing
swap( *this, Foo{} ); // generic clear
Foo tmp = std::move(o); // move to a temporary
swap( *this, tmp ); // swap temporary state into us
return *this;
}
虽然这确实会导致大量分配发生,但它会在大量释放发生后立即发生
拷贝交换的关键部分是它可以实现一个正确的实现,而获得异常安全拷贝的正确性是一件棘手的事情
您将注意到,如果抛出异常,上述情况会导致lhs可能处于空状态。相比之下,标准副本交换将产生有效副本,或lhs保持不变
现在,我们可以做最后一个小把戏。假设我们的状态被捕获在子状态的向量中,并且这些子状态具有异常安全的交换
或移动
。然后我们可以:
Foo& move_substates( Foo && o ) {
if (this == &o) return *this; // efficient self assign does nothing
substates.resize( o.substates.size() );
for ( unsigned i = 0; i < substates.size(); ++i) {
substates[i] = std::move( o.substates[i] );
}
return *this;
}
现在,如果我们从一个源移动,我们会重用内部内存并避免分配,如果你害怕内存分配,我们不会比源大太多
这是复制和交换习惯用法的问题,对吗
视情况而定;如果你有这么大的向量,那么是的你是对的
我是否忽略了复制和交换版本解决的“更好”版本的一些问题
您正在为罕见的情况进行优化。为什么要执行额外的检查并给代码增加圈复杂度?(当然,除非应用程序中的向量太大)
< C++ > 11 C++以来,临时变量的r值可由C++获得。因此,通过const&
传递参数会丢弃优化
一句话:这个词总是很容易被反驳。如果有一个通用的解决方案,总是比任何替代方案都好,我想编译器可以采用它,并基于此隐式生成一个默认赋值运算符
在前面的说明中,隐式声明的复制赋值运算符具有
通过常量而不是值接受它的参数(如复制和交换习惯用法)您的方法有两个警告
考虑将1KB IntVec分配给1GB IntVec的情况。您将得到大量已分配但未使用(浪费)的内存
如果在复制过程中引发异常怎么办?如果要覆盖现有位置,则最终会得到已损坏、部分复制的数据
正如您所指出的,解决这些问题可能不是很节省内存,但软件设计始终是权衡的。您可以看到STL如何实现向量的operator=of
template <class _Tp, class _Alloc>
vector<_Tp,_Alloc>&
vector<_Tp,_Alloc>::operator=(const vector<_Tp, _Alloc>& __x)
{
if (&__x != this) {
const size_type __xlen = __x.size();
if (__xlen > capacity()) {
iterator __tmp = _M_allocate_and_copy(__xlen, __x.begin(), __x.end());
destroy(_M_start, _M_finish);
_M_deallocate(_M_start, _M_end_of_storage - _M_start);
_M_start = __tmp;
_M_end_of_storage = _M_start + __xlen;
}
else if (size() >= __xlen) {
iterator __i = copy(__x.begin(), __x.end(), begin());
destroy(__i, _M_finish);
}
else {
copy(__x.begin(), __x.begin() + size(), _M_start);
uninitialized_copy(__x.begin() + size(), __x.end(), _M_finish);
}
_M_finish = _M_start + __xlen;
}
return *this;
}
模板
向量&
向量::运算符=(常量向量和x)
{
如果(&x!=此){
const size_type_uuxlen=uuuux.size();
如果(\uxlen>capacity()){
迭代器\uuuTMP=\uM\uAllocate\u和\uCopy(\uuxlen,\uuux.begin(),\uuuuux.end());
销毁(开始、结束);
_M_解除分配(_M_开始,_M_结束\u存储-_M_开始);
_M_开始=u tmp;
_存储器的M_end_=_M_start+_xlen;
}
否则如果(大小()>=\uxLen){
迭代器i=copy(uuux.begin(),uuux.end(),begin());
销毁(i,M,finish);
}
否则{
复制(uuu x.begin(),uuu x.begin()+大小(),u M_start);
未初始化的拷贝(uuu x.begin()+size(),uuu x.end(),u M_finish);
}
_M_finish=_M_start+_xlen;
}
归还*这个;
}
就个人而言,我基本上不使用复制和交换,因为正如您所指出的,它实际上从来都不是最有效的技术。。。我们使用C++来提高效率,不要编写拷贝700兆字节向量的程序。至少不是700兆字节的向量,这些向量包含构造函数和析构函数,可以抛出异常。@Dave:Copy and swap比optimal多了一步,编译器可能会对其进行优化。唯一应该成为问题的时间是,如果您的移动是重量级的(如std::array
),也建议复制和交换,因为它只有3行代码,而且相对来说是复制粘贴。更少出错的机会。优化的代码需要更多的代码;掉期(a、b)代码>是
Foo& operator=( Foo && o ) {
using std::swap;
if (this == &o) return *this; // efficient self assign does nothing
if (substates.capacity() >= o.substates.size() && substates.capacity() <= o.substates.size()*2) {
return move_substates(std::move(o));
} else {
swap( *this, Foo{} ); // generic clear
Foo tmp = std::move(o); // move to a temporary
swap( *this, tmp ); // swap temporary state into us
return *this;
}
}
T& T::operator=(const T&);
template <class _Tp, class _Alloc>
vector<_Tp,_Alloc>&
vector<_Tp,_Alloc>::operator=(const vector<_Tp, _Alloc>& __x)
{
if (&__x != this) {
const size_type __xlen = __x.size();
if (__xlen > capacity()) {
iterator __tmp = _M_allocate_and_copy(__xlen, __x.begin(), __x.end());
destroy(_M_start, _M_finish);
_M_deallocate(_M_start, _M_end_of_storage - _M_start);
_M_start = __tmp;
_M_end_of_storage = _M_start + __xlen;
}
else if (size() >= __xlen) {
iterator __i = copy(__x.begin(), __x.end(), begin());
destroy(__i, _M_finish);
}
else {
copy(__x.begin(), __x.begin() + size(), _M_start);
uninitialized_copy(__x.begin() + size(), __x.end(), _M_finish);
}
_M_finish = _M_start + __xlen;
}
return *this;
}