Sök…


Introduktion

Klasser, funktioner och (sedan C ++ 14) variabler kan mallas. En mall är en kodkod med några fria parametrar som kommer att bli en konkret klass, funktion eller variabel när alla parametrar anges. Parametrar kan vara typer, värden eller sig själva mallar. En välkänd mall är std::vector , som blir en betongbehållartyp när elementtypen anges, t.ex. std::vector<int> .

Syntax

  • mall < mall-parameter-lista > deklaration
  • exportera mall < mall-parameter-lista > deklaration / * tills C ++ 11 * /
  • mall <> deklaration
  • mall deklaration
  • extern mall deklaration / * eftersom C ++ 11 * /
  • mall < mall-parameter-lista > klass ... ( opt ) identifierare ( opt )
  • mall < mall-parameter-lista > klassidentifierare ( opt ) = id-expression
  • mall < mall-parameter-lista > typnamn ... ( opt ) identifierare ( opt ) / * sedan C ++ 17 * /
  • mall <mall-parameterlista> Typename identifierare (opt) = id-uttryckning / * eftersom C ++ 17 * /
  • postfix-expression . mall- id-uttryck
  • postfix-expression -> mall id-expression
  • kapslad-namn-specifik template enkel-mall-id ::

Anmärkningar

Ordet template är ett nyckelord med fem olika betydelser i C ++ - språket, beroende på sammanhanget.

  1. När det följs av en lista med mallparametrar som är bifogade i <> , förklarar den en mall som en klassmall , en funktionsmall eller en delvis specialisering av en befintlig mall.

    template <class T>
    void increment(T& x) { ++x; }
    
  2. När det följs av en tom <> förklarar den en uttrycklig (full) specialisering .

    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. När det följs av en deklaration utan <> bildar den en uttrycklig instansförklaring eller definition.

    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. Inom en lista med mallparametrar introducerar den en mall mallparameter .

    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. Efter omfattningen upplösning operatören :: och klassmedlem åtkomst operatörer . och -> , den anger att följande namn är en mall.

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

Innan C ++ 11 kan en mall förklaras med export nyckelordet , vilket gör det till en exporterad mall. Definitionen av en exporterad mall behöver inte finnas i varje översättningsenhet där mallen är instanserad. Till exempel skulle följande fungera:

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
}

På grund av svårigheten att genomföra den export var nyckelordet inte stöds av de flesta stora kompilatorer. Det avlägsnades i C ++ 11; nu är det olagligt att använda export nyckelordet alls. Istället är det vanligtvis nödvändigt att definiera mallar i rubriker (i motsats till icke-mallfunktioner, som vanligtvis inte definieras i rubriker). Se Varför kan mallar bara implementeras i rubrikfilen?

Funktionsmallar

Templering kan också tillämpas på funktioner (liksom de mer traditionella strukturerna) med samma effekt.

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

Detta kan sedan användas på samma sätt som strukturmallar.

printSum<int>(4, 5);
printSum<float>(4.5f, 8.9f);

I båda dessa fall används mallargumentet för att ersätta parametrarna; resultatet fungerar precis som en normal C ++ -funktion (om parametrarna inte stämmer med malltypen använder kompilatorn standardkonverteringar).

En ytterligare egenskap hos mallfunktioner (till skillnad från mallklasser) är att kompilatorn kan sluta mallparametrarna baserat på parametrarna som skickas till funktionen.

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.

Med den här funktionen kan vi förenkla koden när vi kombinerar mallstrukturer och funktioner. Det finns ett vanligt mönster i standardbiblioteket som gör att vi kan skapa template structure X hjälp av en hjälpfunktion 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};
}

Hur hjälper detta?

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.

Obs! Detta är inte utformat för att förkorta koden. Detta är utformat för att göra koden mer robust. Det gör det möjligt att ändra typerna genom att ändra koden på en enda plats snarare än på flera platser.

Argument vidarebefordran

