以下是我能够从更大的代码库中提炼出来的一个片段,希望能说明目前我无法看到的某种内存损坏。这是在 Ubuntu 6.3.0 上使用 g++ 17.04,尽管我在 gcc 7.0.1 和 clang 4.0.0 上看到了同样的问题。
#include <array>
#include <assert.h>
using Msg = std::array<char,sizeof(std::string)*2> ;
class Str {
public:
explicit Str (std::string &&v) : v (std::move(v)) {}
std::string v;
};
void f(Msg &tmsg)
{
Msg m;
new (&m) Str ("hello");
tmsg = m;
}
int main( int , char* [] )
{
Msg tmsg;
f(tmsg);
auto ptr = (Str*) &tmsg;
assert(ptr->v == "hello"); // This fails
return 0;
}
当我尝试运行它时,我得到:
$ g++ main.cpp -g -std=c++11 && ./a.out
a.out: main.cpp:24: int main(int, char**): Assertion `ptr->v == "hello"' failed.
Aborted
有什么想法吗?我已经盯着这个看了几个小时了,我一直无法弄清楚。
根据C++标准,此代码是不合法的。存在多个问题:
-
对准。您不能确保
Str
的存储与std::string
相同的边界对齐,因此您的代码具有无需诊断的未定义行为。使用std::aligned_storage_t
比像您一样std::array
更简单。 -
您正在尝试通过复制基础字节来复制
std::string
。这是不合法的,标准也没有给你这样做的许可。它违反了 C++ 中非平凡类类型的基本生存期要求,在这种情况下违反了严格的别名规则。
在这个函数中,坏事正在发生
void f(Msg &tmsg)
{
Msg m;
new (&m) Str ("hello");
tmsg = m;
}
当tmsg = m
发生时。这是底层字节获取副本的时间,但这不是您安全复制对象的方式。如果它是非平凡的,如 std::string,并且拥有堆分配的缓冲区等资源,则需要调用复制构造函数,否则类无法强制执行其保证。(该行本身不会导致未定义的行为,但是当您尝试将 tmsg 字节重新解释为有效的 Str 时,这就是 UB。
另请注意,由于您使用了放置 new,并且从未在任何地方调用 dtor,因此您正在泄漏您新放置的对象。存储它的缓冲区是否位于堆栈上并不重要,缓冲区没有责任调用 dtor,您可以这样做。
此外,允许优化器假设您不会尝试复制这样的重要对象。优化程序可能假定tmsg
不包含有效的Str
对象,因为从不在那里调用Str
对象构造函数。
您可以将此代码更改为
void f(Msg &tmsg)
{
new (&tmsg) Str ("hello");
}
并修复了对齐问题,然后我认为它具有明确定义的行为,至少我没有看到其他问题(泄漏除外)。
可以在存储缓冲区中分配对象,但必须非常小心。我建议您注意旧的ISO C++常见问题解答的建议:
https://isocpp.org/wiki/faq/dtors#placement-new
建议:除非必须,否则不要使用这种"放置新"语法。仅当您真正关心将对象放置在内存中的特定位置时才使用它。
。 (如果您不知道"对齐"是什么意思,请不要使用放置新语法)。你已经被警告了。
编辑:根据上面的评论:
真正的代码试图将或多或少的任意类型打包到事件队列中。此队列的使用者恢复类型并在完成后进行清理。
我建议你做的是使用variant
,比如boost::variant
或std::variant
。这是一个类型安全的联合,它将管理缓冲区内新放置的细节,安全地复制和移动内容,调用dtor等。你可以有一个std::vector<variant<....>>
或类似的队列,这样你就不会有这种类型的低级问题。
了解问题是什么的另一种方法:如果f
像这样更改,并且对齐问题已修复,则可以执行以下操作:
void f(Msg &tmsg)
{
Msg m;
new (&m) Str ("hello");
new (&tmsg) Str(*reinterpret_cast<Str*>(&m));
}
由于您使用放置新语法调用复制 ctor,因此新Str
在缓冲区tmsg
中正确开始其生存期,并在m
中创建副本。