Что такое нетиповой параметр шаблона

Нетиповой параметр шаблона (non-type template parameter, NTTP) позволяет передать в шаблон значение, известное на этапе компиляции: например, размер массива, числовую константу, указатель на объект или функцию.

В объявлении такого параметра указывают его тип и имя: int Value, std::size_t N. Значение становится частью специализации шаблона. Для параметров-указателей и параметров-ссылок частью специализации является идентичность подходящего объекта, а не снимок его содержимого: сам объект во время выполнения может оставаться изменяемым.

Нетиповые параметры шаблонов классов

Шаблон класса может одновременно принимать типы и значения:

#include <array>
#include <cstddef>

template<typename T, std::size_t N = 16>
struct Buffer
{
    std::array<T, N> data{};
};

Buffer<int, 8> a;
Buffer<int, 16> b;
Buffer<double> c;  // N = 16

Параметр T задаёт тип элементов, а N — их количество. Для N указан аргумент шаблона по умолчанию 16, поэтому при его пропуске запись Buffer<double> означает Buffer<double, 16>.

При этом Buffer<int, 8 + 8> и Buffer<int, 16> обозначают один тип: после вычисления оба аргумента имеют одинаковое значение.

Нетиповые параметры шаблонов функций

Нетиповые параметры используются и в шаблонах функций:

template<int Value, typename T>
T addValue(T x)
{
    return x + Value;
}

auto a = addValue<5>(10);  // 15
auto b = addValue<5>(20);  // 25
auto c = addValue<7>(10);  // 17

В вызове addValue<5>(10):

  • 5 задаёт значение параметра шаблона Value;
  • 10 передаётся обычному параметру функции x;
  • тип T выводится компилятором как int.

Первые два вызова используют специализацию addValue<5, int>, третий — addValue<7, int>. Эти специализации неявно инстанцируются при использовании; писать для них явные специализации с template<> не требуется.

Значение обычного аргумента x, в отличие от Value, может определяться во время выполнения.

Передача функции в алгоритм

Полученную из шаблона функцию можно передать стандартному алгоритму:

Конкретная специализация как вызываемый объект

#include <algorithm>
#include <array>

template<int Value, typename T>
T addValue(T x)
{
    return x + Value;
}

int main()
{
    std::array<int, 3> values{1, 2, 3};

    std::transform(
        values.begin(), values.end(),
        values.begin(),
        addValue<5, int>
    );
    // values = {6, 7, 8}
}

std::transform принимает вызываемый объект и применяет его к каждому элементу диапазона. Здесь передаётся конкретная функция addValue<5, int> без круглых скобок.

Упрощённо основная операция алгоритма выглядит так:

*result = op(*first);

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

Вывод нетипового аргумента

Компилятор может вывести нетиповой аргумент из типа обычного аргумента функции:

#include <cstddef>

template<typename T, std::size_t N>
constexpr std::size_t arraySize(const T (&)[N])
{
    return N;
}

int values[]{10, 20, 30};

auto count = arraySize(values); // T = int, N = 3

Размер N выводится из типа массива int[3].

Ограничения нетиповых параметров

Константные выражения

Для числового нетипового параметра аргумент должен быть преобразованным константным выражением типа этого параметра. Иными словами, компилятор должен вычислить аргумент и допустимым образом привести его к объявленному типу параметра уже на этапе компиляции.

constexpr std::size_t n = 8;

Buffer<int, n * 2> a;  // OK

std::size_t size = 8;
// Buffer<int, size> b; // ошибка

Обычная изменяемая переменная size не образует константное выражение, даже если она инициализирована литералом.

Допустимые типы

В C++17 в качестве типов нетиповых параметров разрешены:

  • целочисленные типы и перечисления (int, bool, enum и другие);
  • указатели на объекты и функции;
  • указатели на члены классов;
  • lvalue-ссылки на объекты и функции;
  • std::nullptr_t.

Начиная с C++20 дополнительно разрешены типы с плавающей точкой и структурные классовые типы:

template<double Factor>
struct Scale {};

struct Point
{
    int x;
    int y;
};

template<Point P>
struct Marker {};

Scale<1.5> scale;              // C++20
Marker<Point{10, 20}> marker;  // C++20

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

Например, std::string не подходит для передачи по значению как NTTP.

Указатели и ссылки

Если аргумент шаблона представляет указатель на объект или ссылку на объект, он должен обозначать подходящий объект со статическим временем хранения. Таким объектом может быть глобальная переменная или локальная переменная, объявленная с static.

Начиная с C++20 снят общий запрет на указатели и ссылки на подобъекты — например, поля классов и элементы массивов:

int numbers[]{10, 20, 30};

struct Settings
{
    int limit;
};

Settings config{10};

template<int* P>
struct Pointer
{
    int* p = P;
};

