放置新对象和继承

8
大家晚上好。
一段代码片段胜过千言万语:

// Storage suitable for any of the listed instances
alignas(MaxAlign<Base, Derived1, Derived2>::value)
char storage[MaxSize<Base, Derived1, Derived2>::value];

// Instanciate one of the derived classes using placement new
new (storage) Derived2(3.14);


// Later...

// Recover a pointer to the base class
Base &ref = *reinterpret_cast<Base*> (storage);

// Use its (virtual) functions
ref.print();

// Destroy it when we're done.
ref.~Base();

如您所见,我只想通过基类访问实例,而不实际存储基类指针。请注意,在第二部分中,“Derived2”类型信息将丢失,因此我只剩下“storage”和一个派生实例的保证。
由于放置new从不调整目标指针,这归结为使用reinterpret_cast向上转换为基类。现在我知道这很危险,因为更合适的static_cast在某些情况下会调整指针。[1]
事实上,它会触发未定义行为。您可以在Coliru上找到完整的代码here on Coliru(g++ 4.9.0),其中它会立即在运行时崩溃。与此同时,在我的PC上(g++ 4.8.2),一切正常。请注意,在g++ 4.9上,在调用函数之前输出两个指针显示相同的值...并且有效。
所以我试图从反面考虑问题:推动派生实例,使指向 Base 的指针等于 storage
void *ptr = static_cast<Derived2*>(reinterpret_cast<Base*>(storage));
new (ptr) Derived2(3.14);

没有运气。这个东西在g++ 4.8上仍然很好,但在g++ 4.9上失败了。
编辑:想一想,上面的方法并不那么聪明。因为,如果派生实例最终出现在storage之前...糟糕。
所以我的问题是:是否有解决我正在尝试实现的问题的方法?或者,[1]中提到的情况是否已经被充分定义,我可以编写适用于一些类的代码(例如,没有虚拟继承)?

3
您可以直接从 new 表达式中检索出 Base*,并将其存储以供以后使用- Base *ref = new (storage) Derived2(3.14);-这避免了显式强制转换。即使进行了此更改,代码仍会崩溃,我认为这可能是由于此错误引起的。 - Praetorian
1
@Praetorian:好的,那个 bug 看起来完美地匹配了这种行为。 - Mooing Duck
@Praetorian C风格的强制类型转换也不行,因为我没有派生类型。我想我会加入一些static_assert来确保指针是相同的,并继续进行。你应该发表有关g++错误的答案,这样我可以接受它。 - Quentin
即使您的特定类层次结构从不需要指针调整,实现也可以检测到未定义行为并使您的程序崩溃,因为它有这个自由。 - n. m.
@n.m. 这是免费的,但我不认为任何实现者会故意设计代码来破坏技术上正确的程序,并让人们使用他的编译器。 - Quentin
显示剩余17条评论
1个回答

3

我刚刚稍微修改了你的代码,似乎reinterpret_cast不是问题所在。 能够复制此错误的最小代码如下:

Coliru链接:http://coliru.stacked-crooked.com/a/dd9a633511a3d08d

#include <iostream>

struct Base { 
    virtual void print() {}
};

int main(int, char**) {
   Base storage[1];
   storage[0].print();

   std::cout <<"Succeed";
   return 0;
}

触发此错误的充分条件为:
  1. 将"storage"变量作为数组分配在堆栈上
  2. print()必须是虚方法
  3. 编译器选项应该是-O2
如果使用-O1,程序会编译并正常运行。
此外,这个错误似乎只会在使用g++ 4.9.0编译时出现。如果使用VS2012/2013或g++ 4.7.2编译,它会正常运行(您可以在http://www.compileonline.com/compile_cpp_online.php上测试)。
从以上情况来看,我认为这可能是一个特定于编译器的问题。
注意:本答案中给出的程序实际输出与原始发布者不同。它不会显示“Segmentation Fault”。但是,当成功运行时,它应该打印“Succeed”,而在Coliru上运行时没有显示。
编辑:修改了代码以复制错误。不需要派生类。

4
即使您在“main”函数中忘记了“Derived”并将其替换为“Base”,使用“g++ -O2”仍会导致分段错误(http://coliru.stacked-crooked.com/a/839f941954a7edad)。但是使用“clang++ -O2”不会出现这种情况。看起来像是一个g++的bug。 - Marc van Leeuwen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接