Полиморфная кухня

2. Таблицы виртуальных функций

В предыдущей статье мы рассмотрели основы наследования и полиморфизма в языке C++. Теперь подробнее рассмотрим, как реализован механизм полиморфизма.

Повторим основные моменты. Если в классе есть хотя бы одна функция с ключевым словом virtual, то она считается виртуальной, а сам класс — полиморфным. Для каждого полиморфного класса создаётся специальная таблица — таблица виртуальных функций. А каждый объект любого из полиморфных классов дополняется указателем на одну из таких таблиц.

Рассмотрим простую иерархию: розетка и заземлённая розетка.

                                          SocketGroundedSocket
class Socket
{
public:
    virtual ~Socket()
    {
    }

    // ...
	
    unsigned GetVoltage() const
    {
        return voltage_;
    }

    virtual bool IsGrounded() const
    {
        return false;
    }

protected:
    unsigned voltage_ = 220;
};



class GroundedSocket : public Socket
{
public:

    // ...

    virtual bool IsGrounded() const
    {
        return true;
    }

protected:
    const enum
    {
        TN,
        TN_С,
        TN_S,
        TN_C_S,
        IT,
        ТТ
    } groundingType;
};

До начала выполнения функции main() в программе есть две таблицы виртуальных функций (virtual method table, VMT) — по одной на каждый полиморфный класс.

Обратите внимание: функция GetVoltage() не переопределена в классе GroundedSocket, поэтому в обеих таблицах напротив неё стоит один и тот же адрес. Другие функции были замещены, поэтому и адреса их не совпадают. Невиртуальные функции в таких таблицах отсутствуют.

Cоздадим два объекта — объект класса Socket и объект класса GroundedSocket.

int main()
{
    using namespace Electricity;
    typedef std::unique_ptr<Socket> socket_ptr;

    socket_ptr socket(new Socket);
    socket_ptr groundedSocket(new GroundedSocket);
	
    ...

Посмотрим внимательно на их внутреннюю структуру и процесс их инициализации. Каждый объект полиморфного класса (строго говоря, это лишь один из популярных вариантов реализации) дополняется указателем на таблицу виртуальных функций. Класс Socket является базовым, поэтому в указатель на таблицу виртуальных функций (варианты названий: vptr, __vfptr) просто запишем соответствующий адрес.

Создадим объект производного класса (GroundedSocket). Он тоже дополнен указателем на таблицу виртуальных функций. Помним, что сперва вызывается конструктор базовой части, он инициализирует этот указатель «неправильно», ведь базовому классу ничего неизвестно о таблице виртуальных функций производного класса.

Когда инициализируется та часть, которая относится к самому производному классу, она установит этот указатель в «правильное» значение: теперь он ведёт к нужной таблице.

Вызовем один и тот же метод для этих объектов.

    ...

    std::cout << socket->IsGrounded() << std::endl;
    std::cout << groundedSocket->IsGrounded() << std::endl;
}

В первом случае адрес, хранящийся в vptr, равен 0x010dcd10, во втором — 0x010dcd24. Вот в этом и вся суть механизма полиморфизма: каждый объект знает свой тип (свою таблицу виртуальных функций); пусть даже объект производного класса скрывается за указателем на базовый класс, в каждом объекте хранится вся нужная информация. А далее достаточно вызвать функцию из нужной таблицы с соответствующим смещением. В нашем случае это смещение равно 8 (элемент таблицы с индексом 2, то есть пропускаем два указателя по 4 байта). По этим смещениям мы находим адреса функций: 0x010d156f и 0x010d158d — и вызываем их.

Вызов из конструктора (деструктора)

Если вернуться к схемам, изображённым выше, можно заметить, что состояние объекта формируется не сразу, а пошагово. По этой причине не следует вызывать виртуальные функции из конструкторов (объект может быть не до конца сформирован) и деструкторов (объект может начать разрушаться).

Представьте, что базовый класс является абстрактным (содержит хотя бы одну чисто виртуальную функцию) и мы пытаемся вызвать виртуальную функцию для объекта производного класса из конструктора, но состояние указателя на таблицу виртуальных функций пока ещё неверное (см. иллюстрацию с перечёркнутой связью). Произойдёт попытка вызова функции для абстрактного класса (pure function call)!

Заключение

В этой статье мы подробно рассмотрели наиболее популярный вариант реализации полиморфизма, а также некоторые следствия работы этого механизма. В следующей статье поговорим о множественном и виртуальном наследовании.

19 октября 2016 · наследование и полиморфизм · программирование
Оставить комментарий (отменить)