使用 moved std::string 的 .data() 成员不适用于小字符串?
using .data() member of moved std::string doesn't work for small strings?
为什么下面的程序打印垃圾而不是 hello
?有趣的是,如果我用 hello how are you
替换 hello
,那么它会打印 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 b("hello");
这是创建临时字符串以传递给构造函数。当该字符串在构造函数末尾超出范围时,您将留下悬空 _raw
.
这意味着调用 Print 时未定义的行为 _raw
指向已释放的内存。
构造函数按值获取其参数,当构造函数 returns 该参数超出范围并且对象 s
被析构时。
但是你保存了一个指向该对象数据的指针,一旦对象被破坏,该指针就不再有效,留下一个流浪指针和undefined behavior 当你取消引用指针时。
根据您的问题,您暗示了 Buffer
的 class 不变量。 class 不变式 是 class 的数据成员之间的关系,假设 总是 为真。在您的情况下,隐含的不变量是:
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;
这是this顺序,而不是构造函数中初始化列表的顺序决定了先构造哪个成员。如果您尝试对构造函数初始化列表的排序与实际构造成员的顺序不同,一些启用了警告的编译器会警告您。
现在,对于任何字符串输入,您的程序都将按预期运行。然而,我们才刚刚开始。 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()
,编译器将不会提供移动成员,但它仍会提供复制成员,并且仍然会弄错它们(尽管这种行为已被弃用)。即使析构函数确实禁止复制成员(未来的标准中可能如此),代码仍然很危险,因为有人正在维护,可能 "optimize" 你的代码是这样的:
#ifndef NDEBUG
~Buffer()
{
assert(_raw == _buffer.data());
}
#endif
在这种情况下,编译器将在发布模式下提供错误的复制和移动成员。
要修复代码,您必须在每次构造 _buffer
时重新建立 class 不变量 ,否则指向它的未完成指针可能会失效。例如:
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)
成员函数需要这种处理。
虽然您没有直接提问,但您的问题暗示了 class 设计中非常重要的一点:注意您的 class 不变量,以及如何维护它们。推论:尽量减少必须手动维护的不变量的数量。并测试您的不变量实际上是否得到维护。
为什么下面的程序打印垃圾而不是 hello
?有趣的是,如果我用 hello how are you
替换 hello
,那么它会打印 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 b("hello");
这是创建临时字符串以传递给构造函数。当该字符串在构造函数末尾超出范围时,您将留下悬空 _raw
.
这意味着调用 Print 时未定义的行为 _raw
指向已释放的内存。
构造函数按值获取其参数,当构造函数 returns 该参数超出范围并且对象 s
被析构时。
但是你保存了一个指向该对象数据的指针,一旦对象被破坏,该指针就不再有效,留下一个流浪指针和undefined behavior 当你取消引用指针时。
根据您的问题,您暗示了 Buffer
的 class 不变量。 class 不变式 是 class 的数据成员之间的关系,假设 总是 为真。在您的情况下,隐含的不变量是:
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;
这是this顺序,而不是构造函数中初始化列表的顺序决定了先构造哪个成员。如果您尝试对构造函数初始化列表的排序与实际构造成员的顺序不同,一些启用了警告的编译器会警告您。
现在,对于任何字符串输入,您的程序都将按预期运行。然而,我们才刚刚开始。 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()
,编译器将不会提供移动成员,但它仍会提供复制成员,并且仍然会弄错它们(尽管这种行为已被弃用)。即使析构函数确实禁止复制成员(未来的标准中可能如此),代码仍然很危险,因为有人正在维护,可能 "optimize" 你的代码是这样的:
#ifndef NDEBUG
~Buffer()
{
assert(_raw == _buffer.data());
}
#endif
在这种情况下,编译器将在发布模式下提供错误的复制和移动成员。
要修复代码,您必须在每次构造 _buffer
时重新建立 class 不变量 ,否则指向它的未完成指针可能会失效。例如:
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)
成员函数需要这种处理。
虽然您没有直接提问,但您的问题暗示了 class 设计中非常重要的一点:注意您的 class 不变量,以及如何维护它们。推论:尽量减少必须手动维护的不变量的数量。并测试您的不变量实际上是否得到维护。