Поиск…


Вступление

Классы, функции и (поскольку 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 ++, в зависимости от контекста.

  1. Когда за ним следует список параметров шаблона, заключенных в <> , он объявляет шаблон, такой как шаблон класса, шаблон функции или частичную специализацию существующего шаблона.

    template <class T>
    void increment(T& x) { ++x; }
    
  2. Когда следует пустой <> , он объявляет явную (полную) специализацию .

    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);
    }
    
  3. При последующей декларации без <> , она образует явное инстанцирование декларацию или определение.

    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
    
  4. В списке параметров шаблона он вводит параметр шаблона шаблона .

    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));
        }
        // ...
    };
    
  5. После оператора разрешения области видимости :: и операторов доступа к классу . и -> , он указывает, что следующее имя является шаблоном.

    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;
}

Шаблон псевдонимов

C ++ 11

Основной пример:

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;
}
C ++ 11
#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 :

C ++ 17
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

C ++ 14

Часто бывает полезно определить классы или структуры, которые имеют переменное число и тип элементов данных, которые определены во время компиляции. Канонический пример - 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 не определен в заголовке. Если эти объявления с явным манифестом не присутствуют, вероятно, возникнет ошибка компоновщика. См. Почему шаблоны могут быть реализованы только в файле заголовка?

C ++ 11

Если явному описанию инстанцирования предшествует ключевое слово 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
}


Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow