std::unique_ptr继承切片和析构函数问题

3
请考虑以下完全运作的示例:
#include <iostream>
#include <memory>

class A {
    public:
    A() {
        std::cout << "A() \n";
    }
    ~A() {
        std::cout << "~A \n";
    }

};
class B:public A {
    public:
    B() {       
        std::cout << "B() \n";
    }
    ~B() {
        std::cout << "~B() \n";
    }
};

int main() {
    std::cout << "Output: \n";
    {
        std::unique_ptr<A> TestB(new B());
    }

    return 0;
}

输出结果为:
Output: 
A() 
B() 
~A 

有没有办法在继承中调用B的析构函数?我不知道unique_ptr也有切片问题。当然,我可以使用std::unique_ptr<B>,但我想要一个std::vector<std::unique_ptr<A>>并添加继承的项目。
是否有一种方法将std::unique_ptr列表与继承结合使用?

15
您需要将 ~A() 声明为虚函数。 - juanchopanza
@juanchopanza 我也是这么想的,这就是正确的答案,谢谢。 - Grapes
没有虚析构函数的UB... - Kerrek SB
2个回答

10
当你使用 delete p; 并且包含*p的最派生对象的类型(通俗地说,“*p的动态类型”)与*p的静态类型不同,那么如果*p的静态类型是一个类类型并且没有虚析构函数,则行为是未定义的。
为了解决这个问题,你需要使用virtual ~A()

这是正确的,但我不明白为什么,我的意思是,如果我声明一个对象B k,其中B是从A派生的,为什么调用A的析构函数应该是“自然”的呢?这里有一个明显的先例,或者只是因为C++支持多重继承... - user2384250
@user2384250:这与所有那些都没有关系。问题在于你的类型是unique_ptr<A>。如果是unique_ptr<B>就没问题了。 - Kerrek SB
也许我没有表达清楚,我考虑的情况是只声明 B k;,其中类在问题帖子中定义。我无法解释C++为什么设计成这样以及它的行为方式,当我声明 B k 并且 k 被销毁时,~A 也会被调用,这是我不太理解的部分。 - user2384250
如果你只是声明 B k;,那么没有问题。唯一会出现问题的情况是,当你试图通过指向基类子对象的指针销毁动态分配的对象时,这是显而易见的原因:子对象必须有某种机制知道它是较大对象的一部分。 - Kerrek SB
我理解这没有问题,我的问题只是当Bjarne S.设计这个时在想什么,而且我看到的是B比A大,所以在一个类型为B的对象上调用2个析构函数B和A对我来说是毫无意义的,因为B比A更大,A可以做什么B不能做的? - user2384250
@user2384250:~A()如何知道它是否是更大类型的一部分?而~B()又如何知道~A()的作用?你必须调用两个析构函数,并按正确的顺序调用... - Kerrek SB

0

@user2384250的真正问题似乎是为什么虚拟分派不是默认设置。

简而言之:您将在调用站点(对于每个创建的实例和整个程序)上支付性能惩罚,因为会破坏缓存局部性。如果所有函数都默认执行虚拟分派,则您将无法通过更加笨拙的语法来弥补这种惩罚。

如果您的类中没有使用虚拟分派,则您的类将具有最佳性能。即使B继承自A,如果A没有任何虚拟方法,则编译器无法区分B和A的实例;如果您有一个变量A* instance;并且调用instance->foo(),编译器无法知道您下面有一个B,并且它将调用A::foo()

当你在A中声明foo()virtual时,编译器会为A创建一个虚拟表,将foo()插入该虚拟表,并向类添加一个隐藏的虚拟表指针。然后,在每次调用foo()时,它知道需要执行虚拟分派(因为foo()被声明为虚拟)。它将加载由指针给出的查找表,并调用那里告诉它的foo()。这样,当你有一个B的实例时,指针将指向B类的查找表;当你有一个A的实例时,它将指向A的实例。因此,无论instance是A*还是B*,编译器只需加载查找表并调用分派表中的foo,而不管在调用点声明的类型如何。

正如您所见,即使添加一个虚方法,也会有一个隐藏的前期成本,这与调用虚方法无关;每个类都会得到1个查找表,而且您类的每个实例都会增加1个指针。此外,编译器无法提前知道您是否会创建子类(虚表指针位于您首次声明该方法为虚方法的类中)。如果您希望默认行为是虚分派,则程序中的每个类都将不必要地支付这种性能惩罚。

此外,由于上述机制,虚方法略微更昂贵:编译器不是插入一条指令:跳转到函数foo(),而是:加载此实例的虚指针,添加函数foo()的偏移量,解引用该条目(函数的地址)并跳转到它。这不仅涉及更多的CPU周期,还破坏了您的缓存局部性。

最后,您应该认真考虑继承、组合或模板哪种更好地解决了问题;每种方法都有其权衡。


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