使用移动的std :: string的.data()成员对小字符串不起作用?

tcb*_*tcb 4 c++ c++11 visual-studio-2013

为什么以下程序打印垃圾而不是hello?有趣的是,如果我替换hellohello how are you,那么它打印hello how are you.

#include <string>
#include <iostream>

class Buffer
{
public:
    Buffer(std::string s):
        _raw(const_cast<char*>(s.data())),
        _buffer(std::move(s))
    {    
    }

    void Print()
    {
        std::cout << _raw;
    }

private:
    char* _raw;
    std::string _buffer;
};    

int main()
{   
    Buffer b("hello");
    b.Print();
}
Run Code Online (Sandbox Code Playgroud)

How*_*ant 11

从你的问题,你意味着类不变Buffer.甲类不变是被假定为一个类的数据成员之间的关系始终是真的.在您的情况下,隐含的不变量是:

assert(_raw == _buffer.data());
Run Code Online (Sandbox Code Playgroud)

Joachim Pileborg正确地描述了为什么在Buffer(std::string s)构造函数中没有维护这个不变量(upvoted).

事实证明,保持这种不变性是非常棘手的.因此,我的第一个建议是你重新设计Buffer,以便不再需要这个不变量.最简单的方法是_raw在需要时动态计算,而不是存储它.例如:

void Print()
{
    std::cout << _buffer.data();
}
Run Code Online (Sandbox Code Playgroud)

话虽这么说,如果你真的需要存储_raw 维护这个不变量:

assert(_raw == _buffer.data());
Run Code Online (Sandbox Code Playgroud)

以下是您需要走的路径......

Buffer(std::string s)
    : _buffer(std::move(s))
    , _raw(const_cast<char*>(_buffer.data()))
{
}
Run Code Online (Sandbox Code Playgroud)

重新排序初始化,以便首先_buffer通过移入它来构造,然后指向_buffer.在s构造函数完成后,不要指向将被破坏的本地.

这里一个非常微妙的观点是,尽管我已经在构造函数中重新排序了初始化列表,但我还没有真正重新排序实际构造.为此,我必须重新排序数据成员声明列表:

private:
    std::string _buffer;
    char* _raw;
Run Code Online (Sandbox Code Playgroud)

它是这个顺序,而不是构造函数中初始化列表的顺序,它确定首先构造哪个成员.某些启用了某些警告的编译器会警告您,如果您尝试按照实际构造成员的顺序对构造函数初始化列表进行排序.

现在,对于任何字符串输入,您的程序将按预期运行.但是我们刚刚开始. Buffer因为你的不变量仍然没有被维护,所以仍然是错误的.证明这一点的最好方法是断言你的不变量~Buffer():

~Buffer()
{
    assert(_raw == _buffer.data());
}
Run Code Online (Sandbox Code Playgroud)

就目前而言(并且没有用户声明~Buffer()我刚推荐),编译器可以为您提供另外四个签名:

Buffer(const Buffer&) = default;
Buffer& operator=(const Buffer&) = default;
Buffer(Buffer&&) = default;
Buffer& operator=(Buffer&&) = default;
Run Code Online (Sandbox Code Playgroud)

编译器会破坏每个签名的不变量.如果你~Buffer()按照我的建议添加,编译器将不会提供移动成员,但它仍然会提供复制成员,但仍然会错误(尽管这种行为已被弃用).即使析构函数确实禁止了复制成员(因为它可能在未来的标准中),代码仍然很危险,因为维护人员可能会"优化"您的代码,如下所示:

#ifndef NDEBUG
    ~Buffer()
    {
        assert(_raw == _buffer.data());
    }
#endif
Run Code Online (Sandbox Code Playgroud)

在这种情况下,编译器将提供错误副本并在发布模式下移动成员.

要修复代码,每次构造时都必须重新建立类不变量_buffer,否则可能会使指向它的未完成指针无效.例如:

Buffer(const Buffer& b)
    : _buffer(b._buffer)
    , _raw(const_cast<char*>(_buffer.data()))
{
}

Buffer& operator=(const Buffer& b)
{
    if (this != &b)
    {
        _buffer = b._buffer;
        _raw = const_cast<char*>(_buffer.data());
    }
    return *this;
}
Run Code Online (Sandbox Code Playgroud)

如果您将来添加任何可能无效的成员_buffer.data(),您必须记住重置_raw.例如,set_string(std::string)成员函数需要这种处理.

虽然你没有直接提问,但你的问题提到了课堂设计中非常重要的一点:要注意你的班级不变量,以及维护它们需要做些什么.推论:最小化您必须手动维护的不变量的数量.并测试实际上是否维护了不变量.


Som*_*ude 7

构造函数通过值获取其参数,并且当构造函数返回时,该参数超出范围并且对象s被销毁.

但是你保存一个指向该对象数据的指针,一旦该对象被破坏,指针不再有效,当你取消引用指针时,你会留下一个迷路指针和未定义的行为.

  • 当你使用`std :: vector`时会发生同样的事吗?我认为对于该类,行为是明确定义的(对于`std :: allocator`) - 引用和指向原始数据的指针在移动后仍然有效.所以答案必须与`std :: string`恕我直言. (2认同)
  • @JoachimPileborg对于`std :: string`没有这样的保证.它的复制和移动ctor被指定为好像是*副本*的字符.重用存储只是一种优化,QoI. (2认同)