ООП в C++ все еще одна из ключевых парадигм. Принято выделять три классические характеристики ООП: инкапсуляция, наследование, полиморфизм. Шутки ради давайте реализуем эти базовые возможности на чистом Си.
Классический и заезженный пример - это иерархия shape <- rectangle. на мой взгляд, это слишком искусственно, непрактично и неинтересно. Более полезным может оказаться текстовый логгер. Не претендуя на идеальность реализации и пренебрегая всякими подробностями в виде разных кодировок и локалей, давайте реализуем его.
Инкапсуляция
Под инкапсуляцией следует понимать локальность данных и операций над ними. В сиплюсплюсном классе мы определяем данные и операции для работы с ними, и неявно ипользуем указатель this
, который передается “нулевым” параметром в функции-члены класса. На сях давайте определим тупую структуру и три тупые функции, которые будут принимать первым параметром указатель на тупую структуру.
Отлично! Поместив эти определения в заголовочный файл, и написав какую-нибудь реализацию конструктора, деструктора и функции log_line (например, с выводом в stdout), мы можем подключить это к основному файлу и использовать.
Наследование
Производный класс в C++ содержит в себе базовый, конструктор производного сперва вызывает контруктор базового, а деструктор производного вызывает детруктор базового напоследок.
Расширим пример с логгером – перенесем логгер в stdout в производный класс и добавим логгер в текстовый файл, а корень иерархии сделаем “абстрактным классом”. Вот так, например, будет выглядеть “класс” логгера в файл
А его конструктор будет вызывать конструктор базового, передавая ему некоторые из параметров
Таким образом получается публичное наследование, и члены базового класса доступны через поле super.
Полиморфизм
Динамический полиморфизм и позднее связывание реализуются через vtbl – таблицу указателей на функции, которые мы помечаем виртуальными, и vptr – указатель на эту таблицу. При создании объекта производного класса, его виртуальные функции в процессе исполнения записываются в vtbl, через которую и происходит вся диспетчеризация. Добавим vtbl в базовый класс:
У нас будет одна виртуальная функция void log_that(base_logger_t const * loggers[], uint8_t n_loggers, char const* msg);
, которую мы будем использовать вот так
Во всех конструкторах, и в базовом, и в производных произведем инициализацию vptr
И затем добавим каждому классу реализацию log_line_impl с модификатором static
. Реализация в базовом классе вызывает assert(0)
, имитируя ошибку при вызове метода у абстрактного класса, у остальных честно напишем логирование заданного сообщения с таймстампом и именем файла, откуда логирование производится.
Как сделать диспетчеризацию вызовов? У нас есть поле базового класса vptr, которому мы привоили при создании разные наборы реализаций виртуальных функций. В базовом класе сделаем вот такую прокси-функцию
Именно она и вызывает нужные реализации в зависимости от типа объекта. Последний штрих – связать “виртуальную” функцию с прокси-диспетчером
Ремарки
Реализация получилась очень наивная. Всё публично, всё явно, руками вызываем все деструкторы, никакого специального синтаксиса. Но главое – базовые идеи локальности данных и операций, иерархии классов и позднего связывания через vtbl реализованы и работают. Полный код можно найти вот тут