7.2 KiB
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.