Конструктор с переменным числом параметров с

Обновлено: 07.05.2024

Понадобилось мне прикрутить Lua к проекту на C++. Писать обертки в ручную — лень (слишком много писать), готовые не подходили по тем или иным причинам. Решил написать свою. А потому задался вопросом, как максимально упростить интерфейс? От одной только мысли об этом в голову лезли жутчайшие конструкции из шаблонов. Так оно в последствии и оказалось, но гораздо проще, чем представлялось.

В C++11 появились шаблоны с переменным числом аргументов, это позволяет писать шаблонные функции/классы так, как в C++03 было невозможно вовсе. Такие шаблоны сильно упрощают задачу.

Первым делом понадобилось написать обертку над простейшими действиями с интерпретатором (можно было бы обойтись простыми вызовами к C API Lua, но держать в памяти кучу индексов различных значений в стеке мне не хочется. Поэтому я обернул их в несколько функций, которые помимо того, что избавляют от необходимости передавать в каждую функцию указатель на состояние интерпретатора, практически не требуют индексов, так как они имеют значения по умолчанию.

В итоге хотелось увидеть интерфейс близкий к следующему:



Можно попробовать. Однако интерфейс будет все-таки чуточку сложнее. Нужно указать интерпретатору имя для экспортируемой функции. И передавать будем адрес на функцию.

    Возвращаемым значением колбэка:


Теперь, можно взяться за собственно экспорт функции. Для каждой функции создадим лямбду, которая будет принимать аргументы от интерпретатора, передавать их в функцию, а потом, возвращать интерпретатору результат. Лямбда должна храниться всё время, что работает экземпляр интерпретатора, поэтому указатель на каждую лямбду я сохраняю внутри класса и удаляю в деструкторе.


Выглядит странно. Попробуем разобраться. Для начала надо получить все аргументы от интерпретатора.


Получаем i-ый аргумент и возвращаем его, а с помощью рекурсии получаем остальные аргументы. Но этого мало.
Эту функцию нужно перегрузить, чтобы на последней итерации исполнялся другой код.


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


И нужно специализировать этот шаблон для последней итерации.


Помимо всего этого потребуется еще несколько специализаций (проблемы в типе void).

Результаты

  • Лямбды все-таки медленнее колбэков, при желании можно переписать код без них, но получится больше шаблонных функций.
  • При каждом вызове функции/метода мы получаем две рекурсии, глубина которых равна количеству аргументов функций. Возможно компилятор сделает всю эту орду шаблонных функций инлайновыми, я не проверял (и не уверен в этом).
  • Шаблоны сильно сказываются на времени компиляции. Но даже на моем довольно слабом ноутбуке сборка этой обертки и кода, который её использует, занимает гораздо меньше времени, чем сборка кода, который использует boost, так что это не критично.
  • Нет поддержки множественного наследования — слишком муторно его делать.
  • Нет доступа к метатаблицам, а значит нет переопределения операторов.
  • Нет поддержки перегрузки функций, но можно просто дать перегружаемым функциям разные имена.
  • Простой интерфейс.
  • Решение на чистом C++11, не требует генерации дополнительного кода дополнительными инструментами.
Как использовать

Прежде всего нужно создать объект класса util::Lua, при этом проинициализируется интерпретатор.


После этого можно экспортировать функции/классы.

Функции

Всё просто. Мы используем только указатель на функцию и имя, под которым она будет доступна в lua.


Типы всех параметров и возвращаемого значения будут определены и обработаны корректно.

Классы
  • Метод export_class должен экспортировать все методы/функции класса.
  • Метод export_me должен вызывать функцию Lua::export_class()
  • Метод class_name должен возвращать имя класса.

Функции util::Lua::export_class передаются в качестве параметров шаблона — класс, который мы хотим
экспортировать и его родитель, чтобы экспортировать и его (если это еще не сделано).

Самое интересное творится в методе export_class. К примеру:


Всё просто. Статические методы экспортируем как функции, методы — похожим образом, но через отдельную функцию. Конструктор экспортируется как функция с именем new, типы его аргументов необходимо указать явно в качестве
аргументов шаблона, связано это с тем, что на конструктор нельзя взять указатель. Приятная вещь в том, что объекты созданные посредством вызова такого конструктора из lua будет обрабатывать Garbage Collector. Когда все ссылки на объект будут удалены будет вызван delete для объекта C++.


Чуть не забыл. Весь код собирается с помощью g++ 4.7.2, также должны работать g++ >= 4.6.4 и clang >= 3.0.

E0309 существует более одного экземпляра конструктора "mathObj::matrix::matrix", соответствующего списку аргументов: .

и будет ссылаться на те два конструктора, т.е. компилятор выбрать не может какой-то из них.

//явно объявлять тип не помогает:

С этим можно что-то сделать? о:

У вас конструктор является функцией с переменным числом аргументов? Никаких преобразований типов данных тут не видно. Вообще следует привести полноценный пример.

for (double *p = &arr; i < N * M; p++) - это еще что такое? Откуда это лезет постоянно? Кто вас учил так работать с . аргументами?

Просто выровняйте количество параметров в перегруженных конструкторах и компилятор тогда Вас поймет. Например matrix(int n, int m, . ) и matrix(int n, double arr, . ) .

@Andrey Sv: О каком "правильно" вы ведете речь? В VS2017: "error C2666: 'matrix::matrix': 2 overloads have similar conversions / while trying to match the argument list '(int, double, double, double, double)'"

3 ответа 3

@a1rltt Они есть, но они проблемные. Если вы ошибетесь с количеством (или даже типом!) параметров, отстрелите себе ногу. Да и вообще, проход по . руками вместо встроенных функций - неопределенное поведение.

Можно преобразовать функции с переменным количеством аргументов в шаблоны c переменным количеством параметров:

Размерность матрицы я бы тоже вынес в качестве параметра шаблона.

Неоднозначным в вашем примере является только вариант

С точки зрения процесса разрешения перегрузки, вторая функция является лучшим кандидатом для второго аргумента: там наблюдается точное совпадение double -> double , в то время как для первой функции требуется преобразование double -> int .

Но в то же время первая функция является лучшим кандидатом для третьего аргумента: там наблюдается точное совпадение double -> double , в то время как во второй функции имеет место "слабое" соответствие double -> . .

В результате получаем неоднозначность.

Не рекомендуется смешивать перегрузки по типу и по . при разном количестве фиксированных аргументов. Особенное если учесть, что даже преобразование double -> int в С++ имеет более высокий ранг, чем double -> . .

Такого в языках С и С++ нет. Работа с . аргументами ведется только через , va_list , va_start и так далее.

Если бы вы не занимались подобной ерундой, вы смогли бы просто не именовать параметр arr в некоторых случаях, тем самым сделав количество фиксированных параметров одинаковым. Именованные аргументы ( n и m ), с которых можно начать va_start в ваших функциях уже есть.

Отдельно можно добавить, что не ясно, как вы собрались реализовывать неконстантный доступ к матрице в классе matrix::Row , если ссылка на матрицу в этом классе у вас жестко "прошита" как const matrix& A;

Чтобы настроить, как класс инициализирует его члены или вызывать функции при создании объекта класса, определите конструктор. Конструкторы имеют имена, совпадающие с именами классов, и не имеют возвращаемых значений. Вы можете определить столько перегруженных конструкторов, сколько необходимо для настройки инициализации различными способами. Как правило, конструкторы имеют открытые специальные возможности, чтобы код за пределами определения класса или иерархии наследования может создавать объекты класса. Но вы также можете объявить конструктор как protected или private .

Конструкторы могут при необходимости принимать список инициализаторов элементов. Это более эффективный способ инициализации членов класса, чем назначение значений в тексте конструктора. В следующем примере показан класс Box с тремя перегруженными конструкторами. Последние два используют списки инициализации элементов:

При объявлении экземпляра класса компилятор выбирает, какой конструктор будет вызываться на основе правил разрешения перегрузки:

  • Конструкторы могут быть объявлены как inline , , explicitfriend или constexpr .
  • Конструктор может инициализировать объект, объявленный как const , volatile или const volatile . Объект становится const после завершения конструктора.
  • Чтобы определить конструктор в файле реализации, присвойте ему полное имя, как и любая другая функция-член: Box::Box() .

Списки инициализаторов элементов

При необходимости конструктор может иметь список инициализаторов элементов, который инициализирует члены класса перед запуском тела конструктора. (Список инициализаторов элементов не совпадает со списком инициализаторов типа std::initializer_list .)

Предпочитать инициализаторы элементов перечисляют значения вместо назначения значений в тексте конструктора. Список инициализаторов элементов напрямую инициализирует элементы. В следующем примере показан список инициализаторов элементов, состоящий из всех identifier(argument) выражений после двоеточия:

Идентификатор должен ссылаться на член класса; он инициализирован со значением аргумента. Аргумент может быть одним из параметров конструктора, вызова функции или . std::initializer_list

const члены и члены ссылочного типа должны быть инициализированы в списке инициализаторов элементов.

Чтобы обеспечить полную инициализацию базовых классов перед запуском производного конструктора, вызовите все параметризованные конструкторы базового класса в списке инициализаторов.

Конструкторы по умолчанию

Конструкторы по умолчанию обычно не имеют параметров, но они могут иметь параметры со значениями по умолчанию.

Конструкторы по умолчанию являются одной из специальных функций-членов. Если конструкторы в классе не объявляются, компилятор предоставляет неявный inline конструктор по умолчанию.

Если используется неявный конструктор по умолчанию, обязательно инициализировать элементы в определении класса, как показано в предыдущем примере. Без этих инициализаторов члены будут неинициализированы, а вызов Volume() создаст значение мусора. Как правило, рекомендуется инициализировать элементы таким образом, даже если не используется неявный конструктор по умолчанию.

Вы можете запретить компилятору создавать неявный конструктор по умолчанию, определив его как удаленный:

Конструктор по умолчанию, созданный компилятором, будет определен как удаленный, если какие-либо члены класса не являются конструктором по умолчанию. Например, все члены типа класса и их члены класса должны иметь конструктор по умолчанию и деструкторы, которые доступны. Все члены данных ссылочного типа и все const члены должны иметь инициализатор элементов по умолчанию.

При вызове конструктора по умолчанию, созданного компилятором, и пытаетесь использовать круглые скобки, выдается предупреждение:

Это утверждение является примером проблемы "Большинство vexing Parse". Можно интерпретировать myclass md(); как объявление функции или как вызов конструктора по умолчанию. Поскольку средства синтаксического анализа C++ предпочитают объявления по сравнению с другими вещами, выражение рассматривается как объявление функции. Дополнительные сведения см. в разделе "Большинство синтаксического анализа".

Если объявлены какие-либо конструкторы, отличные от по умолчанию, компилятор не предоставляет конструктор по умолчанию:

Если у класса нет конструктора по умолчанию, массив объектов этого класса нельзя создать с помощью синтаксиса квадратной скобки. Например, учитывая предыдущий блок кода, массив Boxes нельзя объявить следующим образом:

Однако для инициализации массива объектов Box можно использовать набор списков инициализаторов:

Дополнительные сведения см. в разделе "Инициализаторы".

Конструкторы копии

Конструктор копирования инициализирует объект, копируя значения элементов из объекта того же типа. Если члены класса являются простыми типами, такими как скалярные значения, конструктор копирования, созданный компилятором, достаточно, и вам не нужно определять собственные. Если для класса требуется более сложная инициализация, необходимо реализовать пользовательский конструктор копирования. Например, если член класса является указателем, необходимо определить конструктор копирования для выделения новой памяти и копирования значений из объекта, на который указывает другой объект. Конструктор копирования, созданный компилятором, просто копирует указатель, чтобы новый указатель по-прежнему указывал на расположение памяти другого пользователя.

Конструктор копирования может иметь одну из следующих сигнатур:

При определении конструктора копирования необходимо также определить оператор присваивания копирования (=). Дополнительные сведения см. в разделе "Назначение " и " Копирование конструкторов" и операторов присваивания копирования.

Вы можете запретить копирование объекта, определив конструктор копирования как удаленный:

При попытке копирования объекта возникает ошибка C2280: попытка ссылаться на удаленную функцию.

Конструкторы перемещения

Конструктор перемещения — это специальная функция-член, которая перемещает владение данными существующего объекта в новую переменную без копирования исходных данных. Он принимает ссылку rvalue в качестве первого параметра, а все последующие параметры должны иметь значения по умолчанию. Конструкторы перемещения могут значительно повысить эффективность программы при передаче больших объектов.

Компилятор выбирает конструктор перемещения, когда объект инициализируется другим объектом того же типа, если другой объект будет уничтожен и больше не нуждается в его ресурсах. В следующем примере показано одно дело, когда конструктор перемещения выбирается с помощью разрешения перегрузки. В конструкторе, который вызывает get_Box() , возвращаемое значение является xvalue (значение eXpiring). Поэтому он не назначается какой-либо переменной и поэтому выходит за пределы области действия. Чтобы обеспечить мотивацию для этого примера, давайте предоставим Box большой вектор строк, представляющих его содержимое. Вместо копирования вектора и его строк конструктор перемещения "крадет" его из значения "box", чтобы вектор теперь принадлежит новому объекту. Вызов std::move необходим, так как оба vector класса string реализуют собственные конструкторы перемещения.

Если класс не определяет конструктор перемещения, компилятор создает неявный конструктор, если конструктор копирования не объявлен пользователем, оператор назначения копирования, оператор перемещения или деструктор. Если не определен явный или неявный конструктор перемещения, операции, в противном случае использующие конструктор перемещения, используют конструктор копирования. Если класс объявляет конструктор перемещения или оператор присваивания перемещения, неявно объявленный конструктор копирования определяется как удаленный.

Неявно объявленный конструктор перемещения определяется как удаленный, если какие-либо элементы, являющиеся типами классов, не имеют деструктора или если компилятор не может определить, какой конструктор следует использовать для операции перемещения.

Дополнительные сведения о написании конструктора нетривиального перемещения см. в разделе "Конструкторы перемещения" и "Операторы присваивания перемещения" (C++).

Явно заданные по умолчанию и удаленные конструкторы

Конструкторы копирования по умолчанию , конструкторы по умолчанию, конструкторы перемещения, операторы присваивания копирования, операторы присваивания перемещения и деструкторы. Вы можете явно удалить все специальные функции-члены.

Конструкторы constexpr

Конструктор может быть объявлен как constexpr , если

  • он либо объявлен как стандартный, либо удовлетворяет всем условиям для функций constexpr в целом;
  • класс не имеет виртуальных базовых классов;
  • каждый из параметров является литеральным типом;
  • тело не является блоком try-block функции;
  • инициализированы все нестатические члены данных и подобъекты базового класса;
  • Значение , если класс является (a) объединением, имеющим члены варианта, или (б) имеет анонимные объединения, инициализируется только один из членов профсоюза;
  • каждый нестатический член данных типа класса, а все подобъекты базового класса имеют конструктор constexpr.

Конструкторы списков инициализаторов

Затем создайте объекты Box следующим образом:

Явные конструкторы

Если у класса имеется конструктор с одним параметром, или у всех параметров, кроме одного, имеются значения по умолчанию, тип параметра можно неявно преобразовать в тип класса. Например, если у класса Box имеется конструктор, подобный следующему:

Можно инициализировать Box следующим образом:

Или передать целое значение функции, принимающей объект Box:

В некоторых случаях подобные преобразования могут быть полезны, однако чаще всего они могут привести к незаметным, но серьезным ошибкам в вашем коде. Как правило, необходимо использовать ключевое explicit слово в конструкторе (и определяемых пользователем операторах), чтобы предотвратить такое неявное преобразование типов:

Когда конструктор является явным, эта строка вызывает ошибку компилятора: ShippingOrder so(42, 10.8); . Дополнительные сведения см. в разделе о преобразованиях определяемых пользователем типов.

Порядок строительства

Конструктор выполняет свою работу в следующем порядке.

Вызывает конструкторы базовых классов и членов в порядке объявления.

Если класс является производным от виртуальных базовых классов, конструктор инициализирует указатели виртуальных базовых классов объекта.

Если класс имеет или наследует виртуальные функции, конструктор инициализирует указатели виртуальных функций объекта. Указатели виртуальных функций указывают на таблицу виртуальных функций класса, чтобы обеспечить правильную привязку вызовов виртуальных функций к коду.

Выполняет весь код в теле функции.

В следующем примере показан порядок, в котором конструкторы базовых классов и членов вызываются в конструкторе для производного класса. Сначала вызывается базовый конструктор. Затем члены базового класса инициализируются в том порядке, в котором они отображаются в объявлении класса. Наконец, вызывается производный конструктор.

Выходные данные будут выглядеть следующим образом.

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

Если базовый класс не имеет конструктора по умолчанию, необходимо указать параметры конструктора базового класса в конструкторе производного класса:

Если конструктор создает исключение, то удаление выполняется в порядке, обратном созданию.

Отменяется код в теле функции конструктора.

Объекты базовых классов и объекты-члены удаляются в порядке, обратном объявлению.

Если конструктор не делегируется, все полностью созданные объекты базового класса и члены уничтожаются. Однако поскольку сам объект не полностью построен, деструктор не выполняется.

Производные конструкторы и расширенная инициализация агрегатов

Если конструктор базового класса не является открытым, но доступен для производного класса, нельзя использовать пустые фигурные скобки для инициализации объекта производного типа в /std:c++17 режиме, а затем в Visual Studio 2017 и более поздних версий.

В следующем примере показана соответствующая реакция на событие в C++14:

В C++17 Derived теперь считается агрегатным типом. Это означает, что инициализация Base через закрытый конструктор по умолчанию происходит непосредственно как часть расширенного правила агрегатной инициализации. Ранее частный Base конструктор был вызван через Derived конструктор, и он был успешно выполнен из-за friend объявления.

В следующем примере показано поведение C++17 в Visual Studio 2017 и более поздних версий в /std:c++17 режиме:

Конструкторы для классов с множественным наследованием

Если класс является производным от нескольких базовых классов, конструкторы базового класса вызываются в порядке, в котором они перечислены в объявлении производного класса:

Должны выводиться следующие выходные данные:

Делегирующие конструкторы

Делегирующий конструктор вызывает другой конструктор в том же классе для выполнения некоторых действий по инициализации. Эта функция полезна, если у вас есть несколько конструкторов, которые все должны выполнять аналогичную работу. Основную логику можно написать в одном конструкторе и вызвать из других. В следующем тривиальном примере Box(int) делегирует свою работу Box(int,int,int):

Объект, созданный конструкторами, полностью инициализируется сразу после выполнения любого конструктора. Дополнительные сведения см. в разделе "Делегирование конструкторов".

Наследование конструкторов (C++11)

Производный класс может наследовать конструкторы от прямого базового класса с помощью using объявления, как показано в следующем примере:

Visual Studio 2017 и более поздних версий: оператор using в /std:c++17 режиме и более поздних версиях преобразует все конструкторы из базового класса, за исключением тех, которые имеют идентичную сигнатуру конструкторам в производном классе. Как правило, рекомендуется использовать наследуемые конструкторы, когда производный класс не объявляет новые члены данных или конструкторы.

Шаблон класса может наследовать все конструкторы от аргумента типа, если этот тип определяет базовый класс:

Производный класс не может наследоваться от нескольких базовых классов, если эти базовые классы имеют конструкторы с одинаковой сигнатурой.

Конструкторы и составные классы

Классы, содержащие члены типа класса, называются составными классами. При создании члена типа класса составного класса конструктор вызывается перед собственным конструктором класса. Если у содержащегося класса нет конструктора по умолчанию, необходимо использовать список инициализации в конструкторе составного класса. В предыдущем примере StorageBox при присвоении типу переменной-члена m_label нового класса Label необходимо вызвать конструктор базового класса и инициализировать переменную m_label в конструкторе StorageBox :

Начиная с версии C++11 язык C++ предоставляет возможность определять шаблоны, принимающие переменное число параметров.

Например, можно написать функцию, которая принимает произвольное число параметров произвольных типов и возвращает их сумму. Если суммирование для какой-нибудь пары типов не определено, то получим ошибку компиляции.

Почему в примере выше для организации выхода из рекурсии определён вариант sum, принимающий один параметр, а не вариант sum, принимающий нуль параметров и возвращающий число нуль?

Итак, для определения шаблона с переменным числом параметров необходимо указать пакет типов в template-заголовке:

Возможны комбинации с обычными параметрами (как в примере с суммой), но допустим максимум один пакет. Пакет может быть пустым. Узнать размер пакета можно с помощью sizeof… (имя пакета).

Types можно использовать в списке параметров функции:

Теперь parameters — это пакет параметров, типы которых задаются “параллельным” пакетом Types. Пакет параметров может быть только в функции-шаблоне, и ему обязан соответствовать пакет типов.

Внутри функции можно использовать как пакет типов, так и пакет параметров. Для этого после имени пакета ставится многоточие . , которое интерпретируется компилятором как раскрытие пакета в список (через запятую) в указанном месте (см. пример sum выше — раскрытие пакета параметров other). Из-за такого минимализма возможных действий с пакетами обычным способом определения функций-шаблонов с переменным числом параметров является рекурсивное определение.

Рассмотрим простенький пример шаблона шаблона класса с переменным числом параметров. На этот раз параметры шаблона не будут типами, а будут значениями типа size_t.

Похожий код можно записать на каком-нибудь функциональном языке программирования, например, Haskell:

Зачем сразу три определения?

говорит компилятору, что у нас есть шаблонный класс Sum, параметрами которого является пакет значений типа size_t.

является специализацией шаблона Sum для случая пустого пакета параметров. В этом случае мы определяем вложенную константу value со значением 0 (enum используется для краткости записи).

также является специализацией шаблона Sum, но теперь для случая наличия хотя бы одного параметра (first). Остальные параметры собираются в пакет other, который может быть пуст. Это определение позволяет собственно “запустить” рекурсивный процесс вычисления суммы. Рано или поздно other окажется пустым пакетом и будет подставлен нуль, определённый в предыдущей специализации.

Теперь представим, что требуется уметь вычислять суммарный размер произвольного набора типов — сумму значений sizeof для каждого типа. На основе Sum можно определить новый шаблон Sum_sizeof:

Запись sizeof(Types). есть раскрытие уже не пакета Types, а выражения от Types в список выражений от каждого элемента пакета. Например, если Types = < int, char, bool >, то sizeof(Types). раскрывается в sizeof(int), sizeof(char), sizeof(bool) , что, в свою очередь, может быть передано в качестве пакета целых чисел шаблону Sum.

При использовании C++14 (шаблонов переменных) можно переписать весь код выше более кратко:

Определим две функции: print, выводящую подряд все свои параметры в cout, и println, переадресующую вызов print и затем печатающую символ перевода строки. Шаблоны функций в C++ нельзя специализировать частично (как классы), но можно перегружать. При вызове компилятор должен выбрать наиболее конкретный из подходящих вариантов.

В данной версии определений аргументы копируются (передаются по значению), что может быть неоптимальным решением при выводе на печать строк или других больших объектов. Передачу по значению в данном случае можно заменить передачей по const-ссылке (знак & должен стоять до многоточия).

Попробуем теперь обобщить данный код — разрешим указывать объект потока вывода первым параметром. Если он не указан, то выводить в cout.

Наконец, даже этот код можно обобщить: легко заметить, что print(wcout, L". ") не скомпилируется, поскольку wcout не является объектом класса ostream. Классы ostream и wostream являются частными случаями шаблона basic_ostream.

Предположим, есть набор файлов, описывающих строки интерфейса некоторого приложения на разных языках. Например, английском и русском:

Мы хотим строить фразы, подставляя в модельную фразу-шаблон конкретные слова или значения. Определим для этого функцию format, принимающую первым параметром “модель”, а прочими параметрами значения для подстановки и возвращающую строку. Пусть например, строки из файла были считаны в вектор строк phrase, тогда

даст “Intensity may take values between 0 and 1.” в случае выбора английского файла и “Насыщенность может принимать значения между 0 и 1.” в случае выбора русского файла. А

даст, соответственно “oil price forecasting” и “прогноз цены для нефти” (обратите внимание на возможность разного порядка подстановок).

Попробуем определить функцию format с помощью Стандартной библиотеки C++ и шаблонов с переменным числом параметров.

Если попытаться написать её рекурсивно в стиле print из предыдущего примера, то придётся подставлять по одной строке и организовать подсчёт номера текущей подстановки, т. е. понадобятся вспомогательные определения функции, принимающей номер. Здесь рассмотрим другой способ, а именно, создание массива строковых представлений параметров.

Теперь нет рекурсии, и нешаблонный вариант format уже не нужен. Мы инициализируем массив строк param строками, возвращаемыми для них функцией to_string. (Осторожно: to_string не принимает string! С этим разберёмся далее.). Пустая строка в начале позволяет гарантировать, что массив param будет содержать по крайней мере 1 элемент (массивы размера 0 запрещены).

Чтобы параметры-строки не вызывали ошибки компиляции, создадим свою вспомогательную функцию to_str и будем использовать её вместо to_string:

Реализовать format (собственно выполнение подстановки) можно по-разному. Например, “в лоб” на основе рукописного конечного автомата так:

Список типов type list — конструкция времени компиляции, задающая последовательность (“список”) произвольных типов и позволяющая оперировать этой последовательностью как единым целым.

Начнём с простейшего примера. Предположим, мы хотим подсчитать, сколько раз встречается заданный тип в пакете. Это можно сделать так (C++14):

Здесь, однако, нельзя говорить о списке типов Types, поскольку пакет параметров шаблона не является типом, и возможности его использования довольно ограничены. Впрочем, благодаря механизму частичной специализации шаблонов с подстановкой по образцу, легко связать пакет с конкретным типом. Для этого определим шаблон Type_list (который мог бы вовсе не иметь членов):

После этого следующий код выведет 0 2 :

Помимо подсчёта числа вхождений можно абсолютно аналогично через рекурсивное определение реализовать поиск первого вхождения (индекс):

Теперь предположим, что надо, наоборот, по номеру в списке выбрать из него тип. Это тоже можно определить рекурсивно, но уже не как переменную, а как тип. Обычно это делается через определение структуры с вложенным типом type, который и получает соответствующее значение.

Чтобы при использовании не писать typename и ::type , можно определить using-синоним типа:

Метафункцией называется отображение в множество типов или из множества типов.

Конструкции вроде t_at, t_at_t или даже t_index выше можно считать метафункциями, но синтаксически затруднительно использовать их сами в качестве аргументов других метафункций, что ослабляет комбинативную мощность подобных конструкций.

“Полноценной” метафункцией в синтаксисе C++ можно назвать нешаблонный класс, имеющий вложенное определение шаблона типа, которое уже можно использовать как t_at. Это позволяет передавать метафункцию как обычный тип.

Метафункцией высшего порядка называется метафункция, принимающая другие метафункции как аргументы.

Например, определим метафункцию добавления элемента в начало списка типов.

Недостаток этой конструкции очевиден: уж очень навороченно выглядит “вызов” метафункции:

В принципе, к каждой определяемой метафункции можно добавлять аналогичное парное определение синонима типа через using, но если метафункция пришла как параметр, то мы не знаем её имя и будем вынуждены писать полный синтаксис применения.

Другой вариант состоит в определении макроса, раскрывающегося в эту идиоматическую конструкцию:

Теперь можно записывать более сложные конструкции с меньшим синтаксическим шумом. Например, рекурсивное определение метафункции высшего порядка T_map, порождающей список результатов для заданной метафункции Map (“отображение”) и списка аргументов:

Можно ли T_map записать более кратко без рекурсии?

Определим ещё одну метафункцию высшего порядка — свёртку (fold). Пример свёрток: сумма элементов, произведение элементов, максимум, минимум и т.п. Аргументами свёртки являются ассоциативная бинарная операция и список.

В случае, если аргументы метафункции должны быть значениями, а не типами (наиболее типичные случаи: булевские и целочисленные константы), то их удобно “обернуть” в тип и представлять далее как типы. Например, для целочисленных констант можно было бы определить обёртку вроде

Впрочем, в Стандартной библиотеке уже есть аналогичный шаблон integral_constant, первый параметр которого — тип константы, второй — сама константа.

Определим функцию “максимум”, принимающую аргументы integral_constant:

Определим функцию T_sizeof, преобразующую тип в его размер (в виде соответствующего integral_constant).

Теперь мы можем найти максимальный размер типа из заданного списка:

В наследство от C языку C++ достался ряд низкоуровневых средств. Одним из таких средств является “объединение” (union) — набор значений, размещённых по одному адресу в памяти.

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

Позаимствуем часть кода из предыдущего примера (список типов).

Здесь для простоты метафункции высшего порядка общего вида использовать не будем: вычисление максимума sizeof и alignof будем выполнять с помощью рекурсивных определений шаблонов переменных.

Можно ли записать эти определения более кратко и без рекурсии?

Проверка “является ли список 1 подмножеством списка 2” (квадратичный рекурсивный алгоритм времени компиляции):

Вспомогательная конструкция: найти в списке типов первый тип (вернуть его номер), для которого определён конструктор, принимающий заданный набор параметров Args. Возвращает –1, если такого типа в списке нет:

Теперь есть достаточно вспомогательных определений, чтобы сделать простенькую версию вариантного типа. (Более продвинутый вариант, к тому же не требующий C++14, есть в пакете библиотек Boost, аналог планируется включить в Стандартную библиотеку C++17.)

Пакет Types (и отвечающий ему список типов types) содержит типы, значения которых могут хранится в объекте Variant. Потребуем, чтобы их было не меньше 0, но и не больше 63 (в принципе, текущая реализация допускает до 255 типов).

Память выделяется статически в виде некоего “объекта” (просто кусок памяти нужного размера с нужным выравниванием) _stor. Номер типа текущего значения хранится в поле _type. Для “неинициализированных” Variant это –1, т.е. никакого значения нет. Такие объекты Variant будем называть пустыми.

Теперь представим, что нам нужно уничтожить значение, хранящееся в Variant. Иными словами, надо вызвать деструктор для того типа, номер которого записан в поле _type. Это легко реализовать, если обернуть вызовы деструкторов для всех Types в функции с общим интерфейсом, указатели на которые можно просто сохранить в массиве и выбирать их, используя _type в качестве индекса.

Для инициализации/присваивания объектов Variant данный трюк несложно повторить. Чтобы не усложнять код, реализуем минимальный рабочий вариант на основе конструктора копирования.

Можно определить также универсальный конструктор, принимающий любой набор параметров и пытающийся подобрать первый тип из списка, который допускает инициализацию таким набором параметров. Ему не нужна таблица функций-обёрток, поскольку он выбирает нужный тип статически.

Наконец, можно определить оператор присваивания одной специализации Variant значения другой специализации Variant. Для этого нам понадобится таблица перевода значений _type:

Удовлетворяет ли наш класс Variant правилу пяти?

При работе с подобными полиморфными объектами может быть удобно состыковать их с встроенными средствами C++ RTTI (run-time type information). В частности, можно реализовать извлечение результата typeid для хранимого в Variant значения. Это опять же можно сделать с помощью заранее созданной таблицы.

Наконец, требуется реализовать некий универсальный способ извлечения хранимого значения. Следуя Boost и C++17 реализуем шаблон проектирования “Посетитель” visitor . В качестве посетителя будет выступать функтор, принимающий любой из типов Types. Variant, получив такой функтор, вызовет его для типа хранимого значения. Нетрудно заметить, что деструктор, в общем-то, такой же посетитель, поэтому снова применим приём с таблицей функций-обёрток:

Для простоты кодирования возможный результат функтора-посетителя отбрасывается.

В этой статье показано, как использовать многоточие ( . ) в шаблонах C++ с переменным числом аргументов. Многоточие активно использовалось в C и C++. Они вводят переменные списки аргументов для функций. Одним из наиболее известных примеров является функция printf() из библиотеки времени выполнения C .

Вариативный шаблон — это шаблон класса или функции, который поддерживает произвольное количество аргументов. Этот механизм особенно удобен для разработчиков библиотек C++, поскольку его можно применить к как к шаблонам классов, так и к шаблонам функций. Таким образом, он предоставляет широкий спектр широкий спектр типобезопасных и нетривиальных функций и гибких возможностей.

Синтаксис

В шаблонах шаблонами с переменным числом аргументов многоточие используется двумя способами. Слева от имени параметра он обозначает пакет параметров, а справа от имени параметра расширяет пакеты параметров на отдельные имена.

Ниже приведен базовый пример синтаксиса определения класса variadic шаблона :

В обоих случаях (как при введении пакета параметров, так и при его развертывании) вокруг многоточия можно оставить пробельные символы, как показано в этом примере:

Обратите внимание, что в этой статье используется соглашение, показанное в первом примере (многоточие примыкает к имени типа — typename ).

В предыдущих примерах Аргументы являются пакетом параметров. Класс classname может принимать переменное число аргументов, как показано в следующих примерах:

Кроме того, определение шаблонного класса с переменным числом аргументов может устанавливать требование о том, что должен быть передан по меньшей мере один параметр:

Ниже приведен базовый пример синтаксиса функции шаблона variadic :

Затем пакет параметров Arguments развертывается для использования, как показано в следующем разделе о шаблонах variadic.

Возможны и другие формы синтаксиса шаблонной функции с переменным количеством аргументов возможны. Некоторые примеры приведены ниже.

Описатели, такие как const , также разрешены:

Шаблонные функции с переменным числом аргументов (как и аналогичные шаблонные классы) также могут устанавливать требование о том, что должен быть передан по меньшей мере один параметр.

В шаблонах с переменным числом аргументов используется оператор sizeof. () (он не имеет отношения к старому оператору sizeof() ).

Дополнительные сведения о положении многоточия

Выше в этой статье говорилось, что если многоточие определяет пакеты параметров и их развертывание, то "Слева от имени параметра оно означает пакет параметров, а справа от имени параметра оно служит для развертывания пакетов параметров в отдельные имена". Технически это верно, но может порождать неоднозначности при трансляции в код. При этом нужно учитывать указанные ниже особенности.

В списке параметров шаблона ( template ) typename. представлен пакет параметров шаблона.

В предложении parameter-declaration-clause ( func(parameter-list) ) многоточие верхнего уровня представляет пакет параметров функции, а положение многоточия важно:

Там, где многоточие стоит непосредственно за именем параметра, оно используется для развертывания пакета параметров.

Например, .

Механизм действия шаблонных функций с переменным числом аргументов можно проиллюстрировать на показательном примере — переписать с ее использованием какую-либо функциональность printf :

Выходные данные

Большинство реализаций, включающих функции шаблонов с вариативными шаблонами, используют рекурсию определенной формы, но она немного отличается от традиционной рекурсии. Традиционная рекурсия включает в себя функцию, вызываемую с помощью той же сигнатуры. (Она может быть перегружена или шаблонирована, но одна и та же сигнатура выбирается каждый раз.) Рекурсия с ариадией включает вызов шаблона функции variadic с помощью разных (почти всегда уменьшающихся) чисел аргументов и тем самым вымещает другую сигнатуру каждый раз. "Базовый случай" по-прежнему является обязательным, но природа рекурсии отличается.

Читайте также: