使用基类引用而不是指针时发生意外的虚函数调度

时间:2018-09-25 10:44:16

标签: c++ shared-ptr undefined-behavior object-lifetime vptr

假设我有一个简单的类层次结构,并带有一个通用的api:

#include <memory>

class Base {
    public:
        void api() {
            foo();
        }

    protected:
        virtual void foo() {
            std::cout << "Base" << std::endl;

        }
    };

    class FirstLevel : public Base {
    protected:
        virtual void foo() {
            std::cout << "FirstLevel" << std::endl;
        }
    };

当我使用基类指针时,我得到如下正确的分派:

std::shared_ptr<Base> b = std::make_shared<Base>();
std::shared_ptr<Base> fl = std::make_shared<FirstLevel>();

b->api();
fl->api();

可以正确打印:

Base
FirstLevel

但是,当我使用基类引用时,该行为是意外的:

Base &b_ref = *std::make_shared<Base>();
Base &fl_ref = *std::make_shared<FirstLevel>();

b_ref.api();
fl_ref.api();

打印:

FirstLevel
FirstLevel

为什么使用引用而不是指针时分派有所不同?

3 个答案:

答案 0 :(得分:3)

您有未定义的行为,因为引用在您使用它们调用api()时悬而未决。由共享指针管理的对象在用于初始化b_reffl_ref的行之后不再存在。

您可以通过引用仍然存在的对象来解决此问题:

auto b = std::make_shared<Base>();
auto fl = std::make_shared<FirstLevel>();

Base &b_ref = *b;
Base &fl_ref = *fl;

答案 1 :(得分:2)

在上一个示例中,std::make_shared的返回值未绑定到右值(std::shared_ptr<...>&&)或经const限定的左值引用(const std::shared_ptr<...>&),其生存期为因此不扩展。而是将 temporary 实例的std::shared_ptr::operator*的返回值绑定到表达式(b_refl_ref)的左侧,这导致未定义行为。

如果您要通过对api()const的非Base左值引用访问虚拟FirstLevel方法,则可以通过以下方式解决此问题:

auto b = std::make_shared<Base>();
Base& b_ref = *b;

b_ref.api();

,与FirstLevel类似。不过,b_ref超出范围后请不要使用b。您可以通过

来延长生命周期
auto&& b = std::make_shared<Base>();
Base& b_ref = *b;

b_ref.api();

尽管与上面几乎相同。

答案 2 :(得分:0)

使智能指针(或任何欠下的对象)成为临时对象是错误的设计。

该设计问题导致不良的寿命管理,尤其是破坏了仍在使用的对象。导致不确定的行为;根据定义,未定义的行为不受标准定义,甚至不受标准限制(可以受其他原理,工具,设备约束)。

在许多情况下,我们仍然可以尝试了解UB的代码在实践中是如何翻译的。您观察到的特定行为

  

打印:

FirstLevel
FirstLevel

肯定是由于将被破坏对象留下的内存解释为活动对象而导致的;由于该内存当时没有被重用(由于偶然性,并且对程序或实现的任何更改都可能破坏该属性),因此您会看到一个对象处于销毁期间的状态。

在析构函数中,被破坏对象的虚函数调用始终解析为析构函数类中函数的重写器:在Base::~Base内部,对foo()的调用解析为{ {1}};使用vptrs和vtables的编译器(实际上,所有编译器)都可以通过在基类析构函数执行开始时将vptr重置为Base::foo()的vtable来确保虚拟调用得以解决。

所以您看到的是vptr仍指向基类vtable。

当然,调试实现有权在基类的析构函数的末尾将vptr设置为其他某个值,以确保尝试对销毁的对象调用虚函数以明确且明确的方式失败。