Поиск…
Вступление
Классы, функции и (поскольку C ++ 14) могут быть шаблонами. Шаблон представляет собой фрагмент кода с некоторыми свободными параметрами, которые станут конкретным классом, функцией или переменной, когда будут указаны все параметры. Параметры могут быть типами, значениями или самими шаблонами. Известным шаблоном является std::vector
, который становится конкретным типом контейнера, когда указан тип элемента, например std::vector<int>
.
Синтаксис
- шаблон < шаблон-параметр-список > декларация
- export template < template-parameter-list > объявление / * до C ++ 11 * /
- шаблон <> Объявление
- объявление шаблона
- объявление шаблона extern / *, поскольку C ++ 11 * /
- template < template-parameter-list > class ... ( opt ) identifier ( opt )
- template < template-parameter-list > идентификатор класса ( opt ) = id-expression
- template < template-parameter-list > typename ... ( opt ) identifier ( opt ) / *, поскольку C ++ 17 * /
- template < template-parameter-list > typename identifier ( opt ) = id-expression / *, поскольку C ++ 17 * /
- postfix-expression . Идентификатор шаблона
- postfix-expression -> идентификатор шаблона
-
template
вложенного имени-спецификатора simple-template-id::
замечания
template
слова - это ключевое слово с пятью различными значениями на языке C ++, в зависимости от контекста.
Когда за ним следует список параметров шаблона, заключенных в
<>
, он объявляет шаблон, такой как шаблон класса, шаблон функции или частичную специализацию существующего шаблона.template <class T> void increment(T& x) { ++x; }
Когда следует пустой
<>
, он объявляет явную (полную) специализацию .template <class T> void print(T x); template <> // <-- keyword used in this sense here void print(const char* s) { // output the content of the string printf("%s\n", s); }
При последующей декларации без
<>
, она образует явное инстанцирование декларацию или определение.template <class T> std::set<T> make_singleton(T x) { return std::set<T>(x); } template std::set<int> make_singleton(int x); // <-- keyword used in this sense here
В списке параметров шаблона он вводит параметр шаблона шаблона .
template <class T, template <class U> class Alloc> // ^^^^^^^^ keyword used in this sense here class List { struct Node { T value; Node* next; }; Alloc<Node> allocator; Node* allocate_node() { return allocator.allocate(sizeof(T)); } // ... };
После оператора разрешения области видимости
::
и операторов доступа к классу.
и->
, он указывает, что следующее имя является шаблоном.struct Allocator { template <class T> T* allocate(); }; template <class T, class Alloc> class List { struct Node { T value; Node* next; } Alloc allocator; Node* allocate_node() { // return allocator.allocate<Node>(); // error: < and > are interpreted as // comparison operators return allocator.template allocate<Node>(); // ok; allocate is a template // ^^^^^^^^ keyword used in this sense here } };
Перед C ++ 11 шаблон можно было объявить с ключевым словом export
, превратив его в экспортированный шаблон. Определение экспортируемого шаблона не обязательно должно присутствовать в каждой единицы перевода, в которой создается экземпляр шаблона. Например, должно было работать следующее:
foo.h
:
#ifndef FOO_H
#define FOO_H
export template <class T> T identity(T x);
#endif
foo.cpp
:
#include "foo.h"
template <class T> T identity(T x) { return x; }
main.cpp
:
#include "foo.h"
int main() {
const int x = identity(42); // x is 42
}
Из-за сложности выполнения ключевое слово export
не поддерживалось большинством основных компиляторов. Он был удален в C ++ 11; В настоящее время запрещено использовать ключевое слово export
вообще. Вместо этого обычно необходимо определять шаблоны в заголовках (в отличие от функций без шаблонов, которые обычно не определены в заголовках). См. Почему шаблоны могут быть реализованы только в файле заголовка?
Шаблоны функций
Шаблон может также применяться к функциям (а также к более традиционным структурам) с тем же эффектом.
// 'T' stands for the unknown type
// Both of our arguments will be of the same type.
template<typename T>
void printSum(T add1, T add2)
{
std::cout << (add1 + add2) << std::endl;
}
Затем это можно использовать так же, как шаблоны структуры.
printSum<int>(4, 5);
printSum<float>(4.5f, 8.9f);
В обоих случаях аргумент шаблона используется для замены типов параметров; результат работает так же, как и обычная C ++-функция (если параметры не соответствуют типу шаблона, то компилятор применяет стандартные преобразования).
Еще одно свойство шаблонных функций (в отличие от классов шаблонов) заключается в том, что компилятор может вывести параметры шаблона на основе параметров, переданных функции.
printSum(4, 5); // Both parameters are int.
// This allows the compiler deduce that the type
// T is also int.
printSum(5.0, 4); // In this case the parameters are two different types.
// The compiler is unable to deduce the type of T
// because there are contradictions. As a result
// this is a compile time error.
Эта функция позволяет нам упростить код, когда мы объединяем структуры и функции шаблонов. В стандартной библиотеке существует общий шаблон, который позволяет нам создавать template structure X
с помощью вспомогательной функции make_X()
.
// The make_X pattern looks like this.
// 1) A template structure with 1 or more template types.
template<typename T1, typename T2>
struct MyPair
{
T1 first;
T2 second;
};
// 2) A make function that has a parameter type for
// each template parameter in the template structure.
template<typename T1, typename T2>
MyPair<T1, T2> make_MyPair(T1 t1, T2 t2)
{
return MyPair<T1, T2>{t1, t2};
}
Как это помогает?
auto val1 = MyPair<int, float>{5, 8.7}; // Create object explicitly defining the types
auto val2 = make_MyPair(5, 8.7); // Create object using the types of the paramters.
// In this code both val1 and val2 are the same
// type.
Примечание. Это не предназначено для сокращения кода. Это сделано для того, чтобы сделать код более надежным. Он позволяет изменять типы, изменяя код в одном месте, а не в нескольких местах.
Переадресация аргументов
Шаблон может принимать как значения lvalue, так и rvalue с использованием ссылки пересылки :
template <typename T>
void f(T &&t);
В этом случае реальный тип t
будет выведен в зависимости от контекста:
struct X { };
X x;
f(x); // calls f<X&>(x)
f(X()); // calls f<X>(x)
В первом случае тип T
выводится как ссылка на X
( X&
), а тип t
является ссылкой на X
, а во втором случае тип T
выводится как X
и тип t
качестве ссылки на rvalue к X
( X&&
).
Примечание. Стоит заметить, что в первом случае decltype(t)
совпадает с T
, но не во втором.
Чтобы отлично перевести t
в другую функцию, будь то ссылка lvalue или rvalue, нужно использовать std::forward
:
template <typename T>
void f(T &&t) {
g(std::forward<T>(t));
}
Ссылки на пересылку могут использоваться с вариационными шаблонами:
template <typename... Args>
void f(Args&&... args) {
g(std::forward<Args>(args)...);
}
Примечание. Ссылки на перенаправление могут использоваться только для параметров шаблона, например, в следующем коде, v
является ссылкой на rvalue, а не ссылкой на пересылку:
#include <vector>
template <typename T>
void f(std::vector<T> &&v);
Шаблон базового класса
Основная идея шаблона класса заключается в том, что параметр шаблона заменяется типом во время компиляции. В результате один и тот же класс может быть повторно использован для нескольких типов. Пользователь указывает, какой тип будет использоваться при объявлении переменной класса. Три примера этого показаны в main()
:
#include <iostream>
using std::cout;
template <typename T> // A simple class to hold one number of any type
class Number {
public:
void setNum(T n); // Sets the class field to the given number
T plus1() const; // returns class field's "follower"
private:
T num; // Class field
};
template <typename T> // Set the class field to the given number
void Number<T>::setNum(T n) {
num = n;
}
template <typename T> // returns class field's "follower"
T Number<T>::plus1() const {
return num + 1;
}
int main() {
Number<int> anInt; // Test with an integer (int replaces T in the class)
anInt.setNum(1);
cout << "My integer + 1 is " << anInt.plus1() << "\n"; // Prints 2
Number<double> aDouble; // Test with a double
aDouble.setNum(3.1415926535897);
cout << "My double + 1 is " << aDouble.plus1() << "\n"; // Prints 4.14159
Number<float> aFloat; // Test with a float
aFloat.setNum(1.4);
cout << "My float + 1 is " << aFloat.plus1() << "\n"; // Prints 2.4
return 0; // Successful completion
}
Шаблонная специализация
Вы можете определить реализацию для конкретных экземпляров класса шаблона / метода.
Например, если у вас есть:
template <typename T>
T sqrt(T t) { /* Some generic implementation */ }
Затем вы можете написать:
template<>
int sqrt<int>(int i) { /* Highly optimized integer implementation */ }
Тогда пользователь, который пишет sqrt(4.0)
, получит общую реализацию, тогда как sqrt(4)
получит специализированную реализацию.
Специализация частичного шаблона
В отличие от полной специализированной специализированной специализации шаблона шаблона, можно ввести шаблон с некоторыми аргументами существующего шаблона. Специализация частичного шаблона доступна только для шаблона class / structs:
// Common case:
template<typename T, typename U>
struct S {
T t_val;
U u_val;
};
// Special case when the first template argument is fixed to int
template<typename V>
struct S<int, V> {
double another_value;
int foo(double arg) {// Do something}
};
Как показано выше, частичные специализированные шаблоны могут вводить совершенно разные наборы данных и членов функций.
Когда создается экземпляр частично специализированного шаблона, выбирается наиболее подходящая специализация. Например, давайте определим шаблон и две частичные специализации:
template<typename T, typename U, typename V>
struct S {
static void foo() {
std::cout << "General case\n";
}
};
template<typename U, typename V>
struct S<int, U, V> {
static void foo() {
std::cout << "T = int\n";
}
};
template<typename V>
struct S<int, double, V> {
static void foo() {
std::cout << "T = int, U = double\n";
}
};
Теперь следующие вызовы:
S<std::string, int, double>::foo();
S<int, float, std::string>::foo();
S<int, double, std::string>::foo();
распечатает
General case
T = int
T = int, U = double
Шаблоны функций могут быть полностью специализированными:
template<typename T, typename U>
void foo(T t, U u) {
std::cout << "General case: " << t << " " << u << std::endl;
}
// OK.
template<>
void foo<int, int>(int a1, int a2) {
std::cout << "Two ints: " << a1 << " " << a2 << std::endl;
}
void invoke_foo() {
foo(1, 2.1); // Prints "General case: 1 2.1"
foo(1,2); // Prints "Two ints: 1 2"
}
// Compilation error: partial function specialization is not allowed.
template<typename U>
void foo<std::string, U>(std::string t, U u) {
std::cout << "General case: " << t << " " << u << std::endl;
}
Значение параметра шаблона по умолчанию
Как и в случае аргументов функции, параметры шаблона могут иметь значения по умолчанию. Все параметры шаблона со значением по умолчанию должны быть объявлены в конце списка параметров шаблона. Основная идея заключается в том, что параметры шаблона со значением по умолчанию могут быть опущены при создании шаблона.
Простой пример использования значения параметра шаблона по умолчанию:
template <class T, size_t N = 10>
struct my_array {
T arr[N];
};
int main() {
/* Default parameter is ignored, N = 5 */
my_array<int, 5> a;
/* Print the length of a.arr: 5 */
std::cout << sizeof(a.arr) / sizeof(int) << std::endl;
/* Last parameter is omitted, N = 10 */
my_array<int> b;
/* Print the length of a.arr: 10 */
std::cout << sizeof(b.arr) / sizeof(int) << std::endl;
}
Шаблон псевдонимов
Основной пример:
template<typename T> using pointer = T*;
Это определение делает pointer<T>
псевдоним T*
. Например:
pointer<int> p = new int; // equivalent to: int* p = new int;
Шаблоны псевдонимов не могут быть специализированными. Однако эту функцию можно получить косвенно, если они ссылаются на вложенный тип в структуре:
template<typename T>
struct nonconst_pointer_helper { typedef T* type; };
template<typename T>
struct nonconst_pointer_helper<T const> { typedef T* type; };
template<typename T> using nonconst_pointer = nonconst_pointer_helper<T>::type;
Параметры шаблона шаблона
Иногда мы хотели бы перейти в шаблон типа шаблона без фиксации его значений. Для этого создаются параметры шаблона шаблона. Очень простые примеры шаблонов шаблонов:
template <class T>
struct Tag1 { };
template <class T>
struct Tag2 { };
template <template <class> class Tag>
struct IntTag {
typedef Tag<int> type;
};
int main() {
IntTag<Tag1>::type t;
}
#include <vector>
#include <iostream>
template <class T, template <class...> class C, class U>
C<T> cast_all(const C<U> &c) {
C<T> result(c.begin(), c.end());
return result;
}
int main() {
std::vector<float> vf = {1.2, 2.6, 3.7};
auto vi = cast_all<int>(vf);
for(auto &&i: vi) {
std::cout << i << std::endl;
}
}
Объявление аргументов типа non-type с авто
До C ++ 17, когда вы пишете параметр непигового шаблона, вам нужно сначала указать его тип. Таким образом, общая картина стала писать что-то вроде:
template <class T, T N>
struct integral_constant {
using type = T;
static constexpr T value = N;
};
using five = integral_constant<int, 5>;
Но для сложных выражений, используя что-то вроде этого, приходится писать decltype(expr), expr
при создании шаблонов. Решение состоит в том, чтобы упростить эту идиому и просто разрешить auto
:
template <auto N>
struct integral_constant {
using type = decltype(N);
static constexpr type value = N;
};
using five = integral_constant<5>;
Пустой пользовательский отладчик для unique_ptr
Хороший пример мотивации может прийти от попыток объединить пустую оптимизацию базы с пользовательской Deleter для unique_ptr
. Различные C API-дескрипторы имеют разные типы возврата, но нам все равно - мы просто хотим, чтобы что-то работало для любой функции:
template <auto DeleteFn>
struct FunctionDeleter {
template <class T>
void operator()(T* ptr) const {
DeleteFn(ptr);
}
};
template <T, auto DeleteFn>
using unique_ptr_deleter = std::unique_ptr<T, FunctionDeleter<DeleteFn>>;
И теперь вы можете просто использовать любой указатель на функцию, который может принимать аргумент типа T
в качестве параметра шаблона без типа, независимо от типа возвращаемого значения, и получать из него нестандартные служебные данные unique_ptr
:
unique_ptr_deleter<std::FILE, std::fclose> p;
Параметр шаблона непигового типа
Помимо типов в качестве параметра шаблона нам разрешено объявлять значения константных выражений, удовлетворяющие одному из следующих критериев:
- интегрального или перечисляемого типа,
- указатель на объект или указатель на функцию,
- lvalue ссылка на объект или lvalue ссылку на функцию,
- указатель на член,
-
std::nullptr_t
.
Как и все параметры шаблона, параметры шаблона непигового типа могут быть явно заданы, дефолтны или производятся неявно через вычитание аргумента шаблона.
Пример использования параметров шаблона непигового типа:
#include <iostream>
template<typename T, std::size_t size>
std::size_t size_of(T (&anArray)[size]) // Pass array by reference. Requires.
{ // an exact size. We allow all sizes
return size; // by using a template "size".
}
int main()
{
char anArrayOfChar[15];
std::cout << "anArrayOfChar: " << size_of(anArrayOfChar) << "\n";
int anArrayOfData[] = {1,2,3,4,5,6,7,8,9};
std::cout << "anArrayOfData: " << size_of(anArrayOfData) << "\n";
}
Пример явного указания параметров шаблона типа и не-типа:
#include <array>
int main ()
{
std::array<int, 5> foo; // int is a type parameter, 5 is non-type
}
Нестандартные параметры шаблона являются одним из способов достижения повторения шаблона и позволяют выполнять метапрограммирование .
Структуры данных шаблонов Variadic
Часто бывает полезно определить классы или структуры, которые имеют переменное число и тип элементов данных, которые определены во время компиляции. Канонический пример - std::tuple
, но иногда необходимо определить собственные пользовательские структуры. Вот пример, который определяет структуру с использованием рецептуры (а не наследования, как и для std::tuple
. Начните с общего (пустого) определения, которое также служит базовым случаем для завершения рекрузии в поздней специализации:
template<typename ... T>
struct DataStructure {};
Это уже позволяет нам определить пустую структуру DataStructure<> data
, хотя это еще не очень полезно.
Далее идет рекурсивная специализация случая:
template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>
{
DataStructure(const T& first, const Rest& ... rest)
: first(first)
, rest(rest...)
{}
T first;
DataStructure<Rest ... > rest;
};
Теперь нам достаточно создавать произвольные структуры данных, такие как DataStructure<int, float, std::string> data(1, 2.1, "hello")
.
Так, что происходит? Во-первых, обратите внимание, что это специализация, требование которой состоит в том, что существует хотя бы один параметр вариационного шаблона (а именно T
выше), но не заботятся о конкретном составе пакета Rest
. Знание того, что T
существует, позволяет first
определить его член данных. Остальные данные рекурсивно упаковываются как DataStructure<Rest ... > rest
. Конструктор инициирует оба этих элемента, включая вызов рекурсивного конструктора для rest
членов.
Чтобы понять это лучше, мы можем работать с примером: предположим, что у вас есть данные DataStructure<int, float> data
. Объявление сначала совпадает со специализацией, DataStructure<float> rest
структуру с DataStructure<float> rest
int first
и DataStructure<float> rest
. rest
определение снова совпадает с этой специализацией, создавая float first
свои собственные float first
и DataStructure<> rest
. Наконец, последний rest
совпадает с деформированием базового блока, создавая пустую структуру.
Вы можете визуализировать это следующим образом:
DataStructure<int, float>
-> int first
-> DataStructure<float> rest
-> float first
-> DataStructure<> rest
-> (empty)
Теперь у нас есть структура данных, но она не очень полезна, но мы не можем легко получить доступ к отдельным элементам данных (например, чтобы получить доступ к последнему члену данных DataStructure<int, float, std::string> data
нам пришлось бы использовать data.rest.rest.first
, что не совсем удобно). Таким образом, мы добавляем к нему метод get
(нужен только в специализации, так как базовая структура не имеет данных для get
):
template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>
{
...
template<size_t idx>
auto get()
{
return GetHelper<idx, DataStructure<T,Rest...>>::get(*this);
}
...
};
Как вы можете видеть это get
функцию члена сама шаблонным - на этот раз по индексу элемента , который необходим ( и поэтому использование может быть что - то вроде data.get<1>()
, аналогичен std::tuple
). Фактическая работа выполняется статической функцией в вспомогательном классе GetHelper
. Причина , почему мы не можем определить необходимые функции непосредственно в DataStructure
«s get
потому , что (как мы скоро увидим) , мы должны были бы специализироваться на idx
- но это не возможно специализироваться функциями - члена шаблона без Специализируя содержащий класс шаблон. Обратите также внимание на то, что использование C + + 14-стиля auto
делает нашу жизнь значительно проще, поскольку в противном случае нам понадобится довольно сложное выражение для типа возврата.
Итак, на вспомогательный класс. На этот раз нам понадобится пустая декларация и две специализации. Сначала заявление:
template<size_t idx, typename T>
struct GetHelper;
Теперь базовый регистр (когда idx==0
). В этом случае мы просто возвращаем first
члена:
template<typename T, typename ... Rest>
struct GetHelper<0, DataStructure<T, Rest ... >>
{
static T get(DataStructure<T, Rest...>& data)
{
return data.first;
}
};
В рекурсивном случае мы уменьшаем idx
и вызываем GetHelper
для rest
членов:
template<size_t idx, typename T, typename ... Rest>
struct GetHelper<idx, DataStructure<T, Rest ... >>
{
static auto get(DataStructure<T, Rest...>& data)
{
return GetHelper<idx-1, DataStructure<Rest ...>>::get(data.rest);
}
};
Для работы с примером предположим, что у нас есть данные DataStructure<int, float> data
и нам нужен data.get<1>()
. Это вызывает GetHelper<1, DataStructure<int, float>>::get(data)
(вторая специализация), которая, в свою очередь, вызывает GetHelper<0, DataStructure<float>>::get(data.rest)
, который, наконец, возвращает (по 1-й специализации, так как теперь idx
равен 0) data.rest.first
.
Итак, это все! Вот весь код функционирования, с некоторым примером используемого в main
функции:
#include <iostream>
template<size_t idx, typename T>
struct GetHelper;
template<typename ... T>
struct DataStructure
{
};
template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>
{
DataStructure(const T& first, const Rest& ... rest)
: first(first)
, rest(rest...)
{}
T first;
DataStructure<Rest ... > rest;
template<size_t idx>
auto get()
{
return GetHelper<idx, DataStructure<T,Rest...>>::get(*this);
}
};
template<typename T, typename ... Rest>
struct GetHelper<0, DataStructure<T, Rest ... >>
{
static T get(DataStructure<T, Rest...>& data)
{
return data.first;
}
};
template<size_t idx, typename T, typename ... Rest>
struct GetHelper<idx, DataStructure<T, Rest ... >>
{
static auto get(DataStructure<T, Rest...>& data)
{
return GetHelper<idx-1, DataStructure<Rest ...>>::get(data.rest);
}
};
int main()
{
DataStructure<int, float, std::string> data(1, 2.1, "Hello");
std::cout << data.get<0>() << std::endl;
std::cout << data.get<1>() << std::endl;
std::cout << data.get<2>() << std::endl;
return 0;
}
Явная реализация
Явное определение экземпляра создает и объявляет конкретный класс, функцию или переменную из шаблона, но пока не использует его. На явное создание можно ссылаться на другие единицы перевода. Это можно использовать, чтобы избежать определения шаблона в файле заголовка, если он будет создан только с помощью конечного набора аргументов. Например:
// print_string.h
template <class T>
void print_string(const T* str);
// print_string.cpp
#include "print_string.h"
template void print_string(const char*);
template void print_string(const wchar_t*);
Поскольку print_string<char>
и print_string<wchar_t>
явно создаются в print_string.cpp
, компоновщик сможет их найти, даже если шаблон print_string
не определен в заголовке. Если эти объявления с явным манифестом не присутствуют, вероятно, возникнет ошибка компоновщика. См. Почему шаблоны могут быть реализованы только в файле заголовка?
Если явному описанию инстанцирования предшествует ключевое слово extern
, оно вместо этого становится явным заявлением о создании. Наличие явной декларации о создании для данной специализации предотвращает неявное создание данной специализации в пределах текущей единицы перевода. Вместо этого ссылка на эту специализацию, которая в противном случае вызывала бы неявное создание, может ссылаться на явное определение инстанцирования в том же или другом TU.
foo.h
#ifndef FOO_H
#define FOO_H
template <class T> void foo(T x) {
// complicated implementation
}
#endif
foo.cpp
#include "foo.h"
// explicit instantiation definitions for common cases
template void foo(int);
template void foo(double);
main.cpp
#include "foo.h"
// we already know foo.cpp has explicit instantiation definitions for these
extern template void foo(double);
int main() {
foo(42); // instantiates foo<int> here;
// wasteful since foo.cpp provides an explicit instantiation already!
foo(3.14); // does not instantiate foo<double> here;
// uses instantiation of foo<double> in foo.cpp instead
}