Неожиданная отправка виртуальной функции при использовании ссылки на базовый класс вместо указателя

Допустим, у меня есть простая иерархия классов следующим образом с общим 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 на какое-то другое значение в конце деструктора базового класса, чтобы убедиться, что попытка вызова виртуальных функций на уничтоженном объекте не удалась ясным и однозначным способом.

Другие вопросы по тегам