什么是复制和交换习语?

这个成语是什么,什么时候应该使用?它解决了哪些问题?当使用C ++ 11时,成语是否会改变? 虽然在许多地方已经提到过,但我们没有任何单一的“它是什么”问题和答案,所以在这里。以下是前面提到的地方的部分列表: 你最喜欢的C ++ Coding Style成语是什么:Copy-swap 在C ++中复制构造函数和=运算符重载:可能是一个常见的函数吗? 什么是复制省略以及如何优化复制和交换习惯用法 C ++:动态分配一个对象数组?     
已邀请:
分配的核心是两个步骤:拆除对象的旧状态,并将其新状态构建为其他对象状态的副本。 基本上,这就是析构函数和复制构造函数的作用,因此第一个想法是将工作委托给它们。然而,由于破坏必定不会失败,而建筑可能,我们实际上想要反过来做:首先执行建设性部分,如果成功,那么做破坏性部分。复制和交换习惯用法就是这样做的:它首先调用类的复制构造函数来创建临时文件,然后用临时文件交换数据,然后让临时的析构函数破坏旧状态。 由于
swap()
应该永远不会失败,唯一可能失败的部分是复制构造。首先执行此操作,如果失败,则目标对象中不会更改任何内容。 在其精炼形式中,通过初始化赋值运算符的(非引用)参数来执行复制来实现复制和交换:
T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}
    
已经有一些好的答案了。我将主要关注我认为他们缺乏的东西 - 用复制和交换习语解释“缺点”......   什么是复制和交换习语? 一种根据交换函数实现赋值运算符的方法:
X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}
基本思想是: 分配给对象最容易出错的部分是确保获取新状态所需的任何资源(例如,内存,描述符) 如果获得新值的副本,则可以在修改对象的当前状态(即
*this
)之前尝试获取,这就是为什么
rhs
被值接受(即复制)而不是通过引用 交换本地副本的状态
rhs
*this
通常相对容易做到没有潜在的失败/异常,因为本地副本之后不需要任何特定的状态(只需要状态适合析构函数运行,就像对象一样)被移出> = C ++ 11)   什么时候应该使用? (它解决了哪些问题[/ create]?) 如果您希望被分配的对象不受引发异常的赋值的影响,假设您已经或可以写一个具有强异常保证的
swap
,理想情况下一个不能失败/
throw
..† 当你想要一个干净,易于理解,健壮的方法来定义赋值运算符(简单)复制构造函数,
swap
和析构函数。 作为复制和交换完成的自我分配避免了被忽视的边缘情况。‡ 在分配期间通过拥有额外临时对象而产生的任何性能损失或暂时更高的资源使用对您的应用程序而言并不重要。 ⁂ †
swap
投掷:通常可以通过指针可靠地交换对象跟踪的数据成员,但是没有无抛出交换的非指针数据成员,或者交换必须实现为
X tmp = lhs; lhs = rhs; rhs = tmp;
和复制构造的非指针数据成员或任务可能抛出,仍然有可能失败,留下一些数据成员交换而其他人没有。这个潜力甚至适用于C ++ 03
std::string
's,詹姆斯评论另一个答案:   @wilhelmtell:在C ++ 03中,没有提到std :: string :: swap(由std :: swap调用)可能抛出的异常。在C ++ 0x中,std :: string :: swap是noexcept,不能抛出异常。 - 詹姆斯麦克尼利斯2010年12月22日15:24 ‡当从不同的对象分配时,分配运算符实现看起来很明智,很容易因自我分配而失败。虽然客户端代码甚至可能尝试自我分配似乎是不可想象的,但是在容器上的algo操作期间它可以相对容易地发生,其中
x = f(x);
代码where50ѭ(可能仅适用于某些
#ifdef
分支)宏ala
#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_;
,在新分配的堆区域指向
p_
,然后尝试读取其中未初始化的数据(未定义的行为),如果这没有做任何太奇怪的事情,
copy
尝试自我分配到每一个刚被破坏的'T'! ⁂由于使用额外的临时(当运算符的参数是复制构造时),复制和交换习惯用法会引入效率低下或限制:
struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};
在这里,手写的
Client::operator=
可能会检查
*this
是否已经连接到与
rhs
相同的服务器(如果有用,可能会发送“重置”代码),而复制和交换方法会调用可能是复制构造函数的复制构造函数。写入打开一个独特的套接字连接然后关闭原来的。这不仅意味着远程网络交互而不是简单的进程内变量复制,它可能会对套接字资源或连接的客户端或服务器限制产生影响。 (当然这个类有一个非常可怕的界面,但那是另一回事;-P)。     
这个答案更像是对上述答案的补充和略微修改。 在某些版本的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
对象,因此将其作为参数传递在技术上是多余的。     
在处理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
最初的状态。然而,有一个隐藏的问题:如果
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()
,则程序具有未定义的行为(参见[container.requirements.general / 8]。 结果是,只要容器开始支持有状态分配器,交换就变成了C ++ 11中的一个非常重要的操作。这是一个有点“高级用例”,但并非完全不可能,因为一旦您的类管理资源,移动优化通常只会变得有趣,而内存是最受欢迎的资源之一。     

要回复问题请先登录注册