这个成语是什么?何时应该使用?它解决了哪些问题?当使用C ++ 11时,习惯用法会改变吗?
虽然在许多地方已经提到过,但我们没有任何单一的“它是什么”问题和答案,所以在这里。以下是前面提到的地方的部分列表:
答案 0 :(得分:2002)
答案 1 :(得分:246)
分配的核心是两个步骤: 拆除对象的旧状态 和 将其新状态构建为副本 某个其他对象的状态。
基本上,这就是 析构函数 和 复制构造函数 所做的,所以第一个想法是将工作委托给他们。但是,由于破坏必定不会失败,而构建可能,我们实际上想要反过来做: 首先执行建设性部分 如果成功, 然后执行破坏性部分 。复制和交换习惯是这样做的一种方式:它首先调用类的复制构造函数来创建一个临时的,然后用临时的交换它的数据,然后让临时的析构函数破坏旧状态。
由于swap()
应该永远不会失败,唯一可能失败的部分是复制构造。首先执行此操作,如果失败,则目标对象中不会更改任何内容。
在其精炼形式中,通过初始化赋值运算符的(非引用)参数来执行复制来实现复制和交换:
T& operator=(T tmp)
{
this->swap(tmp);
return *this;
}
答案 2 :(得分:36)
已经有一些好的答案。我将主要关注我认为他们缺乏的东西 - 用复制和交换习语解释“缺点”......
什么是复制和交换习语?
根据交换函数实现赋值运算符的方法:
X& operator=(X rhs)
{
swap(rhs);
return *this;
}
基本理念是:
分配给对象最容易出错的部分是确保获取新状态所需的任何资源(例如内存,描述符)
在修改对象的当前状态(即*this
)之前,可以尝试获取,如果有新值的副本,这就是为什么{{1接受按值(即复制)而不是按引用
交换本地副本rhs
和rhs
的状态通常相对容易做到没有潜在的失败/异常,因为本地副本没有之后需要任何特定的状态(只需要状态适合析构函数运行,就像在> = C ++ 11中移动的对象一样)
什么时候应该使用? (它解决哪些问题 [/ create] ?)
如果您希望被分配的对象不受引发异常的赋值的影响,假设您已经或可以编写具有强异常保证的*this
,并且理想情况下不会失败/ { {1}} ..†
当你需要一个干净,易于理解,强大的方法来根据(更简单的)复制构造函数,swap
和析构函数来定义赋值运算符。
†throw
抛出:通常可以通过指针可靠地交换对象跟踪的数据成员,但是没有无抛出交换的非指针数据成员,或者必须进行交换的非指针数据成员实现为swap
并且复制构造或赋值可能抛出,仍然有可能失败,一些数据成员交换而另一些则没有。这种潜力甚至适用于C ++ 03 swap
,因为James对另一个答案的评论是:
@wilhelmtell:在C ++ 03中,没有提到std :: string :: swap(由std :: swap调用)可能抛出的异常。在C ++ 0x中,std :: string :: swap是noexcept,不能抛出异常。 - 詹姆斯麦克尼利斯2010年12月22日15:24
‡赋值运算符实现在从不同对象进行分配时看起来很明显,很容易因自我赋值而失败。虽然客户端代码甚至可能尝试自我分配似乎是不可想象的,但是在容器上的算法操作期间它可以相对容易地发生,其中X tmp = lhs; lhs = rhs; rhs = tmp;
代码std::string
(可能仅针对某些x = f(x);
分支)宏ala f
或返回对#ifdef
的引用的函数,或甚至(可能是低效但简洁的)代码,如#define f(x) x
)。例如:
x
在自我赋值时,上面的代码删除x = c1 ? x * 2 : c2 ? x / 2 : x;
,在新分配的堆区域指向struct X
{
T* p_;
size_t size_;
X& operator=(const X& rhs)
{
delete[] p_; // OUCH!
p_ = new T[size_ = rhs.size_];
std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
}
...
};
,然后尝试读取其中的未初始化数据(未定义行为)如果这不会做任何太奇怪的事情,x.p_;
会尝试自我分配给每一个刚被破坏的'T'!
copy由于使用额外的临时(当运算符的参数是复制构造时),复制和交换习惯用法会引入效率低下或限制:
p_
在这里,手写copy
可能会检查struct Client
{
IP_Address ip_address_;
int socket_;
X(const X& rhs)
: ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
{ }
};
是否已连接到与Client::operator=
相同的服务器(如果有用,可能会发送“重置”代码),而副本-and-swap方法将调用copy-constructor,它可能被写入以打开一个不同的套接字连接然后关闭原始套接字连接。这不仅意味着远程网络交互而不是简单的进程内变量复制,它可能会对套接字资源或连接的客户端或服务器限制产生影响。 (当然这个类有一个非常可怕的界面,但这是另一回事;-P)。
答案 3 :(得分:22)
这个答案更像是对上述答案的补充和略微修改。
在某些版本的Visual Studio(以及可能的其他编译器)中,存在一个非常烦人且无意义的错误。因此,如果您声明/定义swap
函数,请执行以下操作:
friend void swap(A& first, A& second) {
std::swap(first.size, second.size);
std::swap(first.arr, second.arr);
}
...当你调用swap
函数时,编译器会对你大喊:
这与被调用的friend
函数和this
对象作为参数传递有关。
解决此问题的方法是不使用friend
关键字并重新定义swap
函数:
void swap(A& other) {
std::swap(size, other.size);
std::swap(arr, other.arr);
}
这一次,你可以调用swap
并传入other
,从而使编译器满意:
毕竟,你不需要使用friend
函数来交换2个对象。将swap
成为具有一个other
对象作为参数的成员函数同样有意义。
您已经可以访问this
对象,因此将其作为参数传递在技术上是多余的。
答案 4 :(得分:13)
在处理C ++ 11风格的分配器感知容器时,我想添加警告。交换和赋值具有微妙的不同语义。
为了具体,让我们考虑一个容器std::vector<T, A>
,其中A
是一些有状态的分配器类型,我们将比较以下函数:
void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{
a.swap(b);
b.clear(); // not important what you do with b
}
void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
a = std::move(b);
}
fs
和fm
这两项功能的目的是为a
提供b
最初的状态。但是,有一个隐藏的问题:a.get_allocator() != b.get_allocator()
会发生什么?答案是:这取决于。我们来写AT = std::allocator_traits<A>
。
如果AT::propagate_on_container_move_assignment
为std::true_type
,则fm
会将a
的分配器重新分配给b.get_allocator()
的值,否则会重新分配a
, a
继续使用其原始分配器。在这种情况下,数据元素需要单独交换,因为b
和AT::propagate_on_container_swap
的存储不兼容。
如果std::true_type
为fs
,则AT::propagate_on_container_swap
会以预期的方式交换数据和分配器。
如果std::false_type
为a.get_allocator() == b.get_allocator()
,那么我们需要进行动态检查。
a.get_allocator() != b.get_allocator()
,则两个容器使用兼容的存储空间,并以通常的方式进行交换。结果是,只要容器启动支持有状态分配器,交换就变成了C ++ 11中的一个非常重要的操作。这有点&#34;高级用例&#34;但它并非完全不可能,因为一旦您的班级管理资源,移动优化通常只会变得有趣,而内存是最受欢迎的资源之一