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

В предыдущей статье были рассмотрены два подхода к проектированию библиотеки, а также обозначена идея использования адаптера. Остановимся на ней подробнее. Реализовать адаптер можно двумя способами:
передавать в алгоритмы непосредственно класс-адаптер;
использовать интерфейсы шаблонного класса со статич��скими методами.
При применении первого подхода работа с алгоритмами оказывается менее удобной, поскольку каждый раз требуется преобразовывать пользовательский класс в адаптер. Поэтому далее будет рассмотрен второй вариант.
Подход с использованием концепций, тегов и признаков
Сразу отметим, что данный подход позволяет устранить все недостатки предыдущих решений. Однако за это приходится платить увеличением объёма вспомогательного кода.
Для начала перепишем классы линий так, чтобы ими было удобно пользоваться в прикладном коде, а не только при вызове специализированных алгоритмов. Для большей универсальности сделаем эти классы шаблонными, хотя строго обязательным это не является.
template<typename Type> struct Point{ Type x; Type y; }; template<typename Type> class StraightLine{ public: StraightLine() = default; StraightLine(const Type &a, const Type &b, const Type &c) : a_(a), b_(b), c_(c){} const Type &a() const{ return a_; } void set_a(const Type &a){ a_ = a; } const Type &b() const{ return b_; } void set_b(const Type &b){ b_ = b; } const Type &c() const{ return c_; } void set_c(const Type &c){ c_ = c; } private: Type a_{}, b_{}, c_{}; }; template<typename Type, typename Point, typename Angle> class HalfLine{ public: HalfLine() = default; HalfLine(const Point &start, const Type &angle) : start_(start), angle_(angle){} const Point &start() const{ return start_; } void set_start(const Point &start){ start_ = start; } const Angle &angle() const{ return angle_; } void set_b(const Angle &angle){ angle_ = angle; } private: Point start_; Angle angle_; }; template<typename Type, typename Point> class LineSection{ public: LineSection() = default; LineSection(const Point &start, const Point &stop) : start_(start), stop_(stop){} const Point &start() const{ return start_; } void set_start(const Point &start){ start_ = start; } const Point &stop() const{ return stop_; } void set_stop(const Point &stop){ stop_ = stop; } private: Point start_; Point stop_; };
Теперь реализуем функцию equation не в привычном виде, а в виде структуры со статическим методом get. В данном шаблоне эта структура отсутствует, поскольку пока не требуется, однако далее она будет определена.
template<typename Line, typename LineType> struct equation{};
Первый параметр — это объект линии, второй — это тип. Определим эти типы (теги) следующим образом:
struct straight_line{}; struct half_line{}; struct line_section{};

Теперь, используя частичную специализацию (она возможна только для классов, поэтому функция equation реализована в виде структуры), реализуем алгоритмы получения уравнения линии следующим образом:
template<typename Line> struct equation<Line, straight_line>{ using type = access_line<Line>::type; static constexpr auto get(const Line &temp) -> std::array<type,3>{ return {access_line<Line>::a(temp), access_line<Line>::b(temp), access_line<Line>::c(temp)}; } }; template<typename Object> struct equation<Object, half_line, 2>{ using type = access_line<Object>::type; static constexpr auto get(const Object &temp) -> std::array<type,3>{ using point = access_line<Object>::type_point; const auto &a = -std::cos(access_line<Object>::angle(temp)); const auto &b = std::sin(access_line<Object>::angle(temp)); const auto &start = access_line<Object>::start(temp); const auto &x = access_point<point>::x(start); const auto &y = access_point<point>::y(start); return {a, b, -b * y - a * x}; } }; template<typename Object> struct equation<Object, line_section, 2>{ using type = access_line<Object>::type; static constexpr auto get(const Object &temp) -> std::array<type,3>{ using point = access_line<Object>::type_point; const auto &start = access_line<Object>::start(temp); const auto &stop = access_line<Object>::stop(temp); const auto &x_start = access_point<point>::x(start); const auto &y_start = access_point<point>::y(start); const auto &x_stop = access_point<point>::x(stop); const auto &y_stop = access_point<point>::y(stop); const auto a = y_start - y_stop; const auto b = x_stop - x_start; return {a, b, -b * y_start - a * x_start}; } };
Из реализации функций видно, что необходим общий интерфейс для получения данных из объекта. В качестве такого интерфейса будут использованы шаблонные классы access_line для линии и access_point для точки. В более общем смысле такой подход относится к признакам (traits) и концепциям.

