Привет! Меня зовут Николай, я 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.

Подход с использованием концепций, тегов и признаков

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