标准是否表明副本必须等效?

时间:2017-01-23 22:47:13

标签: c++ language-lawyer copy-constructor copy-elision

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

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。标准是否对此有所说明?

2 个答案:

答案 0 :(得分:5)

关于新问题的代码

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 ++草案。

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

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

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

这不是未定义的行为,在该术语的正式含义意义上,它可以调用鼻子恶魔。对于正式术语,我选择未指定的行为,因为这种行为取决于实现而不需要记录。但正如我所知,人们选择的名称并不重要:重要的是标准只是说在规定条件下编译器可以优化复制/移动构造,而不管优化离开构造函数的副作用 - 因此,你不能也不应该依赖它。

答案 1 :(得分:4)

在此答案之后添加了有关课程X的问题部分。它的根本不同之处在于X复制构造函数不会复制。因此,我回答了separately

关于原始问题WeirdString:这是您的课程,因此标准对此没有任何要求。

然而,该标准有效地让编译器假设复制构造函数复制,而没有别的

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

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

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

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

否则,您冒着两个或更多实例都认为他们拥有缓冲区的风险,并负责解除分配。

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

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

防止通过复制赋值运算符进行无意复制的其他事项。