标准是否规定副本必须相同?

Does the standard state that copies must be equivalent?

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

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 中是保证的).薛定谔的所有权模型表明这个复制构造函数本身就是未定义的行为。

是吗?


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

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。标准对此有任何说明吗?

关于classX的问题部分是在这个回答之后添加的。它的根本不同在于 X 复制构造函数不复制。因此我回答说 .

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

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

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

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

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


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

否则,您可能会冒两个或多个实例都认为它们拥有缓冲区并负责释放的风险。

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

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

除其他外,防止通过复制赋值运算符进行无意复制。

关于新题的代码

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/31 class.copy/31

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects.

这不是未定义的行为,就该术语的正式含义而言,它可以调用鼻恶魔。对于正式术语,我会选择 未指定的行为,因为该行为取决于实现,不需要记录。但正如我所见,选择什么名称并不重要:重要的是标准只是说在指定条件下编译器可以优化 copy/move 结构,而不管 [=35 的副作用=] 构造函数——因此你不能也不应该依赖它。