- С++ Шаблоны с переменным числом параметров
- Шаблоны с многоточием и Variadic
- Синтаксис
- Дополнительные сведения о положении многоточия
- Пример
- Выходные данные
- Variadic templates в C++0x
- Простые варианты использования
- Более сложные случаи
- Ещё некоторое количество хитростей
- Использование C++ и шаблонов с переменным количеством аргументов при программировании микроконтроллеров
- ARM с ядром Cortex Mx (на примере STM32F10x)
- Интернет статьи, использованные при написании заметки
- Статьи на Xабре, побудившие меня всё-таки написать эту заметку
- C++11 variadic templates и длинная арифметика на этапе компиляции
- Parameter packs
- Длинная арифметика
- Что ещё необходимо сказать
С++ Шаблоны с переменным числом параметров
В шаблонах с переменным числом аргументов многоточие используется двумя способами. слева от имени параметра оно означает пакет параметров, а справа от имени параметра оно служит для развертывания пакетов параметров в отдельные имена.
В приведенном выше примере параметр Arguments означает пакет параметров. При инстанцировании класс classname может принимать переменное число аргументов, как показано в следующих примерах:
Кроме того, определение шаблонного класса с переменным числом аргументов может устанавливать требование, что должен быть передан по меньшей мере один параметр:
Ниже представлен простейший пример синтаксиса для определения шаблонной функции с переменным числом аргументов.
Далее пакет параметров Arguments развертывается для использования, что бы показать что функция принимает переменное число аргументов.
Возможны и другие формы синтаксиса шаблонной функции с переменным количеством аргументов:
Шаблонные функции с переменным числом аргументов (как и аналогичные шаблонные классы) также могут устанавливать требование о том, что должен быть передан по меньшей мере один параметр.
В шаблонах с переменным числом аргументов используется оператор sizeof. () (он не имеет отношения к старому оператору sizeof()):
В списке-параметров-шаблона (template
) параметр typename. вводит в программу пакет параметров шаблона.
В предложении объявления параметра (func(parameter-list)) многоточие «верхнего уровня» вводит пакет параметров функции, и положение многоточия имеет большое значение.
Пример проброса значений во внутренние ф-ции:
Шаблоны с многоточием и Variadic
Шаблон Variadic — это класс или шаблон функции, поддерживающий произвольное число аргументов. Этот механизм особенно удобен для разработчиков библиотек C++, поскольку его можно применить к как к шаблонам классов, так и к шаблонам функций. Таким образом, он предоставляет широкий спектр широкий спектр типобезопасных и нетривиальных функций и гибких возможностей.
Синтаксис
В шаблонах шаблонами с переменным числом аргументов многоточие используется двумя способами. Слева от имени параметра обозначается пакет параметров, а справа от имени параметра — пакеты параметров разворачиваются в отдельные имена.
Ниже приведен простой пример синтаксиса определения класса шаблона Variadic :
В обоих случаях (как при введении пакета параметров, так и при его развертывании) вокруг многоточия можно оставить пробельные символы, как показано в этом примере:
Обратите внимание, что в этой статье используется соглашение, показанное в первом примере (многоточие присоединяется к typename ).
В предыдущих примерах аргументы являются пакетом параметров. Класс classname может принимать переменное число аргументов, как показано в следующих примерах:
Кроме того, определение шаблонного класса с переменным числом аргументов может устанавливать требование о том, что должен быть передан по меньшей мере один параметр:
Ниже приведен простой пример синтаксиса функции шаблона Variadic :
Затем пакет параметров arguments разворачивается для использования, как показано в следующем разделе Общие сведения о шаблонах Variadic.
Возможны и другие формы синтаксиса шаблонной функции с переменным количеством аргументов возможны. Некоторые примеры приведены ниже.
const Также разрешены спецификаторы:
Шаблонные функции с переменным числом аргументов (как и аналогичные шаблонные классы) также могут устанавливать требование о том, что должен быть передан по меньшей мере один параметр.
В шаблонах с переменным числом аргументов используется оператор sizeof. () (он не имеет отношения к старому оператору sizeof() ).
Дополнительные сведения о положении многоточия
Выше в этой статье говорилось, что если многоточие определяет пакеты параметров и их развертывание, то «Слева от имени параметра оно означает пакет параметров, а справа от имени параметра оно служит для развертывания пакетов параметров в отдельные имена». Технически это верно, но может порождать неоднозначности при трансляции в код. При этом нужно учитывать указанные ниже особенности.
В шаблоне-parameter-list ( template
) typename. представляет пакет параметров шаблона.
В предложении «параметр-объявление» ( func(parameter-list) ) многоточие «верхнего уровня» представляет пакет параметров функции, а расположение многоточия имеет важное значение:
Там, где многоточие стоит непосредственно за именем параметра, оно используется для развертывания пакета параметров.
Пример
Механизм действия шаблонных функций с переменным числом аргументов можно проиллюстрировать на показательном примере — переписать с ее использованием какую-либо функциональность printf :
Выходные данные
Большинство реализаций, включающих функции шаблонов Variadic, используют рекурсию некоторой формы, но немного отличается от традиционной рекурсии. Традиционная рекурсия включает функцию, вызывающую саму себя, используя ту же сигнатуру. (Она может быть перегружена или включена в шаблон, но одна и та же подпись выбирается каждый раз.) Рекурсия Variadic заключается в вызове шаблона функции Variadic с использованием отличающихся (почти всегда уменьшающихся) чисел аргументов и, таким образом, каждый раз отмечать разные сигнатуры. «Базовый случай» по-прежнему является обязательным, но природа рекурсии отличается.
Variadic templates в C++0x
Те, кто читал книгу Андрея Александреску «Современное программирование на C++» знают, что существует обширный класс задач (в области метапрограммирования с использованием шаблонов), когда шаблону при инстанцировании необходимо указать переменное (заранее неизвестное) количество аргументов. Типичные примеры таких задач:
— Описание кортежей (tuples)
— Описание типов наподобие вариантов (variants)
— Описание функторов (в этом случае перечень типов аргументов зависит от сигнатуры функции)
— Классификация типов по заранее заданным множествам
— и т. п.
В каждой такой задаче точное количество типов, передаваемых соответствующему шаблону в качестве аргументов, заранее определить сложно. И, вообще говоря, зависит от желания и потребностей того, кто намеревается использовать соответствующий шаблонный класс.
В рамках действующего стандарта С++ сколь-нибудь удобного решения таких задач не существует. Шаблоны могут принимать строго определённое количество параметров и никак иначе. А. Александреску (в упомянутой выше книге) предлагает общее решение, основанное на т. н. «списках типов», в котором типы представлены в виде односвязного списка, реализованного посредством рекурсивных шаблонов. Альтернативным решением (используемом, например, в boost::variant и boost::tuple) является объявление шаблонного класса с большим количеством параметров, которым (всем, кроме первого) присвоено некоторое значение по умолчанию. Оба этих решения являются половинчатыми и не охватывают весь спектр возможных задач. По этому, для устранения недостатков существующих решений и упрощения кода новый стандарт предлагает С++-разработчикам новый вариант объявления шаблонов? «шаблоны с переменным количеством параметров» или, в оригинале, «variadic templates».
Простые варианты использования
В качестве достаточно простого примера использования шаблонов с переменным числом параметров можно привести реализацию функтора. Выглядит эта реализация следующим образом:
# include iostream >
FunctorImpl(FT fn) : m_fn(fn)
// Объявляем общий шаблон-диспетчер
template typename FT>
struct Functor : public FunctorImpl
<
Functor() : FunctorImpl ( NULL ) <;>
Functor(FT fn) : FunctorImpl (fn) <;>
>;
int a = 100 ;
std :: cout » » ;
std :: cout std :: endl ;
>
Результат выполнения этого кода вполне ожидаемый:
// Объявляем класс-замыкание, принимающий в конструкторе объект, для которого будет вызываться функция-член. Выглядит он очень просто
template typename FT>
struct Closure : public FunctorImpl
<
typedef typename FunctorImpl ::HostType HostType;
Closure(HostType* obj, FT fn) : FunctorImpl (fn, obj) <;>
>;
// Использование
class A
<
public :
A( int base = 0 ) : m_base(base) <;>
int foo( int a)
private :
int m_base;
>;
A b1( 10 ), b2;
Closure int (A::*)( int )> a_foo(&b1, &A::foo);
// Можно заметить, что общаяя реализация функтора также корректно работает с указателями на функции-члены
Functor int (A::*)( int )> b_foo(&A::foo);
std :: cout 20 ) » » 20 ) » » 20 ) std :: endl ;
3. В ряде случаев при специализации может потребоваться учитывать, что пакет параметров может оказаться пустым. Такое, вообще говоря, допустимо.
При этом необходимо помнить, что в случае с шаблонными классами, параметры, упакованные в пакет, могут конкретизироваться начиная с головы пакета. Конкретизировать параметры начиная с хвоста пакета невозможно (в силу того, что пакет параметров может только замыкать список параметров шаблона). В отношении шаблонных функций такого ограничения нет.
Более сложные случаи
печатает на консоль ожидаемые
5
10 20 30 40 50
Конструкция sizeof … (Nums), приведённая в этом примере, используется для получения количества параметров в пакете. В ней Nums — это имя пакета параметров. К сожалению, дизайн шаблонов с переменным количеством параметров таков, что это — единственное, что можно сделать с пакетом параметров (помимо его непосредственно раскрытия). Получить параметр из пакета по его индексу, например, или совершить какие-либо более сложные манипуляции в рамках проекта нового стандарта невозможно.
что приведёт к выводу на экран другой последовательности:
100 200 300 400 500
Вообще, конкретный вид паттерна зависит от контекста, в котором он раскрывается. Более того, паттерн может содержать упоминание более одного пакета параметров. В этом случае все упомянутые в паттерне пакеты будут раскрываться синхронно, а потому количество фактических параметров в них должно совпадать.
Такая ситуация может возникать в случае, когда требуется определить кортежи значений. Предположим, необходимо организовать универсальный функтор-композитор, задача которого — передать в некоторую функцию результаты выполнения заданных функций для некоего аргумента. Пусть существует некоторый набор функций:
double fn1( double a)
<
return a * 2 ;
>
int fn2( int a)
<
return a * 3 ;
>
int fn3( int a)
<
return a * 4 ;
>
И две операции:
int test_opr( int a, int b)
<
return a + b;
>
int test_opr3( int a, int b, int c)
<
return a + b * c;
>
Необходимо написать универсальный функтор, применение операции вызова функции к которому приводило бы к выполнению такого кода:
test_opr(f1(x), f2(x));
или
test_opr3(f1(x), f2(x), f3(x));
Функтор должен принимать на вход операцию и перечень функций, результаты работы которых надо передать в качестве аргументов этой операции. Каркас определения такого функтора может выглядеть следующим образом:
template typename Op, typename … F>
class Compositor
<
public :
Compositor(Op op, F … fs);
>;
Первая проблема, которую необходимо решить — это каким образом сохранять переданные функции. Для этого можно применить множественное наследование от классов, непосредственно хранящие данные заданного типа:
template typename T>
struct DataHolder
<
T m_data;
>;
// Определяем общий вид шаблона, используемого для порождения кортежа
template int Num, typename Tp = IndexesTuple<>>
struct IndexTupleBuilder;
В итоге, использовать этот шаблон можно следующим образом:
typedef typename IndexTupleBuilder 6 > Indexes;
При этом Indexes будет эквивалентно IndexesTuple
Для того, чтобы этот класс был применён в реализации композитора, надо ввести промежуточный базовый класс, который и будет наследовать от классов с данными. При этом каждый класс с данными будет снабжён своим уникальным индексом:
template int idx, typename T>
struct DataHolder
<
DataHolder(T const & data) : m_data(data)
Для определения типа возвращаемого значения используется другая новая для C++ конструкция — decltype. Результатом её применения (в данном случае) является тип возвращаемого функцией значения. Конструкция выглядит несколько странной. По смыслу она эквивалентна такой
decltype(op(fs(0) …))
Но поскольку в области видимости класса пакет fs не определён, то оператор применяется к сконвертированному к ссылке на тип функции NULL.
и композитор готов. Пара примеров его использования:
auto f = MakeOp(test_opr, fn1, fn2);
auto ff = MakeOp(test_opr3, fn1, fn2, fn3);
auto ff1 = MakeOp(test_opr3, fn1, fn2, [=]( int x) < return f(x) * 5 ;>); // здесь последним параметром в композитор передаётся лямбда-функция.
Composer(Op op, F const &. fs) : m_op(op), Base(fs. )
Такой вариант выглядит несколько проще.
Ещё некоторое количество хитростей
Но при использовании такого метода нельзя забывать, что последовательность вычислений аргументов, строго говоря, не определена, и в каком именно порядке будут выполнены операции — заранее сказать нельзя.
Использование C++ и шаблонов с переменным количеством аргументов при программировании микроконтроллеров
ARM с ядром Cortex Mx (на примере STM32F10x)
Микроконтроллер ARM Cortex M3 STM32F103c8t6 широко распространен как 32-х битный микроконтроллер для любительских проектов. Как для практически любого микроконтроллера, для него существует SDK, включающая, в том числе и заголовочные файлы C++ определения периферии контроллера.
И вот там последовательный порт, например, определен как структура данных, а экземпляр этой структуры находится в области адресов, отведенной под регистры и мы имеем доступ к этой области через указатель на конкретный адрес.
Для тех, кто не сталкивался с этим ранее, я немного опишу, как это определено, те же из читателей, которые знакомы с этим, могут пропустить это описание.
Эта структура и её экземпляр описаны вот так:
Подробнее можно посмотреть здесь stm32f103xb.h ≈ 800 кБайт
И если пользоваться только только определениями в этом файле, приходится писать вот так (пример использования регистра состояний последовательного порта):
А пользоваться приходится, потому что существующие фирменные решения, известные как CMSIS и HAL слишком сложны, чтобы использовать их в любительских проектах.
Но если писать на C++, то можно написать так:
Изменяемая ссылка инициализируется указателем. Это небольшое облегчение, но приятное. Ещё лучше, конечно, написать небольшой класс-оберточку над этим, при этом такой прием всё равно пригодится.
Конечно, хотелось бы сразу написать этот класс-оберточку над последовательным портом (EUSART – extended universal serial asinhronous reseiver-transmitter), таким заманчивым, с расширенными возможностями, последовательным асинхронным приемопередатчиком и иметь возможность связать наш маленький микроконтроллер с настольной системой или ноутбуком, но микроконтроллеры Cortex отличаются развитой системой тактирования и начать придется с неё, а потом ещё сконфигурировать соответствующие выводы портов ввода-вывода для работы с периферией, потому что в серии STM32F1xx, как и во многих других микроконтроллерах ARM Cortex нельзя просто так сконфигурировать выводы порта на ввод или вывод и работать при этом с периферией.
Что же, начнем с включения тактирования. Система тактирования называется RCC регистры управления тактированием (registers for clock control) и тоже представляет из себя структуру данных, объявленному указателю на которую присвоено конкретное значение адреса.
Поля этой структуры, объявленные вот так, где __IO определяет volatile:
соответствуют регистрам из RCC, а отдельные биты этих регистров включению или функции тактирования периферии микроконтроллера. Всё это хорошо описано в документации (pdf).
Указатель на структуру определен как
Работа с битами регистров без использования SDK обычно выглядит таким образом:
Вот включение тактирования порта A.
Можно включить два и более бита сразу
Выглядит для C++ немного, что ли, непривычно. Лучше было бы написать по другому, вот так, например, используя ООП.
Выглядит лучше, но в XXI веке мы пойдем немного дальше, воспользуемся C++ 17 и напишем с использованием шаблонов с переменным количеством параметров ещё красивее:
Где Rcc определена вот таким образом:
От этого и начнем строить обертку над регистрами тактирования. Для начала определим класс и указатель (ссылку) на него.
Сначала хотелось написать в стандарте C++ 11/14 с использованием рекурсивной распаковки параметров шаблона функции. Хорошая статья об этом приведена в конце заметки, в разделе ссылки.
Рассмотрим вызов функции включения тактирования порта:
GCC развернет его вот в такой набор команд:
Получилось? Проверим дальше
Увы, не совсем, наивный GCC развернул замыкающий вызов рекурсии отдельно:
В защиту GCC нужно сказать, что вот так разворачивается не всегда, а только в более сложных случаях, что будет видно при реализации класса порта ввода-вывода. Что же, тут на помощь спешит C++ 17. Перепишем класс TRCC, используя возможности встроенного пролистывания.
Вот теперь получилось:
И код класса стал проще.
Вывод: C++ 17 позволяет нам с помощью шаблонов с переменным числом параметров получить такой же минимальный набор инструкций (даже при выключенной оптимизации), какой получается при использовании классической работы с микроконтроллером через определения регистров, но при этом мы получаем все преимущества сильной типизации C++, проверок во время компиляции, переиспользуемого через структуру базовых классов кода и так далее.
Вот как-то так записанное на C++
И классический текст на регистрах:
Теперь следует продолжить работу и написать класс порта ввода-вывода. Работа с битами портов ввода-вывода осложняется тем, что на конфигурацию одной ножки порта отводится четыре бита и, таким образом, на 16-ти битный порт требуется 64 бита конфигурации, которые разделены на два 32-х битные регистра CRL и CRH. Плюс ещё ширина битовой маски становится больше 1. Но и тут пролистывание C++ 17 показывает свои возможности.
Далее будет написан класс TGPIO, а также классы для работы с другой периферией, последовательного порта, I2C, SPI, ПДП, таймеров и многого другого, что обычно присутствует в микроконтроллерах ARM Cortex и тогда можно будет помигать вот такими светодиодиками.
Но об этом в следующей заметке. Исходники проекта на гитхабе.
Интернет статьи, использованные при написании заметки
Статьи на Xабре, побудившие меня всё-таки написать эту заметку
Написано 12.04.2019 – С Днем Космонавтики!
Картинка STM32F103c8t6 из CubeMX.
В качестве отправной точки использован текст, созданный расширением Eclips’а для работы с микроконтроллерами GNU MCU Eclipse ARM Embedded и STM-ского CubeMX, т. есть файлы стандартных функций C++, _start() и _init(), определения векторов прерываний взяты из MCU Eclipse ARM Embedded, а файлы определения регистров и работы с ядром Cortex M3 – из проекта, сделанного CubeMX.
C++11 variadic templates и длинная арифметика на этапе компиляции
За тридцать лет, прошедших с момента его появления в недрах Bell Labs, C++ проделал долгий путь, пройдя от «усовершенствованного C» до одного из самых популярных и выразительных компилируемых языков. Особенно много, на авторский сугубо личный взгляд, дал C++ новый стандарт C++11, стремительно обретающий поддержку компиляторов. В этой статье мы постараемся «пощупать руками» одну из его мощнейших фич — шаблоны с переменным числом аргументов (variadic templates).
Разработчики, знакомые с книжками Александреску, вероятно, помнят концепцию списка типов. В старом-добром C++03 при необходимости использовать заранее неизвестное число аргументов шаблона предлагалось создать такой список и потом хитрым хаком его использовать. Этим способом реализовывались кортежи (ныне std::tuple) и проч. и проч. А сама глава про списки типов ясно давала понять, что на шаблонах C++ можно проводить практически любые вычисления (заниматься λ-исчислением, например), лишь бы их можно было записать в функциональном стиле. Эту же концепцию можно было бы применить и к длинной арифметике: хранить длинные числа как списки intов, вводить основной класс вида
, операции над ним и так далее. Но во славу нового стандарта мы пойдём другим путём.
Parameter packs
Итак, основа основ — это новый синтаксис, введённый в C++11. Для начала рассмотрим, как же должно выглядеть определение класса tuple, не являющееся преступлением против человечества:
Обратите внимание на троеточие в аргументах шаблона. Теперь Types — не имя типа, а parameter pack — совокупность нуля или более произвольных типов. Как его использовать? Хитро.
Первый способ: как набор типов аргументов функций или методов. Это делается так:
Здесь Types. args — другая специальная синтаксическая конструкция (function parameter pack), которая разворачивается в соответствующую parameter pack’у цепочку аргументов функции. Поскольку C++ поддерживает автоопределение аргументов шаблонных функций, эту нашу функцию теперь можно вызывать с любым количеством аргументов любого типа.
Ну а дальше-то что? Что с этими всеми аргументами можно делать?
Ну и, наконец, в-третьих (и в самых банальных), списки аргументов можно использовать в рекурсии:
На выходе получаем типобезопасный printf — определённо стоило того!
С помощью определённого количества шаблонной магии можно научиться извлекать из pack’ов тип и значение по номеру и производить прочие махинации. Пример под спойлером.
Здесь мы видим ещё не упомянутую, но довольно простую для понимания команду sizeof. (Types) — она возвращает количество типов в parameter pack’е. Кстати, её можно реализовать самостоятельно.
Итак, вся суть в строчке
Она делает не что иное, как вызывает функцию с аргументами из кортежа. Вы только подумайте! Именно для этого фокуса нам и потребовался список чисел от 0 до (n − 1) — именно благодаря ему эта строчка разворачивается в нечто вроде
Напишите генератор списка от (n − 1) до 0 — и вы сможете разворачивать кортежи одной строчкой:
Мораль: pack expansion — убойный инструмент.
Длинная арифметика
Начнём нашу реализацию длинной арифметики. Для начала определим основной класс:
В лучших традициях вычислений на этапе компиляции в C++ все, собственно, данные здесь не хранятся где-либо, а являются непосредственными аргументами шаблона. C++ будет различать реализации класса BigUnsigned с разными наборами параметров, за счёт чего мы сможем реализовать наши вычисления. Условимся, что первый параметр будет содержать младшие 32 бита нашего длинного числа, а последний — самые старшие 32, включая, возможно, ведущие нули (лично мне это видится самым логичным решением).
Прежде, чем заняться реализацией сложения, давайте определим на наших длинных числах операцию конкатенации. На примере этой операции мы познакомимся со стандартной тактикой, которую будем использовать в дальнейшем.
Итак, определим операцию над двумя типами:
Мы подразумеваем, что этими двумя типами будут реализации BigUnsigned. Реализуем операцию для них:
Не менее тривиально можно реализовать битовые операции, например, (очень наивный) xor:
Теперь, собственно, сложение. Никуда не деться — придётся воспользоваться рекурсией. Определим основной класс и базу рекурсии:
Теперь — основные вычисления:
Итак, что же произошло.
Шаблон вида
нужен нам, чтобы отделить самые младшие разряды длинных чисел от всех старших. Если бы мы использовали шаблон более общего вида
, то это сделать было бы не так-то просто и нам потребовалось бы ещё несколько вспомогательных структур. (Зато можно было бы написать их с умом и впоследствии реализовать умножение Карацубы, ня!)
Далее, строка
вычисляет так называемый carry bit, указывающий, произошло ли переполнение в текущем разряде или нет. (Вместо константы UINT32_MAX можно и стоит использовать std::numeric_limits.)
Наконец, финальное вычисление находит результат, применяя рекурсию по правилу
Что ж, можно тестировать! Непосредственно вычисления будут выглядеть примерно так:
Скомпилировалось! Запустилось! Но… как узнать значение C? Как, собственно, тестировать-то?
Простой способ: давайте вызовем ошибку на этапе компиляции. Например, напишем в main
Менее простой способ: давайте напишем функцию, которая будет генерировать строку с бинарным представлением заданного числа. Заготовка будет выглядеть примерно так:
Воспользуемся стандартным объектом std::bitset, чтобы распечатывать текущие 32 бита на каждом этапе:
Осталось только задать базу рекурсии:
Сложный, но наиболее убедительный способ — вывод десятичного представления — потребует от нас определённого труда, а именно реализации операции деления.
Для этого нам потребуются определённые, эмм, prerequisites.
Определим основные классы и зададим базу рекурсии:
Я решил, что логично разделить любой битовый сдвиг на поочерёдное применение малого (сдвиг меньше 32 бит) и большого (сдвиг кратен 32 битам), поскольку их реализации существенно отличаются. Итак, большой сдвиг:
Здесь shift обозначает сдвиг на 32 ⋅ shift бит, и сама операция просто «съедает» или добавляет поочерёдно целые 32-битные слова.
Малый сдвиг — это уже чуть более тонкая работа:
Здесь снова приходится заботиться об аккуратном переносе бит.
Наконец, просто сдвиг:
Как и обещано, он просто грамотно использует большой и маленький сдвиги.
Итак, деление. Начнём как положено: с определения классов и задания базы рекурсии.
И вот, наконец, наш код
Что ещё необходимо сказать
Во-первых, нельзя сказать, что на этом наша длинная арифметика готова. Мы могли
бы добавить сюда много: умножение (столбиком), возведение в степень, даже алгоритм
Евклида. Несложно придумать, как определить класс BigSigned
длинных чисел со знаком и все основные операции на нём.
Во-вторых, баги. Весь код приведён в учебных целях и, вероятно, нуждается в отладке.
(Я отлаживал версию, появившуюся до шаблонов с переменным аргументом, и, к сожалению,
только на одном компиляторе.) Будем надеяться, что хотя бы учебные цели были достигнуты.
В-третьих, нужно признать, что длинная арифметика — далеко не идеальный пример
для демонстрации силы шаблонов с переменным аргументом. Если вы видели интересные и практичные примеры использования этой фичи — пожалуйста, делитесь ссылками в комментариях.