struct Base {
int x = 0;
virtual void f() {}
virtual ~Base() = default;
};
struct Derived : Base {
int y = 0;
void f() override {}
};
int main() {
;
Derived d& b = d;
Basedynamic_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
* pd = dynamic_cast<Derived*>(b) // will return pointer to Derived in case
Derived// 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.