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То, что будет рассказываться в этом параграфе формально не является частью стандарта 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) у разных объектов
наследующих один и тот же тип.
Виртуальные функции очевидно не могут быть static.
Виртуальные функции нельзя оставлять без определения, ведь нужны
указатели на нее, чтобы создать vtable, будет ошибка линковки.
На доске был какой-то пример, который часто дают на собесах(c), поэтому в данном конспекте он отсутствует. Стоит гуглить pure virtual function call example.