Определим общий шаблонный класс, а затем его ча��тичные специализации:
template<typename Line> struct access_line{};
Специализация для Прямой:
template<typename Type> struct access_line<StraightLine<Type>>{ using line_type = straight_line; using type = Type; using line = StraightLine<Type>; inline static constexpr const type &a(const line &temp){ return temp.a(); } inline static constexpr const type &b(const line &temp){ return temp.b(); } inline static constexpr const type &c(const line &temp){ return temp.c(); } };
Специализация для Луча:
template<typename Type, typename Point, typename Angle> struct access_line<HalfLine<Type, Point, Angle>>{ using line_type = half_line; using type = Type; using type_point = Point; using type_angle = Angle; using line = HalfLine<Type, Point, Angle>; static constexpr const type_point &start(const line &temp){ return temp.start(); } static constexpr const type_angle &angle(const line &temp){ return temp.angle(); } };
Специализация для Отрезка:
template<typename Type, typename Point> struct access_line<LineSection<Type, Point>>{ using line_type = line_section; using type = Type; using type_point = Point; using line = LineSection<Type, Point>; inline static constexpr const type_point &start(const line &temp){ return temp.start(); } inline static constexpr const type_point &stop(const line &temp){ return temp.stop(); } };
Концепция для точки:
template<typename Point> struct access_point{}; template<typename Type> struct access_point<Point<Type>>{ using type = Type; inline static constexpr const type &x(const Point<Type> &temp){ return temp.x; } inline static constexpr const type &y(const Point<Type> &temp){ return temp.y; } };
Отступление
Чтобы не увеличивать объём кода, в класс access_line были добавлены все необходимые методы и типы. Однако на практике обычно применяют другой подход.
Теперь можно реализовать полноценную функцию equation:
constexpr auto equation(const Line &line){ return dispatch::equation<Line, typename access_line<Line>::line_type>::get(line); }
dispatch – это пространство имен в котором находятся структуры equation.
Для следующей функции parallelLine добавил следующую шаблонную структуру:
template<typename Line, typename Type, typename ReturnLine, size_t N> struct parallel_line{};
где N — это размерность пространства. Если будет расширение до больших размерностей, то такой параметр необходим.
Частичная специализация будет выглядеть следующим образом:
template<typename Line, typename Type, typename ReturnLine> struct parallel_line<Line,Type,ReturnLine,2>{ static constexpr ReturnLine get(const Line &temp, const Type &distance){ auto [a,b,c] = equation<Line, typename access_line<Line>::line_type>::get(temp); return {a, b, c + distance * std::sqrt(a * a + b * b)}; } };
Определение dimension для линий будет выглядеть так:
template<typename Line> struct dimension{}; template<typename Type> struct dimension<StraightLine<Type>>{ static constexpr size_t value(){ return 2; } }; template<typename Type, template<class> class CPoint, typename Angle> struct dimension<HalfLine<Type, CPoint, Angle>>{ static constexpr size_t value(){ return 2; } }; template<typename Type, template<class> class CPoint> struct dimension<LineSection<Type, CPoint>>{ static constexpr size_t value(){ return 2; } };
В итоге реализация parallelLine:
template<typename Line, typename Type, typename ReturnLine = StraightLine<typename access_line<Line>::type>> constexpr ReturnLine parallelLine(const Line &line, Type distance){ return dispatch::parallel_line<Line, Type, ReturnLine, dimension<Line>::value()>::get(line, distance); }
Отступление
С учётом добавления размерности функция equation позволяет с минимальными изменениями вычислять уравнение прямой в пространстве. Определения соответствующих структур будут выглядеть следующим образом:
template<typename Object, typename ObjectType, size_t N> struct equation{}; template<typename Object> struct equation<Object, straight_line, 3>{ //... }; template<typename Object> struct equation<Object, half_line, 3>{ //... }; template<typename Object> struct equation<Object, line_section, 3>{ //... }; template<typename Line> constexpr auto equation(const Line &line){ return dispatch::equation<Line, typename access_line<Line>::line_type, dimension<Line>::value()>::get(line); }
Для реализации функции isPointLine в целях удобства необходимо создать две структуры: первая будет вычислять значение уравнения для заданной точки, вторая — определять, принадлежит ли точка линии.
Структуры для вычисления значения уравнения:
template<typename Line, typename Point, size_t N> struct value_function{}; template<typename Line, typename Point> struct value_function<Line,Point,2>{ using type = access_line<Line>::type; static constexpr bool get(const Line &temp, const Point &point){ auto [a,b,c] = dispatch::equation<Line, typename access_line<Line>::line_type, dimension<Line>::value()>::get(temp); const auto &x = access_point<Point>::x(point); const auto &y = access_point<Point>::y(point); return a * x + b * y + c; } };
Структуры определения попадания точки в область:
template<typename Line, typename Point, typename LineType> struct belongs_to_area{}; template<typename Line, typename Point> struct belongs_to_area<Line, Point, straight_line>{ static constexpr bool get(const Line &temp, const Point &point){ return true; } }; template<typename Line, typename Point> struct belongs_to_area<Line, Point, half_line>{ static constexpr bool get(const Line &temp, const Point &point){ using Type = access_line<Line>::type_angle; using point_line = access_line<Line>::type_point; const auto &line_angle = access_line<Line>::angle(temp); const auto &start = access_line<Line>::start(temp); const auto &x_start = access_point<point_line>::x(start); const auto &y_start = access_point<point_line>::y(start); const auto &x = access_point<Point>::x(point); const auto &y = access_point<Point>::y(point); Type angle{}; double d = std::numbers::pi / 2.; if((std::exchange(angle, angle + d) <= line_angle) && (line_angle < angle)){ return (x_start <= x) && (y_start <= y); } if((std::exchange(angle, angle + d) <= line_angle) && (line_angle < angle)){ return (x_start <= x) && (y_start >= y); } if((std::exchange(angle, angle + d) <= line_angle) && (line_angle < angle)){ return (x_start >= x) && (y_start >= y); } if((std::exchange(angle, angle + d) <= line_angle) && (line_angle < angle)){ return (x_start >= x) && (y_start <= y); } return false; } }; template<typename Line, typename Point> struct belongs_to_area<Line, Point, line_section>{ static constexpr bool get(const Line &temp, const Point &point){ using point_line = access_line<Line>::type_point; const auto &start = access_line<Line>::start(temp); const auto &stop = access_line<Line>::stop(temp); const auto &x_start = access_point<point_line>::x(start); const auto &y_start = access_point<point_line>::y(start); const auto &x_stop = access_point<point_line>::x(stop); const auto &y_stop = access_point<point_line>::y(stop); const auto &x = access_point<Point>::x(point); const auto &y = access_point<Point>::y(point); bool flag_x = (x_start < x_stop) ? ((x_start <= x) && (x <= x_stop)) : ((x_stop <= x) && (x <= x_start)); bool flag_y = (y_start < y_stop) ? ((y_start <= y) && (y <= y_stop)) : ((y_stop <= y) && (y <= y_start)); return flag_x && flag_y; } };
В итоге реализация isPointLine примет вид:
template<typename Line, typename Point> constexpr bool isPointLine(const Line &line, const Point &point){ using type = access_line<Line>::type; return (dispatch::value_function<Line, Point, dimension<Line>::value()>::get(line, point) == type{}) && dispatch::belongs_to_area<Line, Point, typename access_line<Line>::line_type>::get(line, point); }
Отступление
Можно заметить, что функция isPointLine не реализована в виде структуры (в целях сокращения объёма кода), однако при необходимости это легко сделать с помощью частичной специализации.
В результате для прямой отсутствует необходимость в реализации частичной специализации belongs_to_area, поскольку для определения принадлежности точки прямой достаточно одного условия.
Реализация функции intersectionsLine будет несколько сложнее, так как требуется определить, в каких случаях необходимо использовать функцию belongs_to_area, а также предусмотреть возможность расширения алгоритма.

Для этого введём два тега. Первый тег — тип фигуры: линия без ограничений и линия с границами. В дальнейшем возможно добавление других специализаций, например, для дуги.
struct line{}; //Прямая struct line_borders{}; //Линия с хотя бы одной границы template<typename Object> struct figure{}; template<typename Type> struct figure<StraightLine<Type>>{ using type_figure = line; }; template<typename Type, template<class> class CPoint, typename Angle> struct figure<HalfLine<Type, CPoint, Angle>>{ using type_figure = line_borders; }; template<typename Type, template<class> class CPoint> struct figure<LineSection<Type, CPoint>>{ using type_figure = line_borders; };
Второй тег это стратегия расчета пересечения фигур
//Два объекта являются прямыми struct strategy_two_straight_line{}; //Хотя бы один объект является прямой, а второй линией с границей struct strategy_straight_line_to_line{}; //Два объекта являются линией с границей struct strategy_line_to_line{}; template<typename TypeObject1, typename TypeObject2> struct strategy{}; template<>struct strategy<line, line>{ using type_strategy = strategy_two_straight_line; }; template<> struct strategy<line, line_borders>{ using type_strategy = strategy_straight_line_to_line; }; template<> struct strategy<line_borders, line>{ using type_strategy = strategy_straight_line_to_line; }; template<> struct strategy<line_borders, line_borders>{ using type_strategy = strategy_line_to_line; };
Частичная специализация пересечения линий для каждой стратегии будет выглядеть следующим образом:
template<typename Object1, typename Object2, typename Return, typename Strategy> struct intersections{}; template<typename Object1, typename Object2, typename Return> struct intersections<Object1, Object2, Return, strategy_two_straight_line>{ static constexpr std::optional<Return> get(const Object1 &object1, const Object2 &object2){ auto [a1,b1,c1] = equation<Object1, typename access_line<Object1>::line_type, dimension<Object1>::value()>::get(object1); auto [a2,b2,c2] = equation<Object2, typename access_line<Object2>::line_type, dimension<Object2>::value()>::get(object2); if(const auto d = a1 * b2 - a2 * b1; d != 0){ return Return{(b1 * c2 - b2 * c1) / d, (c1 * c2 - a2 * a1) / d}; } return std::nullopt; } }; template<typename Object1, typename Object2, typename Return> struct intersections<Object1, Object2, Return, strategy_straight_line_to_line>{ static constexpr std::optional<Return> get(const Object1 &object1, const Object2 &object2){ using line_type1 = access_line<Object1>::line_type; using line_type2 = access_line<Object2>::line_type; auto point = intersections<Object1, Object2, Return, strategy_two_straight_line>::get(object1, object2); if(point.has_value()){ if constexpr(std::is_same_v<line_type1, straight_line>){ return dispatch::belongs_to_area<Object2, Return, line_type2>::get(object2, point.value()) ? point : std::nullopt; } else{ return dispatch::belongs_to_area<Object1, Return, line_type1>::get(object1, point.value()) ? point : std::nullopt; } } return std::nullopt; } }; template<typename Object1, typename Object2, typename Return> struct intersections<Object1, Object2, Return, strategy_line_to_line>{ static constexpr std::optional<Return> get(const Object1 &object1, const Object2 &object2){ using line_type1 = access_line<Object1>::line_type; using line_type2 = access_line<Object2>::line_type; auto point = intersections<Object1, Object2, Return, strategy_two_straight_line>::get(object1, object2); if(point.has_value()){ if(dispatch::belongs_to_area<Object1, Return, line_type1>::get(object1, point.value()) && dispatch::belongs_to_area<Object2, Return, line_type2>::get(object2, point.value())){ return point; } } return std::nullopt; } };
Реализация intersectionsLine будет выглядеть так:
template<typename Line1, typename Line2, typename Return = Point<typename access_line<Line1>::type>> constexpr std::optional<Return> intersectionsLine(const Line1 &line1, const Line2 &line2){ using strategy = dispatch::strategy<typename figure<Line1>::type_figure, typename figure<Line2>::type_figure>::type_strategy; return dispatch::intersections<Line1, Line2, Return, strategy>::get(line1, line2); }

Во второй части была рассмотрена реализация третьего подхода к проектированию библиотек с использованием тегов, признаков и концептов. Другой вариант реализации данного подхода — непосредственно определять необходимые методы и признаки внутри самого класса (аналогично созданию итератора).
В следующей части будет рассмотрено, какие проблемы решает данный подход и как можно сократить объём кода, начиная с 20-го стандарта C++.
