Привет! Меня зовут Николай, я C++-разработчик в SimbirSoft. Это третья часть цикла статей о проектировании библиотек на примере решения геометрических задач.
В предыдущих частях статье мы разобрали классическое наследо��ание с виртуальными функциями и использование шаблонов, потом рассмотрели архитектуру на основе признаков (traits), тегов и концептов и показали, как этот подход помогает создавать расширяемые алгоритмы и снижать жёсткую связность между типами и реализациями.
В этой части мы продолжим развивать предложенную архитектуру и разберём, как она позволяет интегрировать в алгоритмы сторонние типы, контейнеры стандартной библиотеки и пользовательские структуры без изменения исходного кода алгоритмов. Мы покажем, как с помощью признаков можно адаптировать внешние классы к интерфейсу библиотеки и как организовать расширение алгоритмов собственными реализациями.
Также рассмотрим практические примеры: адаптацию стандартных контейнеров, расширение алгоритмов через частичную специализацию и добавление альтернативных реализаций. В завершение покажем, как возможности стандарта C++20 позволяют упростить архитектуру за счёт использования концептов и отказаться от части вспомогательных сущностей.
Для комфортного чтения потребуется уверенное понимание шаблонов, частичной специализации и базовых принципов обобщённого программирования в C++. Материал ориентирован на разработчиков уровня middle и выше, которые интересуются проектированием расширяемых библиотек и архитектурой современных C++-систем.
Помимо ранее перечисленных проблем, подход с использованием признаков, тегов и концептов также позволяет решить следующие задачи:
появляется возможность использовать сторонние классы в алгоритмах; однако для этого необходимо написать вспомогательный класс, через который будут получаться требуемые параметры и типы;
становится возможной работа не только с пользовательскими классами, но и с массивами и контейнерами стандартной библиотеки.

Пример
Предположим, что в программе отрезок определён следующим образом:
std::array<double, 4> line;
где элементы 1, 2 — это координаты первой точки, элементы 3, 4 — это координаты второй точки.
Для использования алгоритмов нужно написать следующие концепции и признаки:
Для отрезка:
template<typename Type> struct access_line<std::array<Type, 4>>{ using line_type = line_section; using type = Type; using type_point = std::pair<Type, Type>; using line = std::array<Type, 4>; inline static constexpr type_point start(const line &temp){ return {temp[0], temp[1]}; } inline static constexpr type_point stop(const line &temp){ return {temp[1], temp[2]}; } }; template<typename Type> struct dimension<std::array<Type, 4>>{ static constexpr size_t value(){ return 2; } }; template<typename Type> struct figure<std::array<Type, 4>>{ using type_figure = line_borders; };
Для точки:
template<typename Type> struct access_point<std::pair<Type, Type>>{ using type = Type; inline static constexpr const type &x(const std::pair<Type, Type> &temp){ return temp.first; } inline static constexpr const type &y(const std::pair<Type, Type> &temp){ return temp.second; } };
Расширение алгоритмов своими собственными реализациями под конкретные задачи, при этом использовать одну и туже функцию.

