C++ 使用基类引用而不是指针时发生意外的虚拟函数分派
假设我有一个简单的类层次结构,如下所示,带有一个公共api:C++ 使用基类引用而不是指针时发生意外的虚拟函数分派,c++,shared-ptr,undefined-behavior,object-lifetime,vptr,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; } }
#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;
}
};
但是,当我使用基类引用时,行为是意外的:
Base &b_ref = *std::make_shared<Base>();
Base &fl_ref = *std::make_shared<FirstLevel>();
b_ref.api();
fl_ref.api();
当使用引用而不是指针时,为什么调度不同?您有未定义的行为,因为引用在您使用它们调用
api()
的点上悬空。在用于初始化b_ref
和fl_ref
的行之后,由共享指针管理的对象不再存在
您可以通过引用仍处于活动状态的对象来修复它:
auto b = std::make_shared<Base>();
auto fl = std::make_shared<FirstLevel>();
Base &b_ref = *b;
Base &fl_ref = *fl;
auto b=std::make_shared();
自动fl=std::使_共享();
基准&b_参考=*b;
基础和水平参考=*fl;
您有未定义的行为,因为引用挂起在您使用它们调用api()
的点上。在用于初始化b_ref
和fl_ref
的行之后,由共享指针管理的对象不再存在
您可以通过引用仍处于活动状态的对象来修复它:
auto b = std::make_shared<Base>();
auto fl = std::make_shared<FirstLevel>();
Base &b_ref = *b;
Base &fl_ref = *fl;
auto b=std::make_shared();
自动fl=std::使_共享();
基准&b_参考=*b;
基础和水平参考=*fl;
上一个示例中std::make_-shared
的返回值没有绑定到右值(std::shared_-ptr&
)或const
限定左值引用(const-std::shared_-ptr&
),因此其生存期不会延长。相反,临时实例的std::shared_ptr::operator*
的返回值绑定到表达式的左侧(b_ref
,l_ref
),这会导致未定义的行为
如果要通过对Base
和FirstLevel
的非const
左值引用访问虚拟api()
方法,可以通过
auto b = std::make_shared<Base>();
Base& b_ref = *b;
b_ref.api();
尽管这与上面的几乎相同。最后一个示例中的
std::make_shared
的返回值没有绑定到右值(std::shared_ptr&&
)或const
-限定左值引用(const std::shared_ptr&
),因此其生存期没有延长。相反,临时实例的std::shared_ptr::operator*
的返回值绑定到表达式的左侧(b_ref
,l_ref
),这会导致未定义的行为
如果要通过对Base
和FirstLevel
的非const
左值引用访问虚拟api()
方法,可以通过
auto b = std::make_shared<Base>();
Base& b_ref = *b;
b_ref.api();
尽管这与上面的几乎相同。将智能指针(或任何欠下的对象)作为临时对象是不好的设计
该设计问题会导致糟糕的生命周期管理,特别是对仍在使用的对象的破坏。导致未定义的行为;定义未定义的行为不受标准的定义,甚至不受标准的约束(它可以受其他原则、工具、设备的约束)
在许多情况下,我们仍然可以尝试理解UB代码在实践中是如何翻译的。您观察到的具体行为:
其中打印:
FirstLevel
FirstLevel
FirstLevel
FirstLevel
当然,这是由于将被摧毁的物体留下的记忆解释为好像它是活的物体;由于当时内存没有被重用(由于偶然的原因,对程序或实现的任何更改都可能破坏该属性),因此您可以看到一个对象处于销毁期间的状态
在析构函数中,被析构函数对象的虚拟函数调用总是解析为析构函数类中函数的重写器:在Base::~Base
内部,对foo()
的调用解析为Base::foo()
;使用vptrs和vtables的编译器(实际上,所有编译器)通过在基类析构函数执行开始时将vptr重置为Base
的vtable来确保通过这种方式解决虚拟调用
因此,您看到的是vptr仍然指向基类vtable
当然,调试实现有权在基类的析构函数末尾将vptr设置为其他值,以确保尝试在已销毁对象上调用虚拟函数时以明确无误的方式失败。将智能指针(或任何欠对象)设为临时对象是错误的设计
该设计问题会导致糟糕的生命周期管理,特别是对仍在使用的对象的破坏。导致未定义的行为;定义未定义的行为不受标准的定义,甚至不受标准的约束(它可以受其他原则、工具、设备的约束)
在许多情况下,我们仍然可以尝试理解UB代码在实践中是如何翻译的。您观察到的具体行为:
其中打印:
FirstLevel
FirstLevel
FirstLevel
FirstLevel
当然,这是由于将被摧毁的物体留下的记忆解释为好像它是活的物体;由于当时内存没有被重用(由于偶然的原因,对程序或实现的任何更改都可能破坏该属性),因此您可以看到一个对象处于销毁期间的状态
在析构函数中,被析构函数对象的虚拟函数调用总是解析为析构函数类中函数的重写器:在Base::~Base
内部,对foo()
的调用解析为Base::foo()
;使用vptrs和vtables的编译器(实际上,所有编译器)通过在基类析构函数执行开始时将vptr重置为Base
的vtable来确保通过这种方式解决虚拟调用
因此,您看到的是vptr仍然指向基类vtable
当然,调试实现有权在基类的析构函数末尾将vptr设置为其他值,以确保尝试在已销毁的对象上调用虚拟函数时以明确无误的方式失败。