Привет Хабр! К настоящему времени о полиморфизме вообще и его реализации в C++ в частности написано немало учебников и статей. Однако, к моему удивлению, при описании полиморфизма никто (или почти никто) не обращает внимания на тот факт, что помимо динамического полиморфизма C++ обладает еще и довольно мощной возможностью использования своего младшего брата — статического полиморфизма. Более того, это одна из фундаментальных концепций STL — неотъемлемая часть его стандартной библиотеки.

Поэтому в этой статье мне хотелось бы поговорить о нем хотя бы в общих чертах и ​​его отличиях от известного динамического полиморфизма. Надеюсь, что эта статья будет интересна тем, кто только начал изучать принципы ООП и сможет взглянуть на его «третьего слона» с новой стороны.

Сразу оговорюсь, что зачастую полиморфизм включает в себя и перегрузку функций, но эту тему мы затрагивать не будем. Также в C++ существует довольно интересный способ реализации статического полиморфизма с помощью макросов, но этот аспект заслуживает отдельной статьи и в силу специфики его использования не представляет большого практического интереса.

Полиморфизм

Полиморфизм предоставляет возможность комбинировать различные типы поведения, используя общие обозначения, что обычно означает использование функций для обработки разных типов данных; считается одним из трех столпов объектно-ориентированного программирования. По особенностям реализации полиморфизм можно разделить на ограниченный и неограниченный, динамический и статический.

Концепция ограниченности означает, что интерфейсы полностью определены заранее, например, путем создания базового класса. Неограниченный полиморфизм не накладывает никаких ограничений на тип, а лишь требует реализации определенного синтаксиса.

Статическое означает, что привязка интерфейсов происходит на этапе компиляции, динамическое — во время выполнения.

Язык программирования C++ обеспечивает ограниченная динамика полиморфизм при использовании наследования и виртуальных функций и неограниченный статический – при использовании шаблонов. Таким образом, в рамках данной статьи эти понятия будут просто упомянуты. статический И динамичный полиморфизм. Однако в целом разные инструменты на разных языках могут предоставлять разные комбинации типов полиморфизма.

ЧИТАТЬ   Робот Атлас удален (видео)

Динамический полиморфизм

Динамический полиморфизм – наиболее распространенное воплощение полиморфизма вообще. В C++ эта функциональность реализуется путем объявления общих возможностей с использованием функциональности виртуальных функций. В этом случае указатель на таблицу виртуальных методов (vtable) сохраняется в объекте класса, а вызов метода осуществляется путем разыменования указателя и вызова метода, соответствующего типу, с которым был создан объект. Таким образом, вы можете манипулировать этими объектами, используя ссылки или указатели на базовый класс (однако вы не можете использовать копирование или перемещение).

Рассмотрим следующий простой пример: предположим, что существует абстрактный класс собственности, который описывает налогооблагаемую собственность с помощью одного чисто виртуального метода. getTaxи поле worth, содержащий стоимость; и три класса: CountryHouse, Car, Apartmentкоторые реализуют этот метод, определяя другую ставку налога:

Пример
class Property
{
protected:
    double worth;
public:
    Property(double worth) : worth(worth) {}
    virtual double getTax() const = 0;
};
class CountryHouse :
    public Property
{
public:
    CountryHouse(double worth) : Property(worth) {}
    double getTax() const override { return this->worth / 500; }
};

class Car :
    public Property
{
public:
    Car(double worth) : Property(worth) {}
    double getTax() const override { return this->worth / 200; }
};

class Apartment :
    public Property
{
public:
    Apartment(double worth) : Property(worth) {}
    double getTax() const override { return this->worth / 1000; }
};