Пример 1
В алгоритме предусмотрен шаблон, позволяющий расширять реализацию. Допустим, имеются следующие файлы с реализацией алгоритма.
Файл quation_interface.h содержит:
template<typename Line> auto equation(const Line &line){ return implementation::equation(line); }
Файл equation_implementation.h содержит:
namespace dispatch { template<typename Line, typename Type, size_t N> struct equation{ static constexpr auto get(const Line &temp){ static_assert(false, "Not implemented"); } }; template<typename Line> struct equation<Line, straight_line, 2>{ static constexpr auto get(const Line &temp){ //... } }; } namespace implementation { template<typename Line> auto equation(const Line &line){ return dispatch::equation<Line, typename access_line<Line>::line_type, dimension<Line>::value()>::get(line); } }
И файл main_algorithm.h, в котором подключены все алгоритмы.
Для расширения алгоритма на трёхмерную прямую возможны два варианта. В первом случае можно реализовать перегрузку непосредственно в файле equation_interface.h. Однако если такой возможности нет, необходимо создать новый файл с реализацией и подключить его в main_algorithm.h.
Файл main_equation_implementation.h с собственной реализацией будет выглядеть следующим образом:
namespace dispatch { template<typename Line> struct equation<Line, straight_line, 3>{ static constexpr auto get(const Line &temp){ //... } }; }
main_algorithm.h будет выглядеть так:
#include "equation_interface.h" #include "main_equation_implementation.h"
Пример 2
В алгоритме отсутствуют необходимые шаблонные параметры для расширения.
Предположим, требуется использовать более эффективный алгоритм для конкретного случая, однако возможность частичной специализации класса по существующим шаблонным параметрам отсутствует.
В этом случае создадим собственный файл с интерфейсом алгоритма — main_equation_interface.h — и добавим в него следующую реализацию:
#include "equation_interface.h" //Теги для алгоритмов: struct default_algorithm final{}; struct special_algorithm final{}; template<typename Line, typename Algorithm = default_algorithm> auto equation(const Line &line, Algorithm algorithm){//Тут не обязательно добавлять в сигнатуру переменную Algorithm, т.к. есть возможность вызывать через шаблонный параметр if constexpr(std::is_same_v<Algorithm, default_algorithm>){ //При данном условии будем вызывать общий алгоритм return equation(line); } else{ //Здесь можем вызывать специализированные алгоритмы } }
В результате новая перегрузка может вызываться аналогично функциям из файла equation_interface.h.
С появлением концептов в стандарте C++20 появилась возможность сократить объём кода. Для этого для каждого класса линии создадим собственный концепт.

template<typename Object> concept c_straight_line = requires(){ typename access_line<Object>::type; requires std::same_as<typename access_line<Object>::type, std::remove_cvref_t<std::invoke_result_t<decltype(&access_line<Object>::a), Object>>>; requires std::same_as<typename access_line<Object>::type, std::remove_cvref_t<std::invoke_result_t<decltype(&access_line<Object>::b), Object>>>; }; template<typename Object> concept c_half_line = requires(Object object){ typename access_line<Object>::type_point; typename access_line<Object>::type_angle; requires std::same_as<typename access_line<Object>::type_point, std::remove_cvref_t<std::invoke_result_t<decltype(&access_line<Object>::start), Object>>>; requires std::same_as<typename access_line<Object>::type_angle, std::remove_cvref_t<std::invoke_result_t<decltype(&access_line<Object>::angle), Object>>>; }; template<typename Object> concept c_line_section = requires(Object object){ typename access_line<Object>::type_point; requires std::same_as<typename access_line<Object>::type_point, std::remove_cvref_t<std::invoke_result_t<decltype(&access_line<Object>::start), Object>>>; requires std::same_as<typename access_line<Object>::type_point, std::remove_cvref_t<std::invoke_result_t<decltype(&access_line<Object>::stop), Object>>>; };
В алгоритмах, где на вход может быть подан один из трех типов линии, создадим следующий концепт:
template<typename Object> concept c_line = c_straight_line<Object> || c_half_line<Object> || c_line_section<Object>;
В итоге можно отказаться от тегов типа линии и стратегии, поскольку выбор необходимого шаблонного класса будет осуществляться на основе концептов.
Определения классов будут выглядеть следующим образом.
Для структуры алгоритма equation:
template<typename Object, size_t N> struct equation{}; template<c_straight_line Object> struct equation<Object, 2>{...}; template<c_half_line Object> struct equation<Object, 2>{...}; template<c_line_section Object> struct equation<Object, 2>{...};
Для структуры алгоритма belongs_to_area:
template<typename Line, typename Point> struct belongs_to_area{}; template<c_straight_line Line, typename Point> struct belongs_to_area<Line, Point>{...}; template<c_half_line Line, typename Point> struct belongs_to_area<Line, Point>{...}; template<c_line_section Line, typename Point> struct belongs_to_area<Line, Point>{...};
Для структуры алгоритма value_function:
template<typename Line, typename Point, size_t N> struct value_function{}; template<c_line Line, typename Point> struct value_function<Line,Point,2>{...};
Для структуры алгоритма parallel_line:
template<typename Line, typename Type, typename ReturnLine, size_t N> struct parallel_line{}; template<c_line Line, typename Type, c_straight_line ReturnLine> struct parallel_line<Line,Type,ReturnLine,2>{...};
Для структуры алгоритма intersections:
template<typename Object1, typename Object2, typename Return> struct intersections{}; template<c_line Object1, c_line Object2, typename Return> struct intersections<Object1, Object2, Return>{...};

К недостаткам подхода через теги, признаков и концепции можно отнести:
увеличение объёма кода за счёт введения дополнительных сущностей;
возможные трудности при реализации сложных алгоритмов, требующих создания объектов типов, не входящих в шаблонные параметры функции.
Вместо заключения
Наследование (динамический полиморфизм)
Подходит для небольших и закрытых иерархий. Основной недостаток — жёсткая связность и позднее связывание: конкретные типы и методы фиксируются уже на этапе проектирования интерфейса.
Шаблоны (статический полиморфизм)
Подходят для повышения гибкости работы с типами и улучшения производительности. Основной недостаток — «утиная типизация», которая может быть небезопасной, а также сложность задания ограничений (constraints) до появления стандарта C++20.
Подход с использованием концепций, тегов и признаков
Обеспечивает максимальную гибкость, безопасность и расширяемость. Основной недостаток — высокая сложность реализации и значительный порог входа.