Mall kan acceptera både lvalue- och rvalue-referenser med hjälp av vidarebefordringsreferens :

template <typename T>
void f(T &&t);

I det här fallet, den verkliga typen av t kommer att sluta beroende på sammanhanget:

struct X { };

X x;
f(x); // calls f<X&>(x)
f(X()); // calls f<X>(x)

I det första fallet dras typen T ut som referens till X ( X& ), och typen av t är lvalue-referens till X , medan i det andra fallet typen av T dras som X och typen av t som rvalue-referens. till X ( X&& ).

Obs: Det är värt att notera att decltype(t) i det första fallet är detsamma som T , men inte i det andra.

För att perfekt vidarebefordra t till en annan funktion, oavsett om det är en lvalue- eller rvalue-referens, måste man använda std::forward :

template <typename T>
void f(T &&t) {
    g(std::forward<T>(t));
}

Vidarebefordringsreferenser kan användas med variadiska mallar:

template <typename... Args>
void f(Args&&... args) {
    g(std::forward<Args>(args)...);
}

Obs: Vidarebefordringsreferenser kan endast användas för mallparametrar, till exempel i följande kod är v en rvalue-referens, inte en vidarebefordringsreferens:

#include <vector>

template <typename T>
void f(std::vector<T> &&v);

Grundläggande klassmall

Grundtanken med en klassmall är att mallparametern ersätts av en typ vid sammanställningstiden. Resultatet är att samma klass kan återanvändas för flera typer. Användaren anger vilken typ som ska användas när en variabel i klassen deklareras. Tre exempel på detta visas i 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
}

Mallspecialisering

Du kan definiera implementering för specifika inställningar av en mallklass / -metod.

Till exempel om du har:

template <typename T>
T sqrt(T t) { /* Some generic implementation */ }

Du kan sedan skriva:

template<>
int sqrt<int>(int i) { /* Highly optimized integer implementation */ }

Sedan kommer en användare som skriver sqrt(4.0) att få den generiska implementeringen medan sqrt(4) kommer att få den specialiserade implementeringen.

Partiell mallspecialisering

I motsats till en fullständig mallspecialisering tillåter partiell mallspecialisering att införa mall med några av argumenten för befintlig mall fixad. Partiell mallspecialisering är endast tillgänglig för mallklass / strukturer:

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

Som visas ovan kan partiella mallspecialiseringar införa helt olika uppsättningar data och funktionsmedlemmar.

När en delvis specialiserad mall inställs, väljs den mest lämpliga specialiseringen. Låt oss till exempel definiera en mall och två delvisa specialiseringar:

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

Följande samtal nu:

S<std::string, int, double>::foo();
S<int, float, std::string>::foo();
S<int, double, std::string>::foo();

kommer att skriva ut

General case
T = int
T = int, U = double

Funktionsmallar får endast vara helt specialiserade:

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

Standardmallparametervärde

Precis som för funktionsargument kan mallparametrar ha sina standardvärden. Alla mallparametrar med ett standardvärde måste deklareras i slutet av mallparameterlistan. Den grundläggande idén är att mallparametrarna med standardvärde kan utelämnas under mallinställning.

Enkelt exempel på standardmallparametervärdesanvändning:

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

Alias mall

C ++ 11

Grundläggande exempel:

template<typename T> using pointer = T*;

Denna definition gör pointer<T> ett alias av T* . Till exempel:

pointer<int> p = new int; // equivalent to: int* p = new int;

Aliasmallar kan inte specialiseras. Denna funktionalitet kan emellertid erhållas indirekt genom att låta dem hänvisa till en kapslad typ i en struktur:

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;

Mallarparametrar

Ibland vill vi överföra en malltyp till mallen utan att fastställa dess värden. Detta är vad mallmallparametrar skapas för. Mycket enkla mallmallparameterexempel:

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

Deklarerar icke-typmallargument med auto