int main()
{
    Pointer<&numbers[1]> a;    // C++20
    Pointer<&config.limit> b;  // C++20

    *a.p = 42; // numbers[1] = 42
    *b.p = 20; // config.limit = 20
}

Здесь numbers и config объявлены на уровне пространства имён, поэтому имеют статическое время хранения. В C++17 использование &numbers[1] и &config.limit в качестве нетиповых аргументов запрещено; в C++20 такие аргументы допустимы.

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

Например:

template<const char* Text>
struct Message {};

constexpr char text[] = "hello";

Message<text> a;          // OK
Message<&text[1]> b;      // C++20

// Message<"hello"> c;     // ошибка
// Message<&"hello"[1]> d; // ошибка

Само выражение взятия адреса символа строкового литерала корректно:

const char* p = &"hello"[1]; // адрес символа 'e'

Оно эквивалентно &("hello"[1]). Ошибка возникает именно при использовании такого адреса в качестве нетипового аргумента шаблона.

Выражения с оператором >

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

template<bool Flag>
struct Check {};

Check<(sizeof(int) > 4)> check;

Нетиповые параметры с auto

С C++17 тип нетипового параметра может выводиться автоматически:

template<auto Value>
struct Constant
{
    using value_type = decltype(Value);
};

Constant<42> a;    // int
Constant<42u> b;   // unsigned int
Constant<'A'> c;   // char
Constant<1.5> d;   // double, C++20

При использовании auto учитываются и тип, и значение аргумента. Поэтому Constant<42> и Constant<42u> — разные типы.

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

#include <type_traits>

static_assert(std::is_same_v<
    Buffer<int, 8>,
    Buffer<int, 8u>
>);

static_assert(!std::is_same_v<
    Constant<8>,
    Constant<8u>
>);

В шаблоне Buffer<T, N> параметр N объявлен как std::size_t, поэтому оба аргумента преобразуются к этому типу и дают значение 8. В шаблоне Constant<Value> тип Value выводится через auto, поэтому 8 и 8u приводят к разным специализациям.

Зависимость между параметрами

Тип параметра или аргумент шаблона по умолчанию может зависеть от предыдущих параметров:

template<auto Value, typename T = decltype(Value)>
T getValue()
{
    return Value;
}

template<typename T, T Value = T{}>
T getDefault()
{
    return Value;
}

В первом случае тип T по умолчанию определяется по типу Value:

getValue<5>(); // T = int, возвращает 5

Во втором случае значение Value по умолчанию создаётся выражением T{}:

getDefault<int>(); // Value = 0, возвращает 0

Нетиповые параметры с decltype(auto)

С C++17 вместо auto можно использовать decltype(auto). Такой параметр выводит тип по правилам decltype и поэтому может сохранить ссылочный тип аргумента.

Сначала рассмотрим разницу между двумя формами decltype:

int counter = 0;

using CounterType = decltype(counter);          // int
using CounterReference = decltype((counter));   // int&

decltype(counter) возвращает объявленный тип переменной — int. decltype((counter)) анализирует выражение; поскольку counter является lvalue, результатом будет int&.

Эти правила применяются и к нетиповым параметрам шаблона:

Скобки меняют выводимый тип

template<decltype(auto) Value>
struct Holder
{
    void set(int x)
    {
        Value = x;
    }
};

int main()
{
    static int counter = 0;

    // Holder<counter> a; // ошибка: Value выводится как int
    Holder<(counter)> b;  // OK: Value имеет тип int&

    b.set(42);             // counter = 42
}

Без скобок тип Value выводится как int. Тогда компилятору требуется константное значение counter, но переменная изменяемая, поэтому возникает ошибка.

Со скобками тип Value выводится как int&. Специализация получает ссылку на конкретный объект, значение которого может изменяться во время выполнения.

Объект, передаваемый таким образом, должен иметь статическое время хранения. Именно поэтому counter объявлен с static. Если убрать это слово, обычная локальная переменная с автоматическим временем хранения не подойдёт:

int main()
{
    int counter = 0;

    // Holder<(counter)> b; // ошибка
}

Что нужно запомнить

  • Нетиповой аргумент — это значение, которое участвует в формировании специализации шаблона.
  • Числовой аргумент должен быть преобразованным константным выражением типа соответствующего параметра.
  • При явно заданном типе параметра аргумент преобразуется к нему; при auto тип выводится и тоже отличает специализации.
  • Компилятор умеет выводить нетиповые аргументы из типов обычных аргументов функции, как размер массива N.
  • Набор допустимых типов расширен в C++20: появились числа с плавающей точкой и структурные классовые типы.
  • Указатели и ссылки фиксируют идентичность объекта, но не делают его содержимое неизменяемым.
  • decltype(auto) сохраняет различие между значением и ссылкой, поэтому скобки вокруг выражения могут изменить результат вывода.