我的 C++ 片段中无法解释的断言失败

Unexplained assertion failure in my C++ snippet

以下是我能够从更大的代码库中提取的片段,希望能够说明目前我无法看到的某种内存损坏。这是在 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

有什么想法吗?我已经盯着这个看了好几个小时了,我一直没弄明白。

根据 C++ 标准,此代码不合法​​。存在多个问题:

  1. 对齐。您没有确保 Str 的存储与 std::string 对齐到同一边界,因此您的代码具有未定义的行为,无需诊断。使用 std::aligned_storage_tstd::array 更简单。

  2. 您正试图通过复制底层字节来复制 std::string。那是不合法的,并且该标准没有授予您这样做的许可。它违反了 C++ 中重要 class 类型的基本生命周期要求,并且在这种情况下违反了严格的别名规则。

在这个函数中,坏事正在发生

void f(Msg &tmsg)                                                                                                                                                                                               
{                                                                                                                                                                                                               
   Msg m;                                                                                                                                                                                                       
   new (&m) Str ("hello");                                                                                                                                                                                      
   tmsg = m;                                                                                                                                                                                                    
}                                                                                                                                                                                                               

tmsg = m 发生时。那是底层字节正在获取副本的时候,但这不是您可以安全地复制对象的方式。如果它是非平凡的,如 std::string,并且拥有像堆分配缓冲区这样的资源,则需要调用复制构造函数,否则 class 无法强制执行其保证。 (该行本身不会导致未定义的行为,但是当您尝试将 tmsg 字节重新解释为有效的 Str 时,即 UB。)

另请注意,因为您使用了 placement new,并且您从未在任何地方调用 dtor,所以您正在泄漏您新建的对象。你存储它的缓冲区是否存在于堆栈中并不重要,缓冲区没有责任调用 dtor,你有。

还允许优化器假设您不会尝试像这样复制非平凡的对象。优化器可能假设 tmsg 不包含有效的 Str 对象,因为那里从未调用过 Str 对象构造函数。

您可以将此代码更改为

void f(Msg &tmsg)                                                                                                                                                                                               
{                                                                                                                                                                                                               
   new (&tmsg) Str ("hello");                                                                                                                                                                                      
}                                                                                                                                                                                                               

并修复了对齐问题,然后我认为它具有明确定义的行为,至少我没有看到其他问题(泄漏除外)。

可以在存储缓冲区中分配对象,但必须非常小心。我建议您听取旧的 ISO C++ 常见问题解答的建议:

https://isocpp.org/wiki/faq/dtors#placement-new

ADVICE: Don’t use this “placement new” syntax unless you have to. Use it only when you really care that an object is placed at a particular location in memory.

... (if you don’t know what “alignment” means, please don’t use the placement new syntax). You have been warned.


编辑:根据以上评论:

The real code is trying to package more or less arbitrary types into an event queue. The consumer of this queue recovers the type and cleans up when done.

我建议您使用 variant,例如 boost::variantstd::variant。这是一个类型安全的联合体,它将管理缓冲区内新放置的细节、安全地复制和移动东西、调用 dtors 等。你可以有一个 std::vector<variant<....>> 或类似的队列,然后你不会有这种低级问题。

了解问题所在的另一种方法:如果 f 像这样更改,并且对齐问题已解决,您可以这样做:

void f(Msg &tmsg)                                                                                                                                                                                               
{                                                                                                                                                                                                               
   Msg m;                                                                                                                                                                                                       
   new (&m) Str ("hello");                                                                                                                                                                                      
   new (&tmsg) Str(*reinterpret_cast<Str*>(&m));
}                                                                                                                                                                                                       

因为您正在使用 placement new 语法调用复制构造函数,所以新的 Str 正确地在缓冲区 tmsg 中开始其生命周期,并复制 m 中的副本.