相关内容:
- [你最喜欢的C++编码风格惯用语法:Copy-swap](link1) - [C++中的拷贝构造函数和=运算符重载:是否可能有一个通用函数?](link2) - [什么是拷贝省略,它如何优化copy-and-swap惯用语法?](link3) - [C++:动态分配对象数组?](link4)
任何管理资源(像智能指针这样的wrapper)的类都需要实现三大函数。虽然复制构造函数和析构函数的目标和实现很简单,但是复制赋值运算符可能是最微妙和最困难的。应该如何执行它?需要避免哪些陷阱?
复制并交换惯用语就是解决方案,并优雅地帮助赋值运算符实现两个目标:避免代码重复和提供强异常保证。
从概念上讲,它通过使用复制构造函数的功能创建数据的本地副本,然后使用swap
函数获取已复制的数据,将旧数据与新数据交换。然后临时副本被销毁,带走了旧数据。我们留下了新数据的副本。
为了使用复制并交换惯用语,我们需要三件事:一个可以正常工作的复制构造函数,一个可以正常工作的析构函数(两者都是任何包装器的基础,因此应该完整),以及一个swap
函数。
让我们考虑一个具体的案例。我们想要在一个无用的类中管理动态数组。我们从一个可用的构造函数、拷贝构造函数和析构函数开始:
#include <algorithm> // std::copy
#include <cstddef> // std::size_t
class dumb_array
{
public:
// (default) constructor
dumb_array(std::size_t size = 0)
: mSize(size),
mArray(mSize ? new int[mSize]() : nullptr)
{
}
// copy-constructor
dumb_array(const dumb_array& other)
: mSize(other.mSize),
mArray(mSize ? new int[mSize] : nullptr)
{
// note that this is non-throwing, because of the data
// types being used; more attention to detail with regards
// to exceptions must be given in a more general case, however
std::copy(other.mArray, other.mArray + mSize, mArray);
}
// destructor
~dumb_array()
{
delete [] mArray;
}
private:
std::size_t mSize;
int* mArray;
};
这个类几乎成功地管理了数组,但需要 operator=
来正确工作。
以下是一个天真实现的样子:
// the hard part
dumb_array& operator=(const dumb_array& other)
{
if (this != &other) // (1)
{
// get rid of the old data...
delete [] mArray; // (2)
mArray = nullptr; // (2) *(see footnote for rationale)
// ...and put in the new
mSize = other.mSize; // (3)
mArray = mSize ? new int[mSize] : nullptr; // (3)
std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
}
return *this;
}
我们说我们完成了;现在它可以管理一个数组,没有泄漏。然而,它有三个问题,按顺序在代码中标记为(n)
。
The first is the self-assignment test.
This check serves two purposes: it's an easy way to prevent us from running needless code on self-assignment, and it protects us from subtle bugs (such as deleting the array only to try and copy it). But in all other cases it merely serves to slow the program down, and act as noise in the code; self-assignment rarely occurs, so most of the time this check is a waste.
It would be better if the operator could work properly without it.
The second is that it only provides a basic exception guarantee. If new int[mSize]
fails, *this
will have been modified. (Namely, the size is wrong and the data is gone!)
For a strong exception guarantee, it would need to be something akin to:
dumb_array& operator=(const dumb_array& other)
{
if (this != &other) // (1)
{
// get the new data ready before we replace the old
std::size_t newSize = other.mSize;
int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
// replace the old data (all are non-throwing)
delete [] mArray;
mSize = newSize;
mArray = newArray;
}
return *this;
}
The code has expanded! Which leads us to the third problem: code duplication.
class dumb_array
{
public:
// ...
friend void swap(dumb_array& first, dumb_array& second) // nothrow
{
// enable ADL (not necessary in our case, but good practice)
using std::swap;
// by swapping the members of two objects,
// the two objects are effectively swapped
swap(first.mSize, second.mSize);
swap(first.mArray, second.mArray);
}
// ...
};
(这里是关于public friend swap
的解释。)现在我们不仅可以交换dumb_array
,而且一般情况下交换可以更有效率; 它只是交换指针和大小,而不是分配和复制整个数组。除了这个额外的功能和效率之外,我们现在已经准备好实现拷贝并交换惯用法。
话不多说,我们的赋值运算符是:
dumb_array& operator=(dumb_array other) // (1)
{
swap(*this, other); // (2)
return *this;
}
就是这样!一举三得,所有问题都被优雅地解决了。
我们首先注意到一个重要的选择:参数参数是通过值传递的。虽然可以轻松地执行以下操作(实际上,许多天真的惯用语实现都会这样做):
dumb_array& operator=(const dumb_array& other)
{
dumb_array temp(other);
swap(*this, temp);
return *this;
}
*this
的状态。(我们以前为了获得强异常保证所做的事情,现在编译器正在为我们完成;多么善良。)swap
是不抛出异常的。我们将当前数据与复制的数据交换,旧数据被放入临时变量中。当函数返回时,旧数据被释放。(然后参数的作用域结束,调用其析构函数。)operator=
。(此外,我们也不再对非自我分配产生性能损失。)class dumb_array
{
public:
// ...
// move constructor
dumb_array(dumb_array&& other) noexcept ††
: dumb_array() // initialize via default constructor, C++11 only
{
swap(*this, other);
}
// ...
};
dumb_array& operator=(dumb_array other); // (1)
other
正在使用rvalue进行初始化,它将会被move-constructed(移动构造)。太好了。与C++03以通过按值传递参数来重复使用我们的copy-constructor功能方式一样,C++11也会在适当的时候自动选择move-constructor。(当然,正如先前链接的文章中所提到的,值的复制/移动也可能完全不被执行。)*为什么要将mArray
设置为null?因为如果操作符中的任何进一步代码抛出异常,则可能会调用dumb_array
的析构函数;如果这种情况发生而没有将其设置为null,我们将尝试删除已经删除的内存!通过将其设置为null,我们避免了这种情况,因为删除null是无操作。
†还有其他说法认为我们应该专门针对我们的类型进行std::swap
的特化,提供一个类内的swap
和一个自由函数swap
等等。但这些都是不必要的:任何正确使用swap
的方式都将通过未限定的调用进行,而我们的函数将通过ADL被找到。只需要一个函数。
‡原因很简单:一旦你拥有了资源,你可以在参数列表中复制它,然后将其交换和/或移动(C++11)到需要的任何地方。通过在参数列表中进行复制,最大化优化。
††移动构造函数通常应该是noexcept
,否则某些代码(例如std::vector
调整大小逻辑)即使在移动有意义时也将使用复制构造函数。当然,只有在内部的代码不会抛出异常时才将其标记为noexcept。
boost::swap
和其他各种交换实例),那么需要在ADL期间找到您的swap
函数。在C++中,交换是一个棘手的问题,通常我们都认为单个访问点最好(以确保一致性),而通常情况下实现这一点的唯一方法是使用自由函数(例如,int无法拥有交换成员)。请参阅我的问题了解一些背景信息。 - GManNickG基本上,这就是析构函数和复制构造函数所做的事情,因此第一个想法是将工作委托给它们。但由于销毁不能失败,而构造可能会失败,我们实际上希望反过来做:首先执行构造部分,如果成功,然后执行销毁部分。复制并交换惯用语就是这样做的一种方式:它首先调用类的复制构造函数创建一个临时对象,然后将其数据与临时对象的数据进行交换,最后让临时对象的析构函数销毁旧状态。
由于swap()
不应该失败,唯一可能失败的部分就是复制构造。这是首先执行的,如果失败,则目标对象中的内容不会更改。
在其精炼形式中,复制并交换是通过初始化赋值运算符的(非引用)参数来执行复制的:
T& operator=(T tmp)
{
this->swap(tmp);
return *this;
}
std::swap(this_string, that)
不能提供无抛出保证,它提供了强异常安全性,但并不保证不抛出异常。 - wilhelmtellstd::swap
调用的std::string::swap
可能抛出异常。在C++0x中,std::string::swap
被声明为noexcept
且不能抛出异常。 - James McNellisstd::array
...) - sbi已经有一些好的回答了,我主要关注它们缺少的内容- "cons"与复制并交换惯用语的解释...
什么是复制并交换惯用语?
这是一种通过交换函数实现赋值运算符的方法:
X& operator=(X rhs)
{
swap(rhs);
return *this;
}
如果从一个不同的对象赋值时,看起来合理的赋值运算符实现在自我赋值时很容易失败。虽然客户端代码甚至尝试自我赋值似乎难以想象,但在容器上的算法操作期间,使用x = f(x);
代码可能相对容易发生,在这里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_;
,将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)。Client
的复制赋值运算符的主要问题是它没有被禁止赋值。 - sbiswap
函数: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
函数来交换两个对象。把swap
变成一个成员函数并将另一个对象作为参数也是同样有道理的。
由于你已经可以访问到this
对象,所以把它作为参数传递在技术上是多余的。
friend
函数中冗余的论点。为什么这不是默认的实现方法?这只是C++哲学的问题还是friend
成为最常见的方法只是偶然?除了类本身之外,其他人调用swap
的情况常见吗? - villasv当与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()
,程序具有未定义行为(参见 [container.requirements.general/8])。