以下是我能够从更大的代码库中提取的片段,希望能够说明目前我无法看到的某种内存损坏。这是在Ubuntu 17.04上使用g ++ 6.3.0,虽然我在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
有什么想法吗?我现在已经盯着这几个小时了,我一直无法弄明白。
答案 0 :(得分:5)
根据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 ++ FAQ的建议:
https://isocpp.org/wiki/faq/dtors#placement-new
建议:除非必须,否则请勿使用此“放置新”语法。仅在您真正关心对象放置在内存中的特定位置时才使用它。
... (如果您不知道“对齐”的含义,请不要使用放置新语法)。你被警告了。
编辑:根据上述评论:
真正的代码试图将或多或少的任意类型打包到事件队列中。此队列的使用者恢复该类型并在完成后进行清理。
我建议您使用variant
,例如boost::variant
或std::variant
。这是一个类型安全的联合,它将管理缓冲区中新放置的详细信息,安全地复制和移动东西,调用dtors等。你可以有一个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
中复制该$23,455
。 / p>