Innan C ++ 17 skrivs, måste du ange dess typ först när du skrev en mall som inte är typ. Så ett vanligt mönster blev att skriva något som:

template <class T, T N>
struct integral_constant {
    using type = T;
    static constexpr T value = N;
};

using five = integral_constant<int, 5>;

Men för komplicerade uttryck innebär användning av något liknande detta att man måste skriva decltype(expr), expr när man installerar mallar. Lösningen är att förenkla detta formspråk och helt enkelt tillåta auto :

C ++ 17
template <auto N>
struct integral_constant {
    using type = decltype(N); 
    static constexpr type value = N;
};

using five = integral_constant<5>;

Tom anpassad borttagare för unique_ptr

Ett trevligt motiverande exempel kan komma från att försöka kombinera den tomma basoptimeringen med en anpassad borttagare för unique_ptr . Olika C API-rader har olika returtyper, men vi bryr oss inte - vi vill bara att något ska fungera för någon funktion:

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

Och nu kan du helt enkelt använda valfri funktionspekare som kan ta ett argument av typ T som en mall för icke-typmall, oberoende av returtyp, och få en utan storlek unique_ptr ur det:

unique_ptr_deleter<std::FILE, std::fclose> p;

Icke-typ mallparameter

Bortsett från typer som mallparameter får vi deklarera värden för konstant uttryck som uppfyller ett av följande kriterier:

  • integrerad eller uppräkningstyp,
  • pekaren till objekt eller pekaren för att fungera,
  • lvalue-referens till objekt eller lvalue-referens till funktion,
  • pekaren till medlem,
  • std::nullptr_t .

Liksom alla mallparametrar kan icke-typmallparametrar uttryckligen specificeras, standardiseras eller härledas implicit via mallargumentavdrag.

Exempel på icke-typ mallparameteranvändning:

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

Exempel på att specificera både typ- och icke-typmallparametrar:

#include <array>
int main ()
{
    std::array<int, 5> foo; // int is a type parameter, 5 is non-type
}

Icke-typ mallparametrar är ett av sätten att uppnå mall återfall och gör det möjligt att göra metaprogrammering .

Variadiska malldatastrukturer

C ++ 14