void printTax(Property const& p)
{
    std::cout getTax() 

Если заглянуть «под капот», то можно увидеть, что компилятор (в моем случае gcc) неявно добавляет указатель на виртуальную таблицу в начале класса Property и инициализирует этот указатель в соответствии с желаемым типом конструктора. . А вот как выглядит фрагмент с вызовом метода getTax() в дизассемблированном коде:

mov     rbp, QWORD PTR [rbx]; В регистр rbp помещаем указатель на объект
mov     rax, QWORD PTR [rbp+0]; В регистр rax помещаем указатель на vtable
call    [QWORD PTR [rax]]; Вызываем функцию, адрес которой лежит по адресу, лежащему в rax (первое разыменование даёт vtable, второе – адрес функции.

Статический полиморфизм

Наконец, перейдем к самому интересному. В C++ шаблоны — это средство статического полиморфизма. Однако это очень мощный инструмент, имеющий огромное количество применений, и их детальное рассмотрение и изучение потребует полноценного обучения, поэтому для целей данной статьи мы ограничимся поверхностным рассмотрением.

ЧИТАТЬ   Как мне настроить свой сервер, не имея возможности установить для него статический IP-адрес?

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

Пример
class CountryHouse
{
private:
    double worth;
public:
    CountryHouse(double worth) : worth(worth) {}
    double getTax() const { return this->worth / 500; }
};

class Car
{
private:
    double worth;
public:
    Car(double worth) : worth(worth) {}
    double getTax() const { return this->worth / 200; }
};

class Apartment
{
private:
    unsigned worth;
public:
    Apartment(unsigned worth) : worth(worth) {}
    unsigned getTax() const { return this->worth / 1000; }
};

template 
void printTax(T const& p)
{
    std::cout 

Здесь я заменил тип возвращаемого значения Apartment::GetTax(). Поскольку благодаря перегрузке оператора >> синтаксис (а в данном случае и семантика) остался корректным, этот код компилируется вполне хорошо, тогда как устройство виртуальной функции не простило бы нам такой свободы.

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

Как я отметил во введении, STL — хороший пример использования статического полиморфизма. Например, вот как выглядит простая реализация функции: std::for_each:

template
constexpr UnaryFunc for_each(InputIt first, InputIt last, UnaryFunc f)
{
    for (; first != last; ++first)
        f(*first);
 
    return f; 
}

При вызове функции достаточно предоставить объекты, для которых синтаксис операций, присутствующих в теле функции, будет правильным (кроме того, поскольку передаются параметры и возвращается результат, необходимо определить копию( переместите) конструктор к ним по значению). Однако следует понимать, что модель определяет только синтаксис, поэтому несоответствия между принятым синтаксисом и семантикой могут привести к неожиданным результатам. Так, например, естественно предположить, что *first не меняется на first, хотя синтаксических ограничений на это нет.

ЧИТАТЬ   Удаленный динамический импорт компонента Module Federation на Vue 3

Концепции

Концептуальный аппарат, введенный в стандарт относительно недавно (начиная с C++20), может помочь несколько ужесточить требования к строковым типам. В принципе, аналогичного эффекта можно было добиться и раньше, используя принцип SFINAE (сбой замены не является ошибкой) и производные инструменты типа std::enable_if, но их синтаксис достаточно громоздкий, и полученный код читать не очень приятно. . В частности, использование концепций делает сообщение об ошибке гораздо более прозрачным при попытке использовать неподходящий тип.

В нашем простом примере концепция может выглядеть так:

template 
concept Property = requires (T const& p) { p.getTax(); };

И декларация printTax:

template 
void printTax(T const& p);

Теперь, если мы попытаемся передать int в качестве параметра, мы получим очень конкретное сообщение об ошибке:

:46:13: error: no matching function for call to 'printTax(int)'
   46 |     printTax(5);
      |     ~~~~~~~~^~~
:34:6: note: candidate: 'template  requires  Property void printTax(const T&)'
   34 | void printTax(T const& p)
      |      ^~~~~~~~
:34:6: note:   template argument deduction/substitution failed:
:34:6: note: constraints not satisfied

Заключение

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

На этом на сегодня все, надеюсь, читатель почерпнул из этой статьи что-то новое или освежил хорошо забытое старое.

Source

От admin