假设我有一个简单的类层次结构,并带有一个通用的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
为什么使用引用而不是指针时分派有所不同?
答案 0 :(得分:3)
您有未定义的行为,因为引用在您使用它们调用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;
答案 1 :(得分:2)
在上一个示例中,std::make_shared
的返回值未绑定到右值(std::shared_ptr<...>&&
)或经const
限定的左值引用(const std::shared_ptr<...>&
),其生存期为因此不扩展。而是将 temporary 实例的std::shared_ptr::operator*
的返回值绑定到表达式(b_ref
,l_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设置为其他某个值,以确保尝试对销毁的对象调用虚函数以明确且明确的方式失败。