Неожиданная отправка виртуальной функции при использовании ссылки на базовый класс вместо указателя
Допустим, у меня есть простая иерархия классов следующим образом с общим 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 ответа
У вас неопределенное поведение, потому что ссылки висят в том месте, где вы их используете для вызова 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;
Возвращаемое значение std::make_shared
в последнем примере не привязан к rvalue (std::shared_ptr<...>&&
) или же const
-качественная ссылка lvalue (const std::shared_ptr<...>&
), следовательно, его время жизни не продлено. Вместо этого возвращаемое значение std::shared_ptr::operator*
временного экземпляра привязывается к левой части выражения (b_ref
, l_ref
), что приводит к неопределенному поведению.
Если вы хотите получить доступ к виртуальному api()
метод через неconst
lvalue ссылки на 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();
хотя это почти идентично вышесказанному.
Создание умного указателя (или любого другого объекта) является плохим дизайном.
Эта проблема дизайна вызывает плохое управление сроком службы, в частности, разрушение объекта, который все еще используется. Это вызывает неопределенное поведение; неопределенное поведение по определению не определено или даже не ограничено стандартом (оно может быть ограничено другими принципами, инструментами, устройствами).
Мы все еще можем попытаться понять, как код с UB переводится на практике во многих случаях. Конкретное поведение, которое вы наблюдаете:
который печатает:
FirstLevel FirstLevel
безусловно, вызвано интерпретацией памяти, оставленной уничтоженным объектом, как если бы это был живой объект; поскольку эта память не использовалась повторно (по случайности, и любое изменение в программе или реализации может нарушить это свойство), вы видите объект в состоянии, в котором он находился во время уничтожения.
В деструкторе вызовы виртуальных функций разрушаемого объекта всегда преобразуются в переопределитель функции в классе деструктора: inside Base::~Base
Призыв к foo()
решает в Base::foo()
; компилятор, который использует vptrs и vtables (на практике все компиляторы), гарантирует, что виртуальные вызовы разрешаются таким образом, сбрасывая vptr в vtable для Base
в начале выполнения деструктора базового класса.
Итак, вы видите, что vptr все еще указывает на базовый класс vtable.
Конечно, реализация отладки имеет право установить vptr на какое-то другое значение в конце деструктора базового класса, чтобы убедиться, что попытка вызова виртуальных функций на уничтоженном объекте не удалась ясным и однозначным способом.