为什么以下程序打印出垃圾而不是hello
?有趣的是,如果我替换hello
为hello 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();
}
从你的问题,你意味着类不变的Buffer
。甲类不变是被假定为一个类的数据成员之间的关系始终是真的。在您的情况下,隐式不变是:
assert(_raw == _buffer.data());
乔阿希姆·皮勒博格(Joachim Pileborg)正确地描述了为什么在您的Buffer(std::string s)
构造函数中未维护此不变式(已赞成)。
事实证明,保持此不变性是令人惊讶的棘手。因此,我的第一个建议是重新设计Buffer
,以便不再需要此不变式。最简单的方法是_raw
在需要时随时进行计算,而不是存储它。例如:
void Print()
{
std::cout << _buffer.data();
}
话虽如此,如果您真的需要存储_raw
和维护此不变式:
assert(_raw == _buffer.data());
以下是您需要走的路...
Buffer(std::string s)
: _buffer(std::move(s))
, _raw(const_cast<char*>(_buffer.data()))
{
}
重新排序初始化,以便首先_buffer
通过移入进行构造,然后指向_buffer
。不要指向s
该构造函数完成后将被破坏的本地对象。
这里的一个非常微妙的一点是,尽管事实上我已经对构造函数中的初始化列表进行了重新排序,但实际上尚未真正对实际构造进行重新排序。为此,我必须对数据成员声明列表重新排序:
private:
std::string _buffer;
char* _raw;
顺序是由此顺序决定的,而不是构造函数中初始化列表的顺序决定了首先构造哪个成员。如果尝试将构造函数初始化列表的顺序与成员实际构造的顺序不同,则某些启用了警告的编译器会警告您。
现在,对于任何字符串输入,您的程序都将按预期运行。但是,我们才刚刚开始。Buffer
仍然是越野车,因为您的不变式仍然没有得到维护。证明这一点的最佳方法是在中声明您的不变式~Buffer()
:
~Buffer()
{
assert(_raw == _buffer.data());
}
就目前而言(并且没有~Buffer()
我刚刚建议的用户声明),编译器将为您提供另外四个签名:
Buffer(const Buffer&) = default;
Buffer& operator=(const Buffer&) = default;
Buffer(Buffer&&) = default;
Buffer& operator=(Buffer&&) = default;
并且编译器会为这些签名中的每一个破坏您的不变式。如果~Buffer()
按照我的建议进行添加,则编译器将不会提供move成员,但仍会提供copy成员,并且仍然会出错(尽管该行为已被弃用)。即使析构函数确实禁止了复制成员(就像将来的标准一样),代码仍然很危险,因为在维护过程中,有人可能会像这样“优化”您的代码:
#ifndef NDEBUG
~Buffer()
{
assert(_raw == _buffer.data());
}
#endif
在这种情况下,编译器将提供错误的副本并以发布模式移动成员。
要修复该代码,您必须在每次构造时重新建立您的类不变式_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;
}
如果将来添加任何可能使之失效的成员,则_buffer.data()
必须记住要重置_raw
。例如,set_string(std::string)
成员函数将需要这种处理。
尽管您没有直接提出疑问,但您的问题却暗示了类设计中的一个非常重要的点:要知道您的类不变式以及如何维护它们。推论:尽量减少必须手动维护的不变数。并测试您的不变式实际上是否得到维护。
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句