Неожиданная отправка виртуальной функции при использовании ссылки на базовый класс вместо указателя - PullRequest
0 голосов
/ 25 сентября 2018

Допустим, у меня есть простая иерархия классов следующим образом с общим 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 голосов
/ 25 сентября 2018

Возвращаемое значение 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();

, хотя это почти идентично приведенному выше.

0 голосов
/ 27 сентября 2018

Создание интеллектуального указателя (или любого объекта-должника) временным является плохим проектом.

Эта проблема дизайна приводит к неправильному управлению временем жизни, в частности к разрушению объекта, который все еще используется.Это вызывает неопределенное поведение;неопределенное поведение по определению не определено или даже не ограничено стандартом (оно может быть ограничено другими принципами, инструментами, устройствами).

Мы все еще можем попытаться понять, как код с UB транслируется на практике во многихслучаев. специфическое поведение , которое вы наблюдаете:

, которое печатает:

FirstLevel
FirstLevel

, безусловно, вызвано интерпретацией памяти, оставленной уничтоженным объектом, как будтоэто был живой объект;поскольку эта память не использовалась повторно (по случайности, и любое изменение в программе или реализации может нарушить это свойство), вы видите объект в состоянии, в котором он находился во время уничтожения.

В деструкторевызовы виртуальных функций разрушаемого объекта всегда преобразуются в переопределитель функции в классе деструктора: внутри Base::~Base, вызов foo() разрешается в Base::foo();компилятор, который использует vptrs и vtables (на практике все компиляторы), гарантирует, что виртуальные вызовы разрешаются таким образом, путем сброса vptr в vtable для Base в начале выполнения деструктора базового класса.

Итак, вы видите, что vptr все еще указывает на базовый класс vtable.

Конечно, реализация отладки имеет право установить для vptr какое-то другое значение в конце деструктора базового класса.чтобы убедиться, что попытка вызова виртуальных функций на уничтоженном объекте не дает четкого и однозначного результата.

0 голосов
/ 25 сентября 2018

У вас неопределенное поведение, потому что ссылки висят в том месте, где вы их используете для вызова 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;
...