9

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

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

4

2 回答 2

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++17 的草稿。

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

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

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

从该术语的正式含义来看,这不是未定义的行为,它可以引起鼻恶魔。对于正式术语,我会选择unspecified behavior,因为这种行为取决于实现并且不需要记录在案。但正如我所见,一个人选择什么名字并不重要:重要的是标准只是说在指定条件下编译器可以优化复制/移动构造,而不管优化后构造函数的副作用 -因此,您不能也不应该依赖它。

于 2017-01-23T23:37:51.220 回答
4

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

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

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

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

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

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


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

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

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

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

除其他外,可防止通过复制分配运算符进行无意复制。

于 2017-01-23T22:51:28.340 回答