Det är ofta användbart att definiera klasser eller strukturer som har ett variabelt antal och typ av datamedlemmar som definieras vid sammanställningstiden. Det kanoniska exemplet är std::tuple , men ibland är det nödvändigt att definiera dina egna anpassade strukturer. Här är ett exempel som definierar strukturen med hjälp av sammansättning (snarare än arv som med std::tuple . Börja med den allmänna (tomma) definitionen, som också fungerar som basfall för uppsägningsterminering i den senare specialiseringen:

template<typename ... T>
struct DataStructure {};

Detta gör att vi redan kan definiera en tom struktur, DataStructure<> data , även om det inte är särskilt användbart ännu.

Därefter kommer rekursiv ärende specialisering:

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

Detta är nu tillräckligt för oss att skapa godtyckliga datastrukturer, som DataStructure<int, float, std::string> data(1, 2.1, "hello") .

Så vad händer? Först bör du notera att detta är en specialisering vars krav är att minst en variadisk mallparameter (nämligen T ovan) finns, medan du inte bryr sig om den specifika sminkningen för paketet Rest . Att veta att T finns, tillåter definitionen av sin dataledare first . Resten av data paketeras rekursivt som DataStructure<Rest ... > rest . Konstruktören initierar båda dessa medlemmar, inklusive ett rekursivt konstruktörsrop till rest medlem.

För att förstå detta bättre kan vi arbeta genom ett exempel: anta att du har en deklaration DataStructure<int, float> data . Förklaringen matchar först mot specialiseringen, vilket ger en struktur med int first och DataStructure<float> rest medlemmar. Den rest definition matchar igen specialisering skapar sin egen float first och DataStructure<> rest medlemmar. Slutligen matchar denna sista rest mot basfall-defintionen, vilket ger en tom struktur.

Du kan visualisera detta på följande sätt:

DataStructure<int, float>
   -> int first
   -> DataStructure<float> rest
         -> float first
         -> DataStructure<> rest
              -> (empty)

Nu har vi datastrukturen, men den är inte särskilt användbar ännu eftersom vi inte lätt kan komma åt de enskilda dataelementen (till exempel för att få tillgång till den sista medlemmen i DataStructure<int, float, std::string> data vi skulle behöva använda data.rest.rest.first , vilket inte är exakt användarvänligt). Så vi lägger till en get metod till den (behövs endast i specialiseringen eftersom bas-case-strukturen inte har några data att get ):

template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>
{
    ...
    template<size_t idx>
    auto get()
    {
        return GetHelper<idx, DataStructure<T,Rest...>>::get(*this);
    }
    ...
};

Som du kan se detta get medlemsfunktionen själv templerad - den här gången på indexet för den medlem som behövs (så användning kan vara saker som data.get<1>() , liknande std::tuple ). Det verkliga arbetet utförs av en statisk funktion i en GetHelper , GetHelper . Anledningen till att vi inte kan definiera den nödvändiga funktionaliteten direkt i DataStructure 's get är för att (som vi snart kommer att se) vi skulle behöva specialisera idxidx - men det är inte möjligt att specialisera en mallmedlemfunktion utan att specialisera den innehållande klassen mall. Observera också att användningen av en C ++ 14-stil auto här gör våra liv betydligt enklare eftersom vi annars skulle behöva ganska komplicerat uttryck för returtypen.

Så vidare till hjälpklassen. Den här gången kommer vi att behöva en tom framåtdeklaration och två specialiseringar. Först deklarationen:

template<size_t idx, typename T>
struct GetHelper;

Nu är fallet (när idx==0 ). I det här fallet returnerar vi bara den first medlemmen:

template<typename T, typename ... Rest>
struct GetHelper<0, DataStructure<T, Rest ... >>
{
    static T get(DataStructure<T, Rest...>& data)
    {
        return data.first;
    }
};

I det rekursiva fallet idx vi idx och åkallar GetHelper för rest medlem:

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

För att arbeta igenom ett exempel antar vi att vi har DataStructure<int, float> data och vi behöver data.get<1>() . Detta åberopar GetHelper<1, DataStructure<int, float>>::get(data) (den andra specialiseringen), som i sin tur åberopar GetHelper<0, DataStructure<float>>::get(data.rest) , som äntligen återvänder (av den första specialiseringen som idx är 0) data.rest.first .

Så det är det! Här är hela fungerande kod, med några exempel användas i 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;
}

Explicit instantiation

En uttrycklig instansdefinition skapar och förklarar en konkret klass, funktion eller variabel från en mall utan att använda den ännu. En uttrycklig instans kan refereras från andra översättningsenheter. Detta kan användas för att undvika att definiera en mall i en rubrikfil, om den bara kommer att instanseras med en begränsad uppsättning argument. Till exempel:

// 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*);

Eftersom print_string<char> och print_string<wchar_t> uttryckligen instanseras i print_string.cpp kommer länken att kunna hitta dem även om print_string mallen inte är definierad i rubriken. Om dessa uttryckliga inställningsdeklarationer inte var närvarande skulle ett länkfel troligtvis uppstå. Se Varför kan mallar bara implementeras i rubrikfilen?

C ++ 11

Om en uttrycklig definition instansiering föregås av extern sökord , blir det en uttrycklig instansiering deklaration i stället. Närvaron av en uttrycklig instansdeklaration för en given specialisering förhindrar implicit inställning av den givna specialiseringen inom den aktuella översättningsenheten. Istället kan en hänvisning till den specialiseringen som annars skulle orsaka en implicit instansering hänvisa till en uttrycklig instansdefinition i samma eller en annan 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
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow