Если мы знакомы с доступом к элементам массива с использованием арифметики указателей, легко понять, как объекты размещаются в памяти и как работает dynamic_cast
. Рассмотрим следующий простой класс:
struct point
{
point (int x, int y) : x_ (x), y_ (y) { }
int x_;
int y_;
};
point* p = new point(10, 20);
Предположим, что p
назначено ячейке памяти 0x01
. Его переменные-члены хранятся в своих разных местах, например, x_
хранится в 0x04
и y_
в 0x07
. Проще визуализировать объект p
как массив указателей. p
(в нашем случае (0x1
) указывает на начало массива:
0x01
+-------+-------+
| | |
+---+---+----+--+
| |
| |
0x04 0x07
+-----+ +-----+
| 10 | | 20 |
+-----+ +-----+
Таким образом, код для доступа к полям, по сути, станет доступом к элементам массива с использованием арифметики указателей:
p->x_; // => **p
p->y_; // => *(*(p + 1))
Если язык поддерживает какое-либо автоматическое управление памятью, например, GC, дополнительные поля могут быть добавлены в массив объектов за сценой. Представьте себе реализацию C ++, которая собирает мусор с помощью подсчета ссылок. Затем компилятор может добавить дополнительное поле (rc) для отслеживания этого количества. Приведенное выше представление массива становится:
0x01
+-------+-------+-------+
| | | |
+--+----+---+---+----+--+
| | |
| | |
0x02 0x04 0x07
+--+---+ +-----+ +-----+
| rc | | 10 | | 20 |
+------+ +-----+ +-----+
Первая ячейка указывает на адрес счетчика ссылок. Компилятор выдаст соответствующий код для доступа к частям p
, которые должны быть видны внешнему миру:
p->x_; // => *(*(p + 1))
p->y_; // => *(*(p + 2))
Теперь легко понять, как работает dynamic_cast
. Компилятор работает с полиморфными классами, добавляя дополнительный скрытый указатель на базовое представление. Этот указатель содержит адрес начала другого «массива», называемого vtable , который, в свою очередь, содержит адреса реализаций виртуальных функций в этом классе. Но первая запись в vtable является особенной. Он указывает не на адрес функции, а на объект класса с именем type_info
. Этот объект содержит информацию о типе времени выполнения объекта и указатели на type_info
s его базовых классов. Рассмотрим следующий пример:
class Frame
{
public:
virtual void render (Screen* s) = 0;
// ....
};
class Window : public Frame
{
public:
virtual void render (Screen* s)
{
// ...
}
// ....
private:
int x_;
int y_;
int w_;
int h_;
};
Объект Window
будет иметь следующую структуру памяти:
window object (w)
+---------+
| &vtable +------------------+
| | |
+----+----+ |
+---------+ vtable | Window type_info Frame type_info
| &x_ | +------------+-----+ +--------------+ +----------------+
+---------+ | &type_info +------+ +----+ |
+---------+ | | | | | |
| &y_ | +------------------+ +--------------+ +----------------+
+---------+ +------------------+
+---------+ | &Window::render()|
+---------+ +------------------+
+---------+
| &h_ |
+---------+
Теперь рассмотрим, что произойдет, когда мы попытаемся разыграть Window*
a Frame*
:
Frame* f = dynamic_cast<Frame*> (w);
dynamic_cast
будет переходить по ссылкам type_info
из таблицы w
, подтверждает, что Frame
находится в списке базовых классов, и присваивает w
f
. Если он не может найти Frame
в списке, f
устанавливается на 0
, указывая, что кастинг не удался. vtable предоставляет экономичный способ представления type_info
класса. Это одна из причин, по которой dynamic_cast
работает только для классов с virtual
функциями. Ограничение dynamic_cast
полиморфными типами также имеет смысл с логической точки зрения. Это означает, что если объект не имеет виртуальных функций, им нельзя безопасно управлять, не зная его точного типа.
Тип цели dynamic_cast
не обязательно должен быть полиморфным. Это позволяет нам обернуть конкретный тип в полиморфный тип:
// no virtual functions
class A
{
};
class B
{
public:
virtual void f() = 0;
};
class C : public A, public B
{
virtual void f() { }
};
C* c = new C;
A* a = dynamic_cast<A*>(c); // OK