From 72f0941bb1b7d111c43c2302388dde913785a7ac Mon Sep 17 00:00:00 2001 From: thematdev Date: Tue, 14 Nov 2023 16:05:20 +0300 Subject: [PATCH] Added 11.md --- pages/mipt_cxx1/11.md | 115 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 pages/mipt_cxx1/11.md diff --git a/pages/mipt_cxx1/11.md b/pages/mipt_cxx1/11.md new file mode 100644 index 0000000..0c00128 --- /dev/null +++ b/pages/mipt_cxx1/11.md @@ -0,0 +1,115 @@ +## 5.4 RTTI and dynamic cast. + +```C++ +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(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(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). + +Для полиморфных объектов можно в рантайме узнать информацию о типе явно. + +```C++ + 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(виртуальная таблица) +-- это структура данных, хранящаяся в статической памяти одна на тип, +которая хранит адреса виртуальных методов и предков, предков мы обсудим чуть позже. + +```C++ +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` следующим образом + +```C++ +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.