Комментарии 63
В общем, хочу какой-то реалистичный пример использования того, чего вы тут натворили.)
UPD: Перечитал предпоследний абзац. Это что-то типа boost::spirit получилось что ли?
сформировали «дерево типов»
И сразу вопрос — а зачем? Ответ в вопросе: если в задаче требуется создание дерева и операции с ним на этапе компиляции, то вот и пример) Описан инструментарий, о более реальном(ых) же примерах его применения сказано несколько слов в соответствующем разделе. Вы правы, для описаной вами задачи макроса действительно будет достаточно. Одной из целей статьи, однако, было показать, как можно обходиться без макросов в довольно нетривиальных ситуациях.
Про Spirit — да, в каком-то смысле, вы правы: упрощенно, финальная задача — получить (но на этапе компиляции) некоторый предметно-ориентированный автомат.
Существует несколько алгоритмов прямой конвертации регулярного выражения в ДКА (детерминированный конечный автомат), его распознающий, часть которых оперирует т.н. синтаксическим деревом, которое по своей природе «не более чем» бинарное. Таким образом, бинарное дерево поиска времени компиляции — составная часть
Если под «синтаксическим деревом» вы имеете ввиду АСД представляющее выражение регулярное выражение, то это очень странное утверждение. Потому как АСД не является деревом поиска (даже если оно оказалось бинарным). Так что не очень понятно, каким образом у вас получается, что бинарное дерево поиска — это составная часть?
И другой вопрос, вы пробовали замерять время компиляции на разных примерах использования все этого «чуда»? Если да, то, пожалуйста, поделитесь результатами.
Нет, готовых измерений производительности нет, но вопрос интересный, доп. флагами компилятора можно включить вывод времени фаз компиляции. Сделаю замеры и обновлю комментарий/эпилог статьи.
Не понравилось изложение материала. Сумбурно, неточно, неполно.
Сложность такой реализации search — опять-таки O(n) инстанцирований, глубина — h (высота дерева). «Стоп!» — воскликнет удивленный читатель, — «а как же логарифмическая сложность поиска и всё такое?»
Во-первых, для логарифмической сложности требуется сбалансированность дерева. Здесь про это нет ни слова.
Во-вторых, остановка ненужных конкретизаций шаблонов чрезвычайно важна.
камни в сторону std::conditional
Летят совершенно напрасно.
std::conditional недостаточно для остановки рекурсии
Это не так. std::conditional
здесь не при чём.
Просто нужно конкретизировать шаблон не внутри, а снаружи.
Неверно:
std::conditional
<
condition,
expression1::type,
expression2::type
>
::type;
Верно:
std::conditional
<
condition,
expression1,
expression2
>
::type::type;
это задача для более глубокого исследования и в данном случае будет являться преждевременной оптимизацией
Это не вопрос для исследования, и это не оптимизация. Это необходимое условие.
Без остановки прохода по ненужным веткам и без сбалансированности такое "дерево поиска" — внимание — хуже простого линейного поиска.
в стандарте C++14 планируется ввести
Так 17-й же уже на подходе.
Во-первых, для логарифмической сложности требуется сбалансированность дереваСтатья не претендует на звание исчерпывающего руководства по алгоритмам и структурам данных (она и так получилась весьма объёмной даже со спойлерами), поэтому я намеренно опускал некоторые моменты: сведующий читатель поймёт, о чём речь, остальные же получат повод загуглить, если эти моменты действительно им интересны. И да, корень «логарифм» встречается только в одном месте в тексте, а сырое бинарное дерево не имеет балансировки (и необходимость в ней, вообще говоря, зависит от решаемой задачи*).
Просто нужно конкретизировать шаблон не внутри, а снаружиВот это очень интересное замечание, спасибо, я обращу на конкретизацию особое внимание. Вы хотите сказать, что такой подход автоматически отключит инстанцирование
search
или insert
на неверном пути? Пока я в этом сомневаюсь.Без остановки прохода по ненужным веткам и без сбалансированности такое «дерево поиска» хуже простого линейного поиска.О сбалансированности было сказано ранее, об отключениях — согласитесь, без сбалансированности оно не имеет особого смысла :) Надо сказать, слово «поиска» в определении немного сбивает с толку: кому-то может показаться, что описанная структура — это готовый мета-
std::map
, однако это именно бинарное дерево (не более) с минимально необходимым интерфейсом.Так 17-й же уже на подходе.Справедливо. У меня до сих пор присутствует некоторая инертность относительно новых и грядущих стандартов — политика партии требует стабильности компиляции.
Пока я в этом сомневаюсь.
Ну так попробуйте и убедитесь.
согласитесь, без сбалансированности оно не имеет особого смысла
Не соглашусь. Проходить всегда по всему дереву всё равно хуже, чем делать это иногда.
Это сбалансированность не имеет смысла до тех пор, пока мы всегда ходим по всем веткам.
Можете привести пример. когда редактирования AST на этапе компиляции может быть полезно или необходимо? Правда не понимаю.
А так да, действительно, каждый день просто сажусь и описываю AST в compile-time. Вот скоро порт
boost::spirit
на язык шаблонов закончу, вот тесты на static_assert
дописываю, жаль, компилируются по две недели…Возможность редактирования AST, де факто, уже есть в С++. Очевидно, шаблоны позволяют подменять узлы синтаксического дерева. В описании шаблонного типа мы помечаем узлы для подмены с помощью аргументов, после чего выполняем подстановку типов или констант в указанные места. Однако когда речь заходит о том, чтобы выполнять подобную подмену на основании каких-нибудь свойств существующего дерева (то есть работа на стыке рефлексии и шаблонов) — начинаются костыли с type traits, рекурсивными списками типов и, в сложных случаях, с макросами.
От этого плохо всем. Плохо компилятору — он мучается с рекурсивными шаблонами и затягивает время сборки кода. Плохо парсеру — мы теряем возможность использования фишек IDE (автоподстановка, индексы типов). Плохо программисту — он вынужден ковыряться в жутких функциональных конструкциях без получения внятных сообщений об ошибках. Плохо экосистеме в целом — функционально-ориентированные шаблоны вызывают оторопь у неофитов С++.
Редактирование AST позволило бы навести порядок, вынеся кодогенерацию из кода основной программы в отдельную категорию «поведения времени компиляции» программы.
Я не думал пока о конкретных языковых конструкциях, с помощью которых можно было бы редактировать AST. Однако в процессе написания комментария набросал некоторые имеющиеся сумасшедшие мысли:
Любители ФП могут меня запинать — но я не устану повторять: людям неудобно мыслить рекурсивными категориями. Мы с детства читаем текст последовательно, не сохраняя в голове точки возврата (например, текст в скобках (даже если он выстроен логично (в рамках выбранной области) и без рекурсивных отсылок (хотя сложно представить себе рекурсию в тексте)) воспринимается тяжело).
Если бы логика генерации шаблонных классов описывались аналогично макросным проверкам — имхо, шаблоны воспринимались бы проще. Например:
template< typename T_Type >
class Array {
private:
@if (T_Type is bool) [[ char *_bitarray; ]]
@else [[T_Type *_array; ]]
public:
@if (T_Type.isScalar) [[ T_Type ]] @else [[T_Type &]]
operator [](size_t inIndex) {
@if(T_Type is bool) [[
return _bitarray[inIndex/8*sizeof(char)] & (1 << (inIndex%8*sizeof(char)));
]]
@else [[
return _array[inIndex];
]]
}
}
Примечание: Проверки кодогенерации, описанные здесь, выполняются на этапе парсинга/компиляции кода; по-умолчанию после выполнения кодогенерации никакие необходимые для кодогенерации данные не сохраняются.
2. Обработка списков типов.
template< typename ... T_List >
void print(T_List ... inArgs) {
@for (Variable : inArgs) [[
std::cout << Variable.name;
std::cout << Variable.type.name;
@if (Variable.type.isScalar) [[
std::cout << Varuable.value;
]] @else [[
std::cout << Varuable.value@.toString();
]]
std::cout << std::endl;
]]
}
Обращаю внимание противников RTII — генерация специализаций подобной функции не подразумевает использование RTII. Рефлексийные сведения нужны исключительно на этапе парсинга/компиляции кода. И, опять-таки, мы не засоряем код утилитными типами-специализациями type_traits.
3. Генерация имён типов.
@function VectorsGenerator(Type inType, Int inArgumentsCount) [[
struct Vector##inType.name.capitalized##inArgumentsCount {
@if (inArgumentsCount == 1) [[
Type x;
]] @elseif (inArgumentsCount == 2) [[
Type y;
]] @elseif (inArgumentsCount == 3) [[
Type z;
]] @else [[
@COMPILE_ERROR("Incorrect vector generating arguments count")
]]
};
]]
@decl VectorsGenerator(int, 2);
@decl VectorsGenerator(float, 3);
int main() {
VectorInt2 theIntVector;
VectorFloat3 theFloatVector;
@decl VectorsGenerator(double, 1) theDoubleOneDementional;
}
Подобное использование кодогенерации заменило бы собой лексически/синтаксически нейтральные макросы.
4. Напоследок — несколько сумбурных мыслей по поводу кодогенерации:
2. С описанным подходом достаточно неплохо совместились бы концепты. Фактически, они описывают что-то вроде интерфейса типа, описывают тип типа.
Сейчас всё это выглядит очень неуклюже. Как я указал с самого начала — данные мысли формулировались на ходу, в процессе написания комментария (извиняюсь, кстати, за его размер). Чтобы язык редактирования AST стал лучше — нужны усилия умных людей, намного умнее меня.
Заканчивая, перечислю почему мне кажется, что не-функционально-ориентированная кодогенерация была бы хороша:
0. Она сделала бы написание и чтение кода, требующего кодогенерации, проще.
1. Она вытеснила бы лексически/синтаксически нейтральные макросы.
2. Описанная кодогенерация могла бы упростить оптимизацию кода. На замену утилитным типам type_traits (которые регистрируются компилятором как типы, со всеми вытекающими последствиями) пришли бы инструкции, априори существующие лишь на этапе кодогенерации и созданные исключительно для решения задач котогенерации.
3. Описанная кодогенерация могла пригодиться при введении концепции модулей в С++. Код модулей с элементами кодогенерации мог бы сохраняться в виде «промежуточное AST + логика кодогенерации для формирования кода на этапе сборки приложения/динамической библиотеки».
P.S.: Просьба не сильно пинать. Возможно, тут я написал далеко не самые умные вещи. Однако, во-первых, иногда неумные вещи могут порождать осмысленные дискуссии. Во-вторых, мысль о необходимости доступа к AST завладела моим сознанием не на ровном месте. Будучи не оформленной окончательно, она сформировалась после прочтения интервью Эрика Ниблера, а также мыслей некоторых других умных людей (ссылка на мой комментарий к статье о судьбах С++: в комментарии приведена целевая цитата Гора Нишанова из статьи).
P.P.S.: Ещё раз приношу извинение за большой объём комментария.
Спасибо за развернутый ответ. Ещё такой еретический вопрос: если мы пытаемся реализовать на языке конценцепцию, которая, судя по синтаксису и удобочитаемости, чужда этому языку, то почему не добавить кодогенератор на скриптовом языке как pre-build? А про сборку типов из классов и методов… это похожа на гибрид виртуальной таблицы и описания железа с помощью баз данных
почему не добавить кодогенератор на скриптовом языке как pre-build
Подозреваю, что это был сарказм — однако подобная реализация тоже возможна.
По поводу чуждости языку — я согласен с тем, что текущий вариант кодогенерации выглядит уродливо. Однако если показать некоторые многоэтажные шаблонные конструкции программисту Java или C# — они также воспримут это как извращение. Я проверял. На мой взгляд, их реакция обоснована — причём не только непривычкой создания кода на языке С++.
это похожа на гибрид виртуальной таблицы
Отчасти. В динамических языках в классы можно включать как поля (ivars), так и методы (например, первая ссылка для java). В С++ это может происходить на этапе компиляции. И, следует заметить, данную фичу я описал как возможную — далеко не факт что она будет реально нужна.
описани[е] железа с помощью баз данных
Как я понял, к кодогенерации статья относится в том смысле, что в случае с кодогенерацией мы описываем некий каркас для класса, который потом расширяем определённым образом. Если так — это несколько отдалённая ассоциация, но да. В чём-то похоже.
Эх… Вспомнил весь этот мой код — аж ностальгия взяла. Спасибо за ссылку.
Насколько я понял, цель описанной кодогенерации — автоматическое создание сущностей типов по описанию свойств (полей) и умений (методов) и последующее использование этого boilerplate code в обычном ООП, т.е. пишем код в уверенности, что всё необходимое уже задекларировано/описано, а в определенных случаях создано и инициализировано. Вы эту мысль пытались донести, или я опять нафантазировал?
Подозреваю, что это был сарказм — однако подобная реализация тоже возможна.
Нет, я серьёзно. Пытался понять, как писать одновременно переносимый и быстрый код для разных микроконтроллеров. варианты: жуткие макросы и внешний кодогенератор на скриптоязыке. Что так, что так получается уродливо и тяжко в поддержке
объектно-ориентированное API которых с разной степенью точности описывал бы поведение соответствующих устройств.
Да, удобно, я такое решение тоже видел: описание устройств в micro-manager
Вы эту мысль пытались донести, или я опять нафантазировал?
Не совсем. Цель описанной кодогенерации — иметь возможность использовать нефункциональные подходы при описании сложных, зависящих от контекста шаблонных типов.
Что так, что так получается уродливо и тяжко в поддержке
Да. Я сразу признал что сейчас это выглядит нелепо. Однако моя мысль заключалась в том, что кодогенераторы могут быть ближе к выражению семантики, которая сейчас описывается с помощью шаблонов… Собственно, если почитать существующие в данный момент статьи о рефлексии — достаточно часто люди приходят к идее использования third party кодогенераторов на базе всяких clang-ов.
Да, удобно, я такое решение тоже видел
Спасибо за ссылку. Интересно почитать… На мой взгляд, подход с описанием железа через дешёвые абстракции очень естественен для такого системного языка как С++.
Пожалуй, одно из основных преимуществ нефункциональной кодогенерации, которое я забыл упомянуть — такую кодогенерацию легче отлаживать. Можно ставить компайл-брейкпоинты на логику кодогенерации и пошагово отслеживать процесс модификации AST. Конечно, важно разумно определить точки входа в блоки кодогенерации и порядок их исполнения — но, в целом, это будет всяко лучше чем ломать глаза о десятистраничные скобочные сообщения об ошибке.
P.S.: На одной плюсовой тусовке, помню, кто-то хвастался тем, что дампил шаблонные ошибки компиляции в файл и парсил файл дополнительной тулзой. Подчеркну — хвастался. На мой взгляд, это говорит о достаточно нездоровой атмосфере в сообществе языка.
Спрошу ещё — вы не встречали, случайно, каких-нибудь хороших статей/обзоров с примерами описанного использования этих новых фич языка? Любопытно было бы почитать. Если прям сходу нет — то ладно. Сам нагуглю.
Кроме автоматической генерации имен классов. К черту её, им[х]о
Пожалуй, соглашусь. Variatic template отменяют необходимость в данной фиче для, например, функторов под разное количество аргументов (как это было в старом fastdelegate, созданном без поддержки С++11), а предложенный мною кейс — это вообще пережиток С, где для имитации перегрузки функций нужно было выполнять их декорирование вручную.
Ещё посмотрите на
constexpr if
— фича C++17. Вообще, по мощности C++17 должен получиться тортом. Ждём и желаем удачи (и душевных сил) разработчикам компиляторов!constexpr if
. В описании есть несколько важных моментов, и самыми вкусными являются эти:The return statements in a discarded statement do not participate in function return type deduction.
...if condition is not value-dependent after instantiation, the discarded statement is not instantiated when the enclosing template is instantiated.т.е.
constexpr
функции отныне действительно смогут «конструировать» типы и останавливать рекурсию по типам (с некоторыми оговорками). Рис возвращается в плов.void func (auto arg) {
if constexpr(requires { cout << arg; })
cout << arg << endl;
else
cout << "(obj of type " << typeid(arg).name() << ")" << endl;
}
Вы ограничены C++11? Если да, то почему? В C++14 многое можно сделать без шаблонов в constexpr
.
constexpr
-функции всё-таки не способны создавать новые типы (в зависимости от значений аргументов), а если применять вывод возвращаемого типа из типов аргументов через auto
a la C++14, то задача скатывается к старым добрым шаблонам.Все алгоритмы в статье возвращают именно типы, о наделении же узлов данными-членами сказано несколько слов в Применении.
Не согласен с предыдущим комментатором. Такого качественного материала, который будет понятен даже минимально сведущему в шаблонах C++ и метапрограммированию человеку, давненько уже не было на просторах Хабра. Особенно порадовала внимательность к «мелочам», как то: адекватное форматирование кода и последовательность изложения.
Если настоящая статья вызовет интерес и найдёт своего читателя, я с удовольствием продолжу тему метапрограммирования (и, возможно, не только) в следующих публикациях.
O(nn) шаблонов этому человеку!
Очень грамотно и интересно написано, буду ждать продолжения
Если даже разработанная реализация никогда явно никому не пригодится (кроме инсайдеров нашей команды), но идеи и подходы заинтересуют энтузиастов и помогут в изучении темы или реализации собственных инструментов и идей, то задача статьи будет выполнена.
Балансировка — интересная задача, и, в принципе, вполне решаемая уже на описанном уровне абстракций, реализация вращений будет похожа идеей на
insert
. Не знаю, насколько оправдана она будет (как упражнение — вполне): там, где важна балансировка, нужно смотреть уже в сторону АВЛ или красно-чёрных деревьев, реализацию которых пока оставим как домашнее упражнение заинтересованным читателям :)Более перспективный вариант, ИМХО, просто взять чисто функциональное красно-черное дерево, так как чисто функциональные реализации деревьев поиска имеют больше шансов на удачное переложение на шаблоны C++ и не требуют кроме сравнения никаких дополнительных параметров.
PS: я сомневаюсь в конструктивности нашей дискуссии
Точно так же, как решение любой более простой задачи в какой-либо области помогает набраться опыта для решения более сложной.
Это общий принцип обучения: «сначала изучи что-то более простое, а потом уже более сложное в той же области», и именно из этого общего принципа я и вывожу необходимость для себя сначала потренироваться на куда более простом в реализации для compile-time Treap (да что там писать-то, merge и split, вот и все), и уже потом реализовывать более сложный алгоритм.
Вы могли заметить, что этот же принцип используется повсеместно в технологиях обучения, как в рамках computer science (например, знание стека не нужно для реализации кучи, но его все равно дают раньше), так и вне его.
Да, если бы я уже имел большой опыт реализации compile-time алгоритмов, не было бы ни малейшего смысла в предварительной реализации Treap, но у меня такого опыта почти нет.
Ну это, конечно, если копать глубже, чем очевидный ответ «все равно в стол пишу, вот и выбрал алгоритм, который мне больше нравится»
То что в курсах по алгоритмам раньше дают стек чем кучу, не значит, что стек чем-то полезен для изучения кучи. В противном случае получится, что алфавит, который дают еще раньше, полезен для изучения стека.
А вот то, что в курсах рассказывают про дерево поиска раньше чем про сбалансированные деревья поиска, вот это уже логичный пример. Очевидно, чтобы сделать сбалансированное дерево поиска нужно знать, что такое дерево поиска впринципе — более простая вещь, необходимая для понимая, дается раньше. Аналогично про массивы и свзные списки рассказывают раньше чем про стек и очередь, потому что первые являются средствами реализации последних. Обратите внимание, в моих примерах польза конкретна, в ваших нет.
Касательно более простого в compile-time Treap-а, то вы не имея опыта реализации алгоритмов в compille-time утверждаете это на каком основании? На основании того, что не в compile-time Treap реализовать легче? Так связный список реализовать будет еще легче.
- Описанное в статье дерево может быть сбалансировано очень простым приёмом (однако сложность O(n2) при конструировании с пустого): добавляем элементы «как попало» => делаем симметричный обход (сортируем) => сортированный кортеж разбираем делением пополам (в новое дерево кладём серединный элемент, далее середины середин и т.д.)
- Генератор псевдослучайных чисел можно соорудить и на этапе компиляции (только надо пробрасывать seed для всех последующих «вызовов»). Есть ещё крайне интересная идея: использовать этот подход для генерации псевдослучайной последовательности (ОПАСНОСТЕ: по ссылке разрыв шаблонов во всех смыслах).
Касательно вашего первого варианта балансировки — то он вряд ли может считаться за балансировку дерева. Зачем строить дерево, потом отсортированный список, потом опять дерево, если можно изначально построить список и посортировать его и сделать из него дерево (строить список легко, сортировать тоже, плюс не нужна операция вставки в дерево).
Если под генератором вы имеете ввиду что-то подобного вида:
static const int M = ...;
static const int C = ...;
template <int N, int seed>
struct Random {
static const int value = Random<N - 1, seed>::value * M + C;
};
template <int seed>
struct Random<0, seed> {
static const int value = seed * M + C;
};
Или вроде:
template <int V>
struct Random {
static const int M = ...;
static const int C = ...;
static const int value = V * M + C;
typedef struct Random<value> next;
};
То вам нужно хранить новую версию генератора после того как он «сгенерирует» значение.
Касательно статьи, которую вы привели, то не очень понятно как из этого сделать генератор. По сути, функция из статьи возвращает только последовательность вида false, true, true, true, ...., и так далее. Как заставить значение поменяться обратно с true на false не понятно (и мы еще не рассматриваем вопрос о том, когда нужно поменять значение назад). Т. е. не понятно как из этого получить хотя бы что-то напоминающее случайную последовательность бит.
Касательно статьи, которую я привел: ознакомьтесь с ней, пожалуйста, прежде, чем делать выводы (+ещё лучше сразу с источником перевода, это цикл из 3-х статей). Техника, описанная в них, позволяет реализовать глобальный счётчик времени компиляции как минимум. Применение его (несть числа способам) — дело техники.
Касательно статьи, не надо тыкать в меня пальцами и говорить, что я с ней не ознакомился. Вообще я ее прочитал от начала и до конца и не один раз. Конкретно в той статье, на которую вы дали ссылку, описывается «переменная», которую можно изменить только один раз (вы можете сами попробовать код и убедиться в этом). Заглянув в следующую статью цикла, вы можете заметить, что автор сам пишет ровно то же самое, так что вы поторопились с выводами.
Но да не будем на этом останавливаться, посмотрим что происходит далее. Автор для того, чтобы сделать счетчик с N значениями использует N-1 такую «переменную». Т. е. фактически счетчик построенный с помощью такой идеи не отличается по «сложности» (мы же в данном случае измеряем сложность количеством инстанцирований) от генераторов, которые я привел вам выше, а коли так, то не понятно, зачем вообще это делать используя constexpr функции, если генераторы выше просто проще?
По сложности не отличается, но отличается в удобстве применения. Написать
template<..., counter::next()> func{...};
проще (и логичнее), чем помнить пробрасываемый генератор. Следуя вашей логике вообще можно заранее руками написать всю последовательность «случайных» чисел.Добавлю в TODO мини-задачу реализации compile-time генератора псевдослучайных чисел. Challenge accepted. Закончу — оставлю здесь ссылку.
Касательно сложности и логичности, я, лично, не вижу большой разницы между числом в качестве шаблонного параметра и типом, который хранит число, как не вижу большой разницы между counter::next() и Random::next. Что-то вам все равно придется хранить и передавать.
Не знаю какой моей логике вы следуете, но я нигде не утвеждал, что можно заранее написать всю последовательность. Более того я привел пример того как ее можно сгенерировать, а не описывать руками заранее.
Не понял где вы в моем сообщении увидели какой-то challenge, но удачно вам поразвлекаться.
не вижу большой разницы между counter::next() и Random::next. Что-то вам все равно придется хранить и передавать
Более того я привел пример того как ее можно сгенерироватьКак раз не придётся. То, что вы описали, просто не будет работать: вам либо придётся руками считать вызовы
Random<...>
, либо подсовывать предыдущие псевдонимы в параметр шаблона. Упомянутые статьи как раз и описывают реализацию счётчика, способного инкрементироваться без посторонней помощи.удачно вам поразвлекатьсяСпасибо! Задача действительно интересная.
Естественно, кроме числа/типа в каждом узле, нужно будет еще сохранить Random::next, но в единственном экземпляре на все дерево. Нельзя это назвать такой уж существенной сложностью, особенно если учесть реализацию альтернативы (здесь я имею ввиду проблемы с некоторыми компиляторами, переносимость на разные версии C++, плюс некоторое легкое допиливание, чтобы можно было создавать несколько независимых счетчиков/генераторов + удобство использоввания допиленной версии под вопросом).
ИМХО, гораздо более «страшным» мне видится виртуально-интерфейсный фарш, бездонные иерархии и километровые определения приезжих в C++ из C#/Java, обычно и пышущих ненавистью к углоскобочному миру.
Практика метапрограммирования на C++: бинарное дерево поиска на этапе компиляции