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

Unexplained assertion failure in my C++ snippet

本文关键字:断言 失败 无法解释 C++ 片段 我的      更新时间:2023-10-16

以下是我能够从更大的代码库中提炼出来的一个片段,希望能说明目前我无法看到的某种内存损坏。这是在 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++标准,此代码是不合法的。存在多个问题:

  1. 对准。您不能确保Str的存储与std::string相同的边界对齐,因此您的代码具有无需诊断的未定义行为。使用std::aligned_storage_t比像您一样std::array更简单。

  2. 您正在尝试通过复制基础字节来复制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::variantstd::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中创建副本。