标准是否规定副本必须相等



假设我有一个奇怪的字符串类型,它拥有或不拥有它的底层缓冲区:

class WeirdString {
private:
char* buffer;
size_t length;
size_t capacity;
bool owns;
public:
// Non-owning constructor
WeirdString(char* buffer, size_t length, size_t capacity)
: buffer(buffer), length(length), capacity(capacity), owns(false)
{ }
// Make an owning copy
WeirdString(WeirdString const& rhs)
: buffer(new char[rhs.capacity])
, length(rhs.length)
, capacity(rhs.capacity)
, owns(true)
{
memcpy(buffer, rhs.buffer, length);
}
~WeirdString() {
if (owns) delete [] buffer;
}
};

复制构造函数是否在某个地方违反了标准?考虑:

WeirdString get(); // this returns non-owning string
const auto s = WeirdString(get());

s是拥有的还是不拥有的,这取决于附加的复制构造函数是否被删除,在C++14和更早版本中,这是允许的,但是可选的(尽管在C++17中是有保证的)。Schrödinger的所有权模型表明,这种复制构造函数本身就是一种未定义的行为。

是吗?


一个更具说明性的例子可能是:

struct X {
int i;
X(int i)
: i(i)
{ }
X(X const& rhs)
: i(rhs.i + 1)
{ }        ~~~~
};
X getX();
const auto x = X(getX());

根据消除的副本,x.i可能比getX()中返回的多0、1或2。标准对此有说明吗?

关于新问题的代码

struct X {
int i;
X(int i)
: i(i)
{ }
X(X const& rhs)
: i(rhs.i + 1)
{ }        ~~~~
};
X getX();
const auto x = X(getX());

这里的复制构造函数不复制,所以你打破了编译器的假设。

使用C++17,我相信您可以保证在上面的示例中不会调用它。然而,我手头没有C++17的草稿。

对于C++14及更早版本,是否为getX的调用调用复制构造函数,以及是否为复制初始化调用复制构造函数取决于编译器。

C++14§12.8/31class.copy/31

当满足某些条件时,允许实现省略类的复制/移动构造对象,即使为复制/移动操作选择的构造函数和/或对象的析构函数有副作用。

从该术语的正式含义来看,这不是未定义的行为,它可以调用鼻恶魔。对于正式术语,我会选择未指定的行为,因为这是取决于实现的行为,不需要记录。但在我看来,选择什么名称并不重要:重要的是,标准只是说,在指定的条件下,编译器可以优化复制/移动构造,而不考虑优化的away构造函数的副作用–因此,你不能也不应该依赖

关于类X的问题部分添加在此答案之后。它的根本不同之处在于X复制构造函数不复制。因此,我单独回答了这个问题

关于原始问题的WeirdString:这是你的类,所以标准对它没有要求。

然而,该标准有效地让编译器假设是复制构造函数,而不是其他

令人高兴的是,这就是你的复制构造函数所做的,但如果(我知道这不适用于你,但如果)它主要有一些你所依赖的其他效果,那么复制省略规则可能会对你的期望造成严重破坏。

如果你想要一个有保证的拥有实例(例如,为了将其传递给线程),你可以简单地提供一个unshare成员函数,或者一个带有标记参数的构造函数,或者一种工厂函数。

您通常不能依赖于正在调用的复制构造函数。


为了避免出现问题,您最好处理所有可能的复制,这也意味着复制分配运算符operator=

否则,可能会有两个或多个实例都认为自己拥有缓冲区,并负责解除分配。

通过定义move-a构造函数和声明或定义move赋值运算符来支持move语义也是一个好主意。

通过使用std::unique_ptr<char[]>来保持缓冲区指针,可以更确定所有这些操作的正确性。

防止通过复制分配运算符进行无意复制的其他功能。

最新更新