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

sam*_*ise -1 c++ memory-corruption

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

当我尝试运行时,我得到:

$ g++ main.cpp -g -std=c++11 && ./a.out
a.out: main.cpp:24: int main(int, char**): Assertion `ptr->v == "hello"' failed.
Aborted
Run Code Online (Sandbox Code Playgroud)

有任何想法吗?我一直在盯着这几个小时,我一直无法理解.

Chr*_*eck 5

根据C++标准,此代码不合法​​.有很多问题:

  1. 对准.您无法确保a的存储Str与同一边界对齐std::string,因此您的代码具有未定义的行为,无需诊断.它std::aligned_storage_tstd::array你喜欢的更简单.

  2. 您试图std::string通过复制底层字节来复制周围.这不合法,标准不会授予您许可.它违反了C++中非平凡类类型的基本生存期要求,并且在这种情况下违反了严格的别名规则.

在这个功能中,坏事正在发生

void f(Msg &tmsg)                                                                                                                                                                                               
{                                                                                                                                                                                                               
   Msg m;                                                                                                                                                                                                       
   new (&m) Str ("hello");                                                                                                                                                                                      
   tmsg = m;                                                                                                                                                                                                    
}                                                                                                                                                                                                               
Run Code Online (Sandbox Code Playgroud)

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

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

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

您可以将此代码更改为

void f(Msg &tmsg)                                                                                                                                                                                               
{                                                                                                                                                                                                               
   new (&tmsg) Str ("hello");                                                                                                                                                                                      
}                                                                                                                                                                                                               
Run Code Online (Sandbox Code Playgroud)

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

可以在存储缓冲区中分配对象,但必须非常小心.我建议你听听好旧的ISO C++ FAQ的建议:

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

建议:除非必须,否则请勿使用此"放置新"语法.仅在您真正关心对象放置在内存中的特定位置时才使用它.

...(如果您不知道"对齐"的含义,请不要使用放置新语法).你被警告了.


编辑:基于以上评论:

真正的代码试图将或多或少的任意类型打包到事件队列中.此队列的使用者恢复该类型并在完成后进行清理.

我建议你做的是使用a variant,like 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));
}                                                                                                                                                                                                       
Run Code Online (Sandbox Code Playgroud)

因为您使用放置新语法调用复制文件,所以新版本Str在缓冲区中正确地开始其生命周期,tmsg并且它会复制该文件m.