5.4 RTTI and dynamic cast.

struct Base {
    int x = 0;
    virtual void f() {}
    virtual ~Base() = default;
};

struct Derived : Base {
    int y = 0;
    void f() override {}
};

int main() {
    Derived d;
    Base& b = d;
    dynamic_cast<Derived&>(b); // an example of correct dynamic_cast
    // if we know, that Base was from Derived, we can (dynamic) cast Base to Derived.
    // dynamic_cast will throw std::bad_cast if cast failed

    Derived* pd = dynamic_cast<Derived*>(b) // will return pointer to Derived in case 
    // of success, and nullptr otherwise
    if (pd) {
        // OK
    }
}

dynamic_cast работает только для типов с виртуальными функциями(полиморфных). Для типов у которых нет виртуальных функций нет способов узнать по родителю, что за базовый класс лежит по этому адресу. Для классов с виртуальными функциями поддерживается специальная информация(vtable), который нужен для того, чтобы понять какую функцию вызывать. По ней в рантайме можно восстановить тип. В случае если тип не полиморфный, код с dynamic_cast не скомпилируется.

Этот механизм называется RTTI(Runtime type information).

Для полиморфных объектов можно в рантайме узнать информацию о типе явно.

    std::type_info info = typeid(b);
    std::cout << info.name() << std::endl; // will print MANGLED type name
    // std::type_info can(and should) be compared

5.5 Memory layout of polymorphic objects

То, что будет рассказываться в этом параграфе формально не является частью стандарта C++, но является частью стандарта ABI и по факту почти наверняка так и есть.

Полиморфные объекты хранят указатель на vtable. vtable(виртуальная таблица) – это структура данных, хранящаяся в статической памяти одна на тип, которая хранит адреса виртуальных методов и предков, предков мы обсудим чуть позже.

struct Base {
    virtual void f() {}
    void h() {}
    int x;
}; // sizeof(Base) == 16 due to padding, Base is [vptr, int]

Что хранится в vtable для Base? Там хранится type_info для Base, а затем указатель на функцию f. По нулевому смещению хранится Base::f, информация о типе лежит “сзади”. Пусть мы ещё определили Derived следующим образом

struct Derived : Base {
    void f() override {}
    virtual void g() {}
    int y;
}; // Derived is [vptr, x(from Base), y]

Таблица для Derived выглядит как [&typeinfo, &f, &g], по нулевому смещению находится f. Как тогда выглядит вызов виртуальной функции? Пусть у нас есть b.f(). Компилятор видит, что она виртуальная, поэтому это не просто call по константому адресу, как это было бы, например, при вызове b.h(). Поэтому при вызове b.f() мы разыменовываем указатель на виртуальную таблицу. Компилятор уже знает, на каком месте в таблице стоит f, поэтому он может ее вызвать. Если бы под b лежал на самом деле Derived, то под b был бы указатель на другую таблицу, так и происходит разрешение.

dynamic_cast, соответственно, использует type_info чтобы понять, что ему надо делать.

Рассмотрим пример наследования Son -> Mom -> Granny с полями s, m, g, причем Granny не полиморфная, а Mom полиморфная. Тогда dynamic_cast от Son к Granny возможен, в памяти [ptr, g, m, s] нужно сдвинуть указатель на начало к g, так как у Granny нет ptr в силу того, что она не полиморфна. А вот обратно dynamic_cast невозможен, т.к Granny не является полиморфным типом.

А что происходит при множественном наследовании?

       / -> Mom -> Granny
Son --/
       \ -> Dad -> Granny

Для простоты положим Granny полиморфной, а наследование не виртуальным. Как выглядит Son? [ptr, g, m, ptr(from Dad), g, d, s]. Для Dad нужен второй указатель, так как у Son и Mom начало общее, а у Son и Dad уже нет.

Положим Granny имеет функцию f. Вызов son.f() неоднозначен. Но если мы переопределим f в Son, то вызов f в любой из двух Granny будет приводить к вызову нужной нам f, неоднозначности нет.

Теперь добавим щепотку виртуального наследования. Если по умолчанию в виртуальной таблице хранится top_offset – то, как далеко мы находимся от начала объекта, то при виртуальном наследовании в таблице хранится ещё virtual_offset, по какому смещению хранится предок.

Представим, что Granny виртуальная. Тогда Son выглядит в памяти как [ptr, m, ptr, d, s, ptr, g]. Таблица зависит не только от типа, но и от его положения в графе наследований, ведь теперь у нас есть разные смещения(virtual_offset) у разных объектов наследующих один и тот же тип.

5.7 Non obvious problems with virtual functions

Виртуальные функции очевидно не могут быть static. Виртуальные функции нельзя оставлять без определения, ведь нужны указатели на нее, чтобы создать vtable, будет ошибка линковки.

На доске был какой-то пример, который часто дают на собесах(c), поэтому в данном конспекте он отсутствует. Стоит гуглить pure virtual function call example.