Added 11.md
This commit is contained in:
parent
62601523d4
commit
72f0941bb1
115
pages/mipt_cxx1/11.md
Normal file
115
pages/mipt_cxx1/11.md
Normal file
@ -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<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).
|
||||
|
||||
Для полиморфных объектов можно в рантайме узнать информацию о типе явно.
|
||||
|
||||
```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.
|
Loading…
Reference in New Issue
Block a user