Как стать автором
Обновить

Комментарии 9

мы должны сделать функцию Process не виртуальной и переместить ее в заголовочный файл

Перемещать имплементацию функции Process в заголовочный файл вовсе не обязательно, класс DebugPrint  не является темплейтом и имплементация его функций вполне может остаться в .cpp файле.

И кстати, если уж взялись писать про CRTP можно было бы рассказать насколько проще и удобней эту функциональность можно сделать в C++23.

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

Для ограничения шаблонов взаимодействия CRTP-наследников обычно использую в requires концепт is_crtp_base_of_v из объявления ниже:

#include <type_traits> 
#include <concepts>

 template <class T> 
 struct remove_cvref : std::remove_cv<std::remove_reference_t<T>> {}; 
  
 template <class T> 
 using remove_cvref_t = typename remove_cvref<T>::type; 
  
 template <class T, class U> 
 struct is_base_of : std::is_base_of<remove_cvref_t<T>, remove_cvref_t<U>> {}; 
  
 template <class T, class U> 
 concept is_base_of_v = is_base_of<T, U>::value; 
  
 template <template<typename T> class CRTP_Base, class CRTP_Derived> 
 struct is_crtp_base_of : std::is_base_of<remove_cvref_t<CRTP_Base<remove_cvref_t<CRTP_Derived>>>, remove_cvref_t<CRTP_Derived>> {}; 
  
 template <template<typename T> class CRTP_Base, class CRTP_Derived> 
 concept is_crtp_base_of_v = is_crtp_base_of<CRTP_Base, CRTP_Derived>::value;

Допустим, нам нужно чтобы CRTP-наследники могли сравниваться между собой:

// В теле CRTP-интерфейса Base
// Где: Derived - наследник передаваемый через шаблон

int getValue() const {
  return static_cast<Derived*>(this)->getValueImpl();
}

template<typename T>
requires is_crtp_base_of_v<Base, T>
bool operator==(T&& other) const { return getValue() == other.getValue(); }

// Предполагается что метод Derived::getValueImpl приватный/защищённый,
// чтобы лишние "потраха" не торчали наружу, и при этом CRTP-интерфейс Base дружественный
// для Derived, в ином случае (если метод public) в шаблоне оператора можно напрямую
// цеплять getValueImpl (ну или другой метод) без лишних обёрток, но если не будет указанного выше
// метода Base<Derived>::getValue то нет и гарантий, что наследник интерфейса обязан содержать
// метод getValueImpl, в следствии чего об ошибке мы узнаем только
// при инстанциировании оператора сравнения, так что лучше использовать
// подобную обёртку как контракт того что функция getValueImpl есть в наследнике, и если обёртка нигде не нужна,
// то просто бъявить её как private с модификатором inline в CRTP-интерфейсе.

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

Проводились ли замеры накладных расходов влияния виртуальных таблиц ? Ведь именно ради этого CRTP используется ?

Не только. Виртуальные вызовы невозможно оптимизировать. С CRTP возможна не только оптимизация вызовов как таковых, но и дальнейший анализ codeflow для других оптимизаций.

Пусть виртуальные методы немного медленнее, но это не причина использовать CRTP только ради быстродействия, есть ли другие ?

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

Да, именно недооцениваю, ведь цифр не приведено )
И да, кэп, оптимизация может ускорять выполнение программы )

Поясните, что значит "виртуальные вызовы невозможно оптимизировать"? Девиртуализация, существующая в современных компиляторах, получается блеф?

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

Например, помимо указанного мной в комментарии выше варианта использования трейтса на проверку CRTP-наследника is_crtp_base_of_v, можно проверить, является ли наследник одного базового CRTP-класса наследником какого-либо другого базового CRTP-класса и в зависимости от этого включить/отключить какой-нибудь метод (через requires) или же поменять логику существующего (через if constexpr). Иными словами "построить мост" в логике взаимодействия между двумя базовыми CRTP-классами одного CRTP-наследника.

Так же можно создавать базовые классы-метки, без полей и методов, то есть они не будут производить никакого мутирующего воздействия на наследников, но базовые CRTP-классы смогут отловить сам факт, того, что их наследник наследуется от класса-метки через std::is_base_of_v и за счёт этого могут так же менять логику работы. Таким образом можно писать "настраиваемое наследование".

Как видите идиома CRTP более гибкая нежели классический вариант наследования, условие одно - более или менее освоиться в работе шаблонов C++. Так же я считаю что данная идиома, отлично согласуется с наличием в C++ множественного наследования, попросту позволяя выжать из него больше.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий