Аргументы шаблона по умолчанию
Параметру шаблона класса можно назначить аргумент по умолчанию. Если вызывающий код не передаст соответствующий аргумент, компилятор подставит указанное значение.
Добавим в Stack второй параметр Cont, определяющий тип внутреннего контейнера. По умолчанию стек будет использовать std::vector<T>:
#include <cassert>
#include <type_traits>
#include <vector>
template<typename T, typename Cont = std::vector<T>>
class Stack
{
static_assert(
std::is_same<T, typename Cont::value_type>::value,
"Cont::value_type must be T"
);
public:
using value_type = T;
using container_type = Cont;
using const_reference = typename container_type::const_reference;
void push(const T& value);
void pop()
{
assert(!elems.empty());
elems.pop_back();
}
const_reference top() const
{
assert(!elems.empty());
return elems.back();
}
bool empty() const
{
return elems.empty();
}
private:
container_type elems;
};
template<typename T, typename Cont>
void Stack<T, Cont>::push(const T& value)
{
elems.push_back(value);
}
В выражении Stack<int> указан только первый аргумент:
Stack<int> stack;
Компилятор подставляет параметры следующим образом:
T = int
Cont = std::vector<int>
Аргумент по умолчанию может зависеть от параметров, объявленных левее. Поэтому std::vector<T> может использовать уже известный параметр T.
Тип контейнера разрешается задать явно:
#include <deque>
Stack<int, std::deque<int>> stack;
В этом случае параметры получают другие значения:
T = int
Cont = std::deque<int>
В списке параметров определения метода за пределами класса значение по умолчанию указывать нельзя. В заголовке template<typename T, typename Cont> перечисляются параметры самого шаблона, а полное имя класса записывается как Stack<T, Cont>.
Нормативные правила приведены в разделе стандарта об аргументах шаблонов по умолчанию.
Правила объявления значений по умолчанию
Для параметров шаблона класса действует правило следования: после параметра со значением по умолчанию каждый следующий параметр также должен иметь значение по умолчанию, за исключением завершающего пакета параметров.
#include <memory>
#include <vector>
template<
typename T,
typename Cont = std::vector<T>,
typename Alloc = std::allocator<T>
>
class Storage;
Следующее объявление, напротив, было бы некорректным:
// Ошибка: после Cont со значением по умолчанию
// расположен обязательный параметр Alloc.
template<
typename T,
typename Cont = std::vector<T>,
typename Alloc
>
class InvalidStorage;
Значения по умолчанию могут накапливаться в нескольких объявлениях одного шаблона в одной области видимости. Компилятор рассматривает их совместно:
#include <memory>
#include <vector>
template<
typename T,
typename Cont,
typename Alloc = std::allocator<T>
>
class Storage;
template<
typename T,
typename Cont = std::vector<T>,
typename Alloc
>
class Storage
{
};
После объединения объявлений у Cont и Alloc есть значения по умолчанию. Однако одному и тому же параметру нельзя назначать значение по умолчанию повторно в той же области видимости - даже если повторяется то же самое выражение:
template<typename T, typename Cont = std::vector<T>>
class Queue;
// Ошибка: значение по умолчанию для Cont уже было задано.
// template<typename T, typename Cont = std::vector<T>>
// class Queue
// {
// };
В списке параметров шаблона при определении члена шаблонного класса вне класса значения по умолчанию указывать запрещено:
template<typename T, typename Cont>
void Stack<T, Cont>::push(const T& value)
{
elems.push_back(value);
}
Значения по умолчанию запрещено указывать и в списке параметров частичной специализации:
template<typename T, typename U>
class PairBox;
// Корректная форма частичной специализации:
template<typename T, typename U>
class PairBox<T*, U>;
// Значение по умолчанию в списке параметров недопустимо:
// template<typename T, typename U = int>
// class PairBox<T*, U>;
Требования к параметру Cont
Параметр Cont нельзя заменить совершенно произвольным типом. Реализация Stack использует определённый интерфейс контейнера:
elems.push_back(value);
elems.pop_back();
elems.back();
elems.empty();
Следовательно, для задействованных методов переданный тип должен:
- содержать вложенные типы
value_typeиconst_reference; - поддерживать добавление значения с помощью
push_back(); - поддерживать удаление последнего элемента с помощью
pop_back(); - предоставлять последний элемент через
back()для константного контейнера; - позволять проверять пустоту вызовом
empty()для константного контейнера.
Кроме наличия операций важна их смысловая совместимость. В этой реализации предполагается, что Cont::value_type в точности совпадает с T. Это условие проверяет static_assert в определении класса.
Совместимые контейнеры
Stack<int, std::vector<int>> vectorStack;
Stack<int, std::deque<int>> dequeStack;
Оба контейнера хранят int и предоставляют операции, необходимые этой реализации стека.
Если объект Stack создаётся конструктором по умолчанию, его контейнер также должен допускать соответствующую инициализацию. Для другой стратегии создания контейнера потребовались бы дополнительные конструкторы самого Stack.
Зависимые имена и typename
Тип результата top() берётся из контейнера:
using const_reference = typename Cont::const_reference;
Имя Cont::const_reference является зависимым: конкретный тип Cont станет известен только после подстановки аргументов шаблона. На этапе разбора шаблона компилятор в общем случае не может заранее определить, обозначает ли вложенное имя тип, статическое поле или другой член.
Ключевое слово typename явно сообщает, что зависимое квалифицированное имя обозначает тип:
template<typename Cont>
using Iterator = typename Cont::iterator;
Псевдоним зависимого типа
#include <vector>
std::vector<int> values{1, 2, 3};
Iterator<std::vector<int>> it = values.begin();
После подстановки Cont = std::vector<int> псевдоним обозначает тип std::vector<int>::iterator.
Во всех показанных объявлениях псевдонимов правая часть после знака = является определяющим type-id - контекстом, в котором ожидается тип. В C++11–C++17 перед зависимым квалифицированным именем в такой записи требуется typename. Начиная с C++20, именно в этом контексте typename разрешено опустить. В примерах оно сохранено для совместимости с C++11–C++17 и для явного обозначения типа.
Это послабление C++20 не отменяет общие правила зависимых имён: в других контекстах необходимость typename определяется отдельно.
Подробнее зависимые имена рассматриваются в разделе стандарта о разрешении имён в шаблонах.
Псевдонимы типов
Длинному имени типа можно назначить более короткое имя с помощью объявления using:
using IntStack = Stack<int>;
using IntDequeStack = Stack<int, std::deque<int>>;
После этого записи с псевдонимом и с исходным типом эквивалентны:
IntStack first;
Stack<int> second;
IntDequeStack third;
Stack<int, std::deque<int>> fourth;
Псевдоним не создаёт новый тип. Например, IntStack и Stack<int> имеют одну и ту же идентичность типа, поэтому между ними нет отдельного преобразования.
Для обычного, нешаблонного псевдонима доступен и исторический синтаксис typedef:
typedef Stack<int, std::deque<int>> IntDequeStackOld;
Обе формы создают псевдоним существующего типа. Синтаксис using обычно читается слева направо и лучше подходит для более сложных объявлений.
Правила таких объявлений описаны в разделе стандарта о именах, вводимых объявлениями typedef и alias-declaration.
Шаблоны псевдонимов
Псевдоним может зависеть от параметров шаблона. Например, можно зафиксировать std::deque в качестве контейнера стека и оставить изменяемым только тип элементов:
#include <deque>
template<typename T>
using DequeStack = Stack<T, std::deque<T>>;
Такое объявление называется шаблоном псевдонима, или alias template.
DequeStack<int> numbers;
DequeStack<double> measurements;
После подстановки DequeStack<int> обозначает тот же тип, что и Stack<int, std::deque<int>>.
Шаблоны псевдонимов особенно удобны, когда часть аргументов большого шаблона постоянно повторяется:
#include <map>
#include <string>
template<typename T>
using StringMap = std::map<std::string, T>;
StringMap<int> counters;
StringMap<std::string> descriptions;
Запись StringMap<int> обозначает std::map<std::string, int>.
Шаблоны псевдонимов нельзя полностью или частично специализировать:
template<typename T>
using Pointer = T*;
// Недопустимая попытка полной специализации:
// template<>
// using Pointer<void> = const void*;
Если требуется специализируемое вычисление типа, его обычно помещают во вспомогательный шаблон класса, который разрешено специализировать, а затем предоставляют удобный псевдоним для его вложенного типа.
Определение и семантика таких объявлений приведены в разделе стандарта о шаблонах псевдонимов, а запрет их полной и частичной специализации - в общих правилах объявлений шаблонов.
Псевдонимы вложенных типов
Классы стандартной библиотеки часто предоставляют вложенные типы:
std::vector<int>::value_type
std::vector<int>::iterator
std::vector<int>::const_reference
Шаблон псевдонима может извлекать такой тип из произвольного подходящего класса:
template<typename Cont>
using Iterator = typename Cont::iterator;
template<typename Cont>
using Element = typename Cont::value_type;
using IntIterator = Iterator<std::vector<int>>;
using IntElement = Element<std::vector<int>>;
IntIterator обозначает std::vector<int>::iterator, а IntElement - int. И здесь создаются только альтернативные имена существующих типов, а не новые типы.
Псевдонимы внутри шаблонного класса
В определении Stack уже объявлены три публичных псевдонима:
using value_type = T;
using container_type = Cont;
using const_reference = typename container_type::const_reference;
Они делают интерфейс класса удобнее для обобщённого кода. Внешний код может получить связанные со стеком типы, не воспроизводя его аргументы и внутренние правила:
using Value = Stack<int>::value_type;
using Container = Stack<int>::container_type;
using Reference = Stack<int>::const_reference;
В результате:
Value // int
Container // std::vector<int>
Reference // std::vector<int>::const_reference
Если содержащий тип сам зависит от параметра шаблона, в C++11–C++17 перед его вложенным типом в правой части такого объявления псевдонима требуется typename. Начиная с C++20, в этом определяющем type-id его можно опустить. Для совместимости сохраним явную форму:
template<typename StackType>
using StackValue = typename StackType::value_type;
using Value = StackValue<Stack<int>>;
Подобные имена - value_type, iterator, reference и другие - составляют привычную часть интерфейсов стандартных контейнеров и упрощают написание алгоритмов, не зависящих от конкретного класса.
Стандартные псевдонимы с суффиксом _t
Многие шаблоны из заголовка <type_traits>, вычисляющие тип, предоставляют результат через вложенный член type. Например, собственный псевдоним можно записать так:
#include <type_traits>
template<typename T>
using AddConstLegacy = typename std::add_const<T>::type;
Начиная с C++14, стандартная библиотека предоставляет для многих таких преобразований готовые псевдонимы с суффиксом _t:
template<typename T>
using AddConst = std::add_const_t<T>;
using ConstInt = AddConst<int>; // const int
Следующие пары псевдонимов обозначают одинаковые типы:
template<typename T>
using AddConstLong = typename std::add_const<T>::type;
template<typename T>
using AddConstShort = std::add_const_t<T>;
template<typename T1, typename T2>
using CommonTypeLong = typename std::common_type<T1, T2>::type;
template<typename T1, typename T2>
using CommonTypeShort = std::common_type_t<T1, T2>;
template<typename T>
using DecayLong = typename std::decay<T>::type;
template<typename T>
using DecayShort = std::decay_t<T>;
Формы с _t сокращают запись и избавляют вызывающий код от непосредственного обращения к вложенному type. При этом они не создают новые типы: это стандартные шаблоны псевдонимов для результатов соответствующих type traits.
Состав стандартных преобразований типов определён в разделе стандарта о шаблонах преобразования типов.
Что нужно запомнить
- Аргумент по умолчанию подставляется, когда соответствующий аргумент шаблона не указан явно.
- Значение по умолчанию может использовать параметры, объявленные левее.
Stack<int>опускает параметрCont, ноStack<>некорректен, пока уTнет значения по умолчанию.- После параметра со значением по умолчанию следующие параметры шаблона класса также должны иметь значения по умолчанию, кроме завершающего пакета параметров.
- Значения по умолчанию могут накапливаться в нескольких объявлениях, но одно значение нельзя повторно задавать одному параметру в той же области видимости.
- Значения по умолчанию запрещено указывать в списке параметров определения члена шаблонного класса вне класса и в списке параметров частичной специализации.
- Тип, переданный как
Cont, должен не только содержать нужные операции, но и быть смыслово совместимым с типом элементовT. - В показанных объявлениях псевдонимов зависимое квалифицированное имя требует
typenameв C++11–C++17; начиная с C++20 его разрешено опустить. usingиtypedefдают существующему типу другое имя, а не создают новый тип.- Шаблоны псевдонимов объявляются с помощью
usingи не допускают полной или частичной специализации. - Стандартные сокращения с суффиксом
_tдоступны начиная с C++14.