美文网首页C++ Templates
【C++ Templates(11)】深入模板基础

【C++ Templates(11)】深入模板基础

作者: downdemo | 来源:发表于2018-05-10 09:33 被阅读180次

参数化声明

  • C++现在支持四种基本模板:类模板,函数模板,变量模板(variable template),别名模板(alias template),这些模板的每一种都可以出现在命名空间和类中,在类作用域中它们变为嵌套类模板,成员函数模板,静态数据成员模板和成员别名模板(member alias template)。下面是这四种模板的例子
// details/definitions1.hpp

template<typename T> // a namespace scope class template
class Data {
public:
    static constexpr bool copyable = true;
    ...
};
template<typename T> // a namespace scope function template
void log (T x) {
   ...
}

template<typename T> // a namespace scope variable template (since C++14)
T zero = 0;

template<typename T> // a namespace scope variable template (since C++14)
bool dataCopyable = Data<T>::copyable;

template<typename T> // a namespace scope alias template
using DataList = Data<T*>;
  • 注意静态数据成员Data<T>::copyable不是一个变量模板,只是通过类模板Data间接参数化。但一个变量模板可以出现在类作用域,这种情况下是一个静态成员模板。下面是四种模板作为类成员在类内定义的例子
// details/definitions2.hpp

class Collection {
public:
    template<typename T>        // an in-class member class template definition
    class Node {
        ...
    };

    template<typename T>        // an in-class (and therefore implicitly inline)
    T* alloc() {                // member function template definition
        ...
    }

    template<typename T>        // a member variable template (since C++14)
    static T zero = 0;
    template<typename T>        // a member alias template
    using NodePtr = Node<T>*;
};
  • C++17中,变量(包括静态数据成员)和变量模板能被内联,因此它们的定义能跨编译单元。这对能定义在多个编译单元中的变量模板是多余的。不像成员函数,一个定义在最近的类中的静态数据成员不会使其内联,inline关键字必须定义在所有情况中
  • 下面的代码说明了如何类外定义不是别名模板的成员模板,定义于类外的成员模板可以有多重template<...>参数化子句,一个代表模板本身,其余各个字句代表外围的每一层类模板,子句从最外层模板写起
// details/definitions3.hpp


template<typename T>            // a namespace scope class template
class List {
public:
    List() = default;           // because a template constructor is defined

    template<typename U>        // another member class template,
    class Handle;              // without its definition

    template<typename U>        // a member function template
    List (List<U> const&);     // (constructor)

    template<typename U>        // a member variable template (since C++14)
    static U zero;
};

template<typename T>            // out-of-class member class template definition
    template<typename U>
class List<T>::Handle {
    ...
};

template<typename T>            // out-of-class member function template definition
    template<typename T2>
List<T>::List (List<T2> const& b)
{
    ...
}

template<typename T>           // out-of-class static data member template definition
    template<typename U>
U List<T>::zero = 0;
  • union模板(通常被看作类模板的一种)
template <typename T>
union AllocChunk {
    T object;
    unsigned char bytes[sizeof(T)];
};
  • 函数模板同普通函数一样,可以有默认实参
template <typename T>
void report_top (Stack<T> const&, int number = 10);

template <typename T>
void fill (Array<T>&, T const& = T{}); // C++11前要写为T()
  • 后一个声明表明默认实参可以依赖于模板参数,如果提供了第二个参数,就不会实例化这个默认实参,这样就保证了不提供默认实参调用时也可能不会出错
class Value {
public:
    explicit Value(int);  // no default constructor
};

void init (Array<Value>& array)
{
    Value zero(0);
    fill(array, zero);   // OK: default constructor not used
    fill(array);         // ERROR: undefined default constructor for Value is used
}
  • 除了四种基本类型模板,被参数化的普通类成员也可以作为类模板的一部分,它们也会被不正确地视为成员模板。虽然它们都能被参数化,但它们的定义都不是first-class(第一次使用)的模板,它们的参数由外围类模板决定,定义中的参数化子句的参数来自外围类而非它们本身,因为它们本身不是模板
template<int I>
class CupBoard
{
    class Shelf;                // ordinary class in class template
    void open();                // ordinary function in class template
    enum Wood : unsigned char;  // ordinary enumeration type in class template
    static double totalWeight;  // ordinary static data member in class template
};

template<int I>         // definition of ordinary class in class template
class CupBoard<I>::Shelf {
    ...
};

template<int I>         // definition of ordinary function in class template
void CupBoard<I>::open()
{
    ...
}

template<int I>         // definition of ordinary enumeration type class in class template
enum CupBoard<I>::Wood {
    Maple, Cherry, Oak
};

template<int I>         // definition of ordinary static member in class template
double CupBoard<I>::totalWeight = 0.0;
  • C++17开始,静态成员可以使用inline在类内初始化
template<int I>
class CupBoard
    ...
    inline static double totalWeight = 0.0;
};

虚成员函数

  • 成员函数模板不能为虚函数,因为虚函数表的大小是固定的,而成员函数模板的实例化个数要编译完成后才能确定,这是冲突的
  • 类模板的普通成员(非模板成员)可以为虚函数,因为类被实例化后成员数量是固定的
template <typename T>
class Dynamic {
public:
    virtual ~Dynamic(); // 正确:每个Dynamic<T>对应一个析构函数

    template <typename T2>
    virtual void copy(T2 const&); // 错误:编译器不知道一个Dynamic<T>中copy()个数

模板的链接(Linkage of Template)

  • 每个模板在其作用域中必须有一个唯一的名称,除非函数模板可以被重载。类模板不能和其他实体共用一个名称
int C;
class C; // 正确:两者名称在不同的空间

int X;
template <typename T>
class X; // 错误:名称冲突

struct S;
template <typename T>
class S; // 错误:名称冲突
  • 模板名称是具有链接的,但不能有C链接(C linkage)
extern "C++" template <typename T>
void normal(); // 默认方式,上面的链接规范可以省略不写

extern "C" template <typename T>
void invalid(); // 错误:不能使用C链接

extern "Java" template<typename T>
void javaLink(); // 非标准链接:某些编译器可能支持
  • 模板通常具有外部链接,唯一例外的是static修饰的命名空间作用域下的函数模板
template<typename T>     // refers to the same entity as a declaration of the
void external();         // same name (and scope) in another file

template<typename T>     // unrelated to a template with the same name in
static void internal();  // another file

template<typename T>     // redeclaration of the previous declaration
static void internal();

namespace {
    template<typename>     // also unrelated to a template with the same name
    void otherInternal();  // in another file, even one that similarly appears
}                        // in an unnamed namespace

namespace {
    template<typename>     // redeclaration of the previous template declaration
    void otherInternal();
}

struct {
    template<typename T> void f(T) {}  // no linkage: cannot be redeclared
} x;

基本模板(Primary Templates)

  • 如果模板声明是一个普通声明(没有在模板名称后添加尖括号),这个声明就是一个基本模板
template<typename T> class Box;              // OK: primary template
template<typename T> class Box<T>;           // ERROR: does not specialize

template<typename T> void translate(T);      // OK: primary template
template<typename T> void translate<T>(T);   // ERROR: not allowed for functions

template<typename T> constexpr T zero = T{};     // OK: primary template
template<typename T> constexpr T zero<T> = T{};  // ERROR: does not specialize
  • 声明类的局部特化或变量模板时,声明的就是非基本模板
  • 函数模板必须是基本模板

模板参数

  • 模板参数有三种
    • 类型参数(最常见的使用)
    • 非类型参数
    • 模板的模板参数(template template parameters)
  • 在声明后不会引用模板参数名称时,模板参数的名称可以省略不写
template <typename, int>
class X; // X<> is parameterized by a type and an integer
  • 在模板声明后需要引用参数名称时则必须写,另外,同一对尖括号中后面的模板参数声明可以引用前面的模板参数名称,但反过来不行
template<typename T,             // the first parameter is used
    T Root,                 // in the declaration of the second one and
    template<T> class Buf>  // in the declaration of the third one
class Structure;

非类型参数

  • 非类型参数表示在编译期或链接期可以确定的常值,必须是下面的一种
    • 整型或枚举类型
    • 指针类型
    • 左值引用类型
    • std::nullptr_t
    • 包含auto或decltype(auto)类型(C++17)
  • 在非类型参数的声明前使用关键字typename是为了指明受限的名称
template <typename T, // 类型参数
    typename T::Allocator* Allocator> // 非类型参数
class List;
  • 函数和数组类型也可以被指定为非类型参数,但它们会decay为对应的指针类型
template<int buf[5]> class Lexer;         // buf is really an int*
template<int* buf> class Lexer;           // OK: this is a redeclaration

template<int fun()> struct FuncWrap;      // fun really has pointer to function type
template<int (*)()> struct FuncWrap;      // OK: this is a redeclaration
  • 非类型参数不能用static、mutable修饰,可以用const和volatile修饰,但如果这两个限定符是最外层的参数类型,编译器会忽略它们
template <int const length> class Buffer; // const被忽略
template <int length> class Buffer; // 和上面的声明等价
  • 在表达式中使用时,非引用非类型参数总是纯右值(prvalue),它们不能被寻址或赋值,而左值引用的非类型参数能表示一个左值
template<int& Counter>
struct LocalIncrement {
    LocalIncrement() { Counter = Counter + 1; }   //OK: reference to an integer
    ~LocalIncrement() { Counter = Counter - 1; }
};

模板的模板参数

  • 模板的模板参数是类或别名模板的占位符,声明和类模板类似,但不能用关键字struct和union
template<template<typename X> class C>          // OK
void f(C<int>* p);

template<template<typename X> struct C>         // ERROR: struct not valid here
void f(C<int>* p);

template<template<typename X> union C>         // ERROR: union not valid here
void f(C<int>* p);
  • C++17允许使用typename代替class
template<template<typename X> typename C>        // OK since C++17
void f(C<int>* p);
  • 模板的模板参数的参数也可以有默认模板实参
template <template <typename T,
    typename A = MyAllocator> class Container>
class Adaptation {
    Container<int> storage; // Container<int, MyAllocator>
    ...
};
  • 模板的模板参数的参数名称只能被自身其他参数的声明使用
template<template<typename T, T*> class Buf>  // OK
class Lexer {
    static T* storage;  // ERROR: a template template parameter cannot be used here
    ...
};
  • 通常模板的模板参数的名称不会在后面被用到,所以一般可以省略不写
template <template <typename, // 省略T
    typename = MyAllocator> class Container>
class Adaptation {
    Container<int> storage; // Container<int, MyAllocator>
    ...
};

模板参数包(Template Parameter Pack)

  • C++11开始,任何类型的模板参数都能转换为一个模板参数包,语法为...后接模板参数名称
template<typename... Types>   // declares a template parameter pack named Types
class Tuple;
  • 模板参数包行为类似于模板参数,不同的是,一个模板参数能匹配一个模板实参,而模板参数包能匹配任意数量的模板实参,这表明上面声明的Tuple类模板能接收任意数量的模板实参
using IntTuple = Tuple<int>;              // OK: one template argument
using IntCharTuple = Tuple<int, char>;    // OK: two template arguments
using IntTriple = Tuple<int, int, int>;   // OK: three template arguments
using EmptyTuple = Tuple<>;               // OK: zero template arguments
  • 类似地,nontype和template template parameter模板参数包能接收任意数量的对应实参
template<typename T, unsigned... Dimensions>
class MultiArray;       // OK: declares a nontype template parameter pack

using TransformMatrix = MultiArray<double, 3, 3>;   // OK: 3x3 matrix

template<typename T, template<typename,typename>... Containers>
void testContainers();  // OK: declares a template template parameter pack
  • 基本类模板,变量模板和变量模板可能有至多一个模板参数包,如果存在则必须作为最后一个模板参数。函数模板的限制较为宽松,可以有多个模板参数包,只要每个模板参数都接在一个模板参数包后,这个模板参数包要么有一个默认值,要么能被推断
template<typename... Types, typename Last>
class LastType;  // ERROR: template parameter pack is not the last template parameter

template<typename... TestTypes, typename T>
void runTests(T value);  // OK: template parameter pack is followed
                         //     by a deducible template parameter
template<unsigned...> struct Tensor;
template<unsigned... Dims1, unsigned... Dims2>
    auto compose(Tensor<Dims1...>, Tensor<Dims2...>);
                         // OK: the tensor dimensions can be deduced
  • 类的局部特化和变量模板的声明能有多个参数包
template<typename...> Typelist;
template<typename X, typename Y> struct Zip;
template<typename... Xs, typename... Ys>
    struct Zip<Typelist<Xs...>, Typelist<Ys...>>;
          // OK: partial specialization uses deduction to determine
          //     theXs and Ys substitutions
  • 一个类型参数模板包不能用于它自己的参数子句扩展
template<typename... Ts, Ts... vals> struct StaticValues {};
    // ERROR: Ts cannot be expanded in its own parameter list
  • 然而嵌套的模板则可以使用类似的方法
template<typename... Ts> struct ArgList {
    template<Ts... vals> struct Vals {};
};
ArgList<int, char, char>::Vals<3, 'x', 'y'> tada;

默认模板实参

  • 模板参数包外的任意模板参数都可以有默认实参,默认实参不能依赖于自身参数,但可以依赖于前面的参数
template <typename T, typename Allocator = allocator<T>>
class List;
  • 和函数的默认实参一样,默认实参后面所有的参数都要是默认实参。后面的缺省值通常在同一模板声明中提供,但也可以在前面的模板声明中提供
template <typename T1, typename T2, typename T3,
    typename T4 = char, typename T5 = char>
class Quintupe; // 正确

template <typename T1, typename T2, typename T3 = char,
    typename T4, typename T5>
class Quintupe; // 正确:T4和T5在前面已经有了缺省值

template <typename T1 = char, typename T2, typename T3,
    typename T4, typename T5>
class Quintupe; // 错误:T2没有缺省值
  • 函数模板不需要后续的模板参数有默认实参
template<typename R = void, typename T>
R* addressof(T& value);  // OK: if not explicitly specified, R will be void
  • 不能重复指定默认模板参数
template <typename T = void>
class Value;

template <typename T = void> // 错误:重复定义默认实参
class Value;
  • 一些语境不允许默认模板实参
// 局部特化
template<typename T>
class C;
...
template<typename T = int>
class C<T*>;                                        // ERROR

// 参数包
template<typename... Ts = int> struct X;        // ERROR

// 类模板成员的类外定义
template<typename T> struct X
{
    T f();
};

template<typename T = int> T X<T>::f() {          // ERROR
    ...
}

// 友元类模板声明
struct S {
 template<typename = void> friend struct F;
};

// 友元函数模板声明,除非它是一个定义且在同一编译单元的其他地方未出现过
struct S {
template<typename = void> friend void f();    // ERROR: not a definition
    template<typename = void> friend void g() {   // OK so far
    }
};
template<typename> void g();  // ERROR: g() was given a default template argument
                              // when defined; no other declaration may exist here

模板实参

  • 模板实参指实例化模板时用来替换模板参数的值,可以用下列几种机制在确定
    • 显式模板实参:紧跟在模板名称后在一堆尖括号内部的显式模板实参值,所组成的完整名称称为template-id
    • 注入式类名称:在带有参数P1,P2...的类模板X作用域中,模板名称(即X)等同于template-id(即X<P1, P2, ...)
    • 默认模板实参:如果提供了默认模板实参,类模板的实例中就可以省略显式模板实参。即使所有模板参数都有默认值也不能省略尖括号(即使尖括号内部为空也要保留)
    • 实参推断:编译器可根据函数调用实参类型推断出函数模板实参。如果所有的模板实参都可以通过推断获得,函数模板名称后的尖括号就可以省略

函数模板实参

  • 函数模板的模板实参既可以显式指定也可以让它们被编译器推断出来
// details/max.cpp

template <typename T>
T max (T a, T b)
{
    return a < b ? b : a;
}

int main()
{
    ::max<double>(1.0, -3.0); // 显式指定
    ::max(1.0, -3.0); // 隐式推断
    ::max<int>(1.0, 3.0); // 指定为int抑制推导
}
  • 某些模板实参无法推断,最好把这些实参放在模板参数列表的最前面,这样只要显式指定这些参数,其余参数仍可进行实参推断
// DstT未出现在参数列表中,无法被推导
template <typename DstT, typename SrcT>
DstT implicit_cast (SrcT const& x)
{
    return x;
}

int main()
{
    double value = implicit_cast<double>(-1);
    // 如果模板参数顺序调换就得指定两个实参
}
  • 函数模板可被重载,因此即使显式指定函数模板的所有实参也不能让编译器确定调用哪个实例
template<typename Func, typename T>
void apply (Func funcPtr, T x)
{
    funcPtr(x);
}

template <typename T> void single(T);

template <typename T> void multi(T);
template <typename T> void multi(T*);

int main()
{
    apply(&single<int>, 3); // OK
    apply(&multi<int>, 7); // 错误:multi<int>不唯一
}
  • 显式指定模板实参可能导致构造一个无效的C++类型参考下列代码。test<int>对第一个函数模板没有意义,因为int类型没有成员类型X,而第二个表达式没有此问题,因此&test<int>唯一标识第二个函数的地址,尽管第一个模板用int替换失败,但没有造成&test<int>非法
template <typename T> RT1 test(typename T::X const*);
template <typename T> RT2 test(...);
  • 替换失败并非错误(substitution-failure-is-not-an-error;SFINAE)原则使得函数模板的重载可行,借助此原则可以创造很多出色的编译期技术
typedef char RT1;
typedef struct { char a[2]; } RT2;
template <typename T> RT1 test(typename T::X const*);
template <typename T> RT2 test(...);
// 在编译期判断给定类型T是否具有成员类型X
#define type_has_member_type_X(T) (sizeof(test<T>(0)) == 1)
  • 如果选择的是第一个test模板(返回一个大小为1的char),如果是第二个则返回一个大小至少为2的结构,由此可以确定调用的是哪个模板,调用第一个则说明T有成员类型X,调用第二个则无
  • SFINAE原则只是防止创建非法类型,并不能防止非法算式
template <int I> void f(int (&) [24/(4-I]);
template <int I> void f(int (&) [24/(4+I]);

int main()
{
    &f<4>; // 错误:替换后第一个除数为0,未使用SFINAE原则
}
  • 这个错误出现在算是求值过程中,而非编译器把算式值绑定到模板实参时,下例合法
template <int N> int g() { return N; }
template <int* P> int g() { return *p; }

int main()
{
    return g<1>(); // 1不能被绑定到int*,应用了SFINAE原则
}

类型实参

  • 常用的大多数类型都可被用作模板的类型实参,但下列两种情况例外(C++11前)
    • 局部类和局部枚举(即函数你定义内部声明的类型)
    • unnamed class类型或枚举类型(但如果用typedef使其具名则可用)。如struct { int x; } s; enum { e = 3 } c;s和c就是unnamed type
template <typename T> class List {
    ...
};

typedef struct {
    double x, y, z;
} Point;

typedef enum { red, green, blue } *ColorPtr;

int main()
{
    struct Association
    {
        int* p;
        int* q;
    };
    List<Association*> error1; // 错误:不能是局部类型
    List<ColorPtr> error2; // 错误:不能是unnamed type
    List<Point> ok; // 正确:无名的type因typedef有了名称
}
  • 其他类型(C++11后为任意类型)可以作为模板实参,但前提是该类型替换模板参数后获得的构造必须有效
template <typename T>
void clear(T p)
{
    *p = 0; // 要求单目运算符*可用于类型T
}

int main()
{
    int a;
    clear(a); // 错误:int不支持*
}

非类型实参

  • 非类型实参是替换非类型参数的值,这个值必须是以下一种
    • 某个具有正确类型的非类型模板参数
    • 是一个编译期整型或枚举值,但前提是必须与对应的参数类型匹配或可以被隐式转换为该类型(如char值可以作为int参数的实参)
    • 前面有取址符&的外部变量或函数名。对函数或数组变量可以省略&。这类模板实参可以匹配指针类型的非类型参数。C++17放宽了这个要求,允许任何生成一个指向函数或变量的指针的常量表达式
    • 如外部变量或函数名前没有&,则匹配引用类型非类型参数。C++17放宽了这个要求,允许一个函数或变量的任意的常量表达式泛左值(glvalue)
    • 一个指向成员的指针常量(如&C::m,C是一个class,m是一个非静态成员)。这类实参只能匹配类型为成员指针的非类型参数。C++17中实际的语法形式不再受约束,允许任何指向成员的指针的常量表达式
    • 一个空指针常量也是有效的非类型参数的实参,这个非类型参数是指针或指向成员的指针类型
  • 对于整型非类型参数,会考虑隐式转换。随着C++11的constexpr转换函数的引入,实参在转换前可以有一个类类型。C++17前,当实参匹配一个指针类型或引用类型的模板参数时,自定义的类型转换(如单参数的构造函数和重载类型转换运算符)和派生类到基类的类型转换都不被编译器考虑,即使在其他情况下这些隐式转换有效。隐式类型转换的唯一应用只能是给实参加上const或volatile关键字
  • 下例中的非类型模板实参都合法
template <typename T, T nontype_param>
class C;

C<int, 33>* c1; // 整型

int a;
C<int*, &a>* c2; // 外部变量的地址

void f();
void f(int);
C<void(*)(int), f>* c3; // 匹配f(int),f前的&省略

template<typename T> void templ_func();
C<void(), &templ_func<double>>* c4; // 函数模板实例同时也是函数

struct X {
    static bool b;
    int n;
    constexpr operator int() const { return 42; }
};

C<bool&, X::b>* c5; // 静态成员是可取的变量/函数名称

C<int X::*, &X::n>* c6; // 指向成员的指针常量

template <typename T>
void templ_func();

C<long, X{}>* c7; // X先通过constexpr转换函数转为int,然后由int转为long
  • 模板实参的一个普遍约束是,必须能在编译期或链接期确定实参值,运行期才能确定实参值(如局部变量地址)就不符合模板在程序创建时才实例化的概念
  • 下列常量不能作为有效的非类型实参
    • 空指针常量(C++11前)
    • 浮点数
    • string literal
  • 不能用字符串字面常量作为非类型模板实参的技术原因是,两个内容一样的字符串可能存在两个不同地址中。一种笨拙的解决方法是引入一个额外的变量存储这个字符串
template<char const* str>
class Message {
  ...
};

extern char const hello[] = "Hello World!";
char const hello11[] = "Hello World!";

void foo()
{
    static char const hello17[] = "Hello World!";

    Message<hello> msg03;     // OK in all versions
    Message<hello11> msg11;   // OK since C++11
    Message<hello17> msg17;   // OK since C++17
}
  • 非类型模板参数声明为引用或指针的要求是,在所有版本中可以是一个有外部链接的常量表达式,在C++11中则是内部链接,在C++17中则可以是任意链接
  • 下面是非法实例
template<typename T, T nontypeParam>
class C;

struct Base {
    int i;
} base;

struct Derived : public Base {
} derived;

C<Base*, &derived>* err1; // 错误:不允许派生类转基类
C<int&, base.i>* err2; // 错误:域运算符(.)后的变量不被看成变量
int a[10];
C<int*, &a[0]>* err3; // 错误:不能使用数组内某个元素的地址

模板的模板实参

  • 模板的模板实参必须是一个类模板或别名模板,它本身具有参数,该参数必须精确匹配它所替换的双重模板参数的参数。C++17前模板的模板参数的默认模板实参会被编译器忽略,除非对应的模板的模板参数有默认实参。C++17放宽了匹配规则,只要求模板的模板参数至少能特化为对应的模板的模板实参。下列是一个C++17前非法的例子
#include <list>
    // declares in namespace std:
    // template<typename T, typename Allocator = allocator<T>>
    // class list;

template<typename T1, typename T2,
    template<typename> class Cont>  // Cont expects one parameter
class Rel {
    ...
};

Rel<int, double, std::list> rel;  // ERROR before C++17: std::list has more than one template parameter
  • 问题在于std::list有多个参数,第二个参数(通常是一个allocator)有一个默认值,但C++17前匹配std::list和Container参数时不会考虑这个默认值
  • 老的解决方法是给模板的模板参数添加一个有默认值的参数,这样做虽然并不完善,但可以让标准容器模板得到使用
#include <memory>
template <typename T1,
    typename T2,
    template<typename T,
        typename = std::allocator<T>> class Container>
class Relation {
public:
    ...
private:
    Container<T1> dom1;
    Container<T2> dom2;
};
  • 变长模板的模板参数是C++17前的精确匹配规则的例外,它允许更通用的模板的模板实参匹配,参数包能匹配同类型的0到多个实参
#include <list>

template<typename T1, typename T2,
    template<typename... > class Cont>  // Cont expects any number of type parameters
class Rel {
    ...
};

Rel<int, double, std::list> rel;  // OK: std::list has two template parameters
                                  //     but can be used with one argument
  • 模板参数包只能匹配同类型的模板实参,比如下面这个类模板,能实例化为任何只有模板类型参数的类模板或别名模板,因为传递的模板类型参数包TT,能匹配任意数量的模板类型参数
#include <list>
#include <map>
    // declares in namespace std:
    //  template<typename Key, typename T,
    //           typename Compare = less<Key>,
    //           typename Allocator = allocator<pair<Key const, T>>>
    //  class map;
#include <array>
    // declares in namespace std:
    //  template<typename T, size_t N>
    //  class array;

template<template<typename... > class TT>
class AlmostAnyTmpl {
};

AlmostAnyTmpl<std::vector> withVector; //two type parameters
AlmostAnyTmpl<std::map> withMap;       // four type parameters
AlmostAnyTmpl<std::array> withArray;   // ERROR: a template type parameter pack
                                       // doesn't match a nontype template parameter
  • C++17前,语法上只有class关键字才能声明模板的模板参数,但不代表模板的模板实参必须是class type,事实上struct,union和别名模板(C++11)都可以当作模板的模板参数的有效实参

实参的等价性

  • 当两组模板实参对应的元素都相等时,这两组模板实参等价。对于类型实参,typedef的名称不影响对比过程,最后比较的还是typedef原本的类型。对于非类型的整型实参,比较的是实参的值,与表达方式无关
template <typename T, int I>
class Mix;

typedef int Int;

Mix<int, 3*3>* p1;
Mix<int, 4+5>* p2; // p2和p1类型相同
  • 在模板依赖的语境中,模板实参的值不是总能确定地建立,等价性的规则变得稍微复杂一些
template<int N> struct I {};

template<int M, int N> void f(I<M+N>);  // #1
template<int N, int M> void f(I<N+M>);  // #2

template<int M, int N> void f(I<N+M>);  // #3 ERROR
  • 1和2的声明是等价的,但3则不是,但在被调用时会产生同样的结果,因此3是功能等价的(functionally equivalent)。以不同的方式声明模板是一个错误,因为只是功能等价而非真正的等价。然而,这个错误不需要由编译器诊断,因为一些编译器内部可能把N+1+1视为和N+2等价,所以标准在这里放松了约束,不需要一个具体的实现方法
  • 从函数模板实例化而来的函数永远不和普通函数等价,这样对类成员有两个重要影响
    • 由成员函数模板实例化的函数不会重写虚函数
    • 由构造函数模板实例化的构造函数一定不会是一个拷贝或移动构造函数,同理由assigment template产生的赋值运算符也不会是一个拷贝或移动赋值运算符(这个问题较少出现,因为拷贝或移动赋值运算符不会被隐式调用)

可变参数模板(Varaidic Template)

  • 可变参数模板是包含至少一个模板参数包的模板
  • 当为可变参数模板确定模板实参时,模板参数包都可以匹配任意数量的模板实参,模板实参的序列称为实参包
template<typename... Types>
class Tuple {
    // provides operations on the list of types in Types
};

int main() {
    Tuple<> t0;           // Types contains an empty list
    Tuple<int> t1;        // Types contains int
    Tuple<int, float> t2; //  Types contains int and float
}
  • 因为一个模板参数包代表一个模板实参列表,所以实参包的实参必须有相同的构造,sizeof...操作就是这样一个构造,作用是计算实参包的实参数量
template<typename... Types>
class Tuple {
public:
    static constexpr std::size_t length = sizeof...(Types);
};

int a1[Tuple<int>::length];              // array of one integer
int a3[Tuple<short, int, long>::length]; // array of three integers

包扩展(Pack Expansion)

  • 包扩展是一个扩展实参包到分离的实参的构造,sizeof...表达式就是一个包扩展的例子,包扩展的定义方法是在列表中的元素右边加上省略号
template<typename... Types>
class MyTuple : public Tuple<Types...> {
    // extra operations provided only for MyTuple
};

MyTuple<int, float> t2;  // inherits from Tuple<int, float>
  • 模板实参Types...就是一个包扩展,作用是产生一个模板实参序列,其中的每个实参都替换为Types,这里MyTuple<int, float>把实参包int,float替换给了模板类型参数包Types,当这发生在包扩展Types...时,就会得到一个int的实参和一个float的实参,因此MyTuple<int, float>继承自Tuple<int, float>
  • 一个直观的理解包扩展的方法是,把它们视为一个语法扩展,比如如果有两个参数,MyTuple相当于
template<typename T1, typename T2>
class MyTuple : public Tuple<T1, T2> {
    // extra operations provided only for MyTuple
};
  • 有三个参数则是
template<typename T1, typename T2, typename T3>
class MyTuple : public Tuple<T1, T2, T3> {
    // extra operations provided only for MyTuple
};
  • 但是不能直接通过名字访问参数包的单个元素,因为T1,T2这些名字在可变参数模板中没有定义,如果要需要这些类型,只能把它们传给另一个类或函数
  • 每个包扩展有一个模式(pattern),也就是参数包的每个实参重复的类型,前面的例子中只有简单的模式,即参数包的名字,但模式也可以复杂化
template<typename... Types>
class PtrTuple : public Tuple<Types*...> {
    // extra operations provided only for PtrTuple
};

PtrTuple<int, float> t3; // Inherits from Tuple<int*, float*>
  • 包扩展Types...的模式是Types,把每个元素用模式扩展后就得到一个实参序列,每个实参的类型都是Type*,如果有三个参数,PtrTuple就是下面这样
template<typename T1, typename T2, typename T3>
class PtrTuple : public Tuple<T1*, T2*, T3*> {
    // extra operations provided only for PtrTuple
};

包扩展出现的位置

  • 包扩展基本可以在任何提供了一个逗号分割的列表中使用
    • 基类列表
    • 基类构造函数的初始化列表
    • 调用实参的列表(模式是实参表达式)
    • 初始化列表(如括号初始化列表)
    • 类或函数或别名模板的模板参数列表
    • 函数抛出的异常列表(C++11/14弃用,C++17不允许)
    • 在一个属性(attribute)中,如果属性本身支持包扩展(C++标准中没有定义这样的attribute)
    • 指定一个声明的alignment
    • 指定lambda的捕获列表
    • 函数类型的参数列表
    • using声明中(C++17)
  • 基类列表中的包扩展可以扩展一些直接基类
template<typename... Mixins>
class Point : public Mixins... {    // base class pack expansion
    double x, y, z;
public:
    Point() : Mixins()... { }         // base class initializer pack expansion

    template<typename Visitor>
    void visitMixins(Visitor visitor) {
        visitor(static_cast<Mixins&>(*this)...); // call argument pack expansion
    }
};

struct Color { char red, green, blue; };
struct Label { std::string name; };
Point<Color, Label> p;             // inherits from both Color and Label
  • 包扩展也能用于模板参数列表来创建一个非类型或模板参数包
template<typename... Ts>
struct Values {
    template<Ts... Vs>
    struct Holder {
    };
};

int i;
Values<char, int, int*>::Holder<'a', 17, &i> valueHolder;

函数参数包

  • 一个函数参数包是一个匹配任意数量函数调用实参的函数参数,模板参数包和函数参数包统称为参数包
  • 不同于模板参数包,函数参数包总是包扩展,因此它们的声明类型必须包含至少一个参数包
template<typename... Mixins>
class Point : public Mixins...
{
    double x, y, z;
public:
    //default constructor, visitor function, etc. elided*
    Point(Mixins... mixins)    // mixins is a function parameter pack
    : Mixins(mixins)... { }  //initialize each base with the supplied mixin value
};

struct Color { char red, green, blue; };
struct Label { std::string name; };
Point<Color, Label> p({0x7F, 0, 0x7F}, {"center"});
  • 一个函数模板的函数参数包可能依赖于模板中的模板参数包的声明,这允许函数模板不丢失类型信息地接受任意数量的调用实参
template<typename... Types>
void print(Types... values);

int main
{
    std::string welcome("Welcome to ");
    print(welcome, "C++", 2011, '\n'); // calls print<std::string, char const*, int, char>
}
  • print()的实现使用了递归模板实例化,一种模板元编程技术
  • 参数列表末尾出现的未命名的函数参数包,和C-style的可变参数(vararg)会产生歧义
template<typename T> void c_style(int, T...);
template<typename... T> void pack(int, T...);
  • 第一个例子中的T...被视为T, ...,一个未命名的T类型参数,后接一个C-style的可变参数。第二个例子的T...构造为视为一个函数参数包,因为T是一个有效的扩展模式
  • 在lambada中,如果紧接在...前(中间没有逗号)的类型包含auto,...就会被视为参数包

多重嵌套包扩展(Multiple and Nested Pack Expansion)

  • 当实例化包含多个参数包的包扩展时,所有的参数包必须具有相同的长度。依次将每个参数包的第一个实参替换为模式,接着是每个参数包的第二个实参,从而生成一个类型或值的序列
template<typename F, typename... Types>
void forwardCopy(F f, Types const&... values) {
    f(Types(values)...);
}
  • 三个实参时如下
template<typename F, typename T1, typename T2, typename T3>
void forwardCopy(F f, T1 const& v1, T2 const& v2, T3 const& v3) {
    f(T1(v1), T2(v2), T3(v3));
}
  • 包扩展本身也可能嵌套,每个参数包由它最近的包扩展来扩展,下面是调用三个不同参数包的嵌套包扩展
template<typename... OuterTypes>
class Nested {
    template<typename... InnerTypes>
    void f(InnerTypes const&... innerValues) {
        g(OuterTypes(InnerTypes(innerValues)...)...);
    }
};
  • 调用g()时,模式为InnerTypes(innerValues)的包扩展处于最内层,将扩展InnerTypes和innerValues,生成一个函数调用实参的序列,用于初始化OuterTypes的一个对象,每个OuterTypes中的类型的初始化再给g()生成一系列调用实参。OuterTypes有两个实参,InnerTypes和innerValues都有三个实参时,相当于
template<typename O1, typename O2>
class Nested {
    template<typename I1, typename I2, typename I3>
    void f(I1 const& iv1, I2 const& iv2, I3 const& iv3) {
        g(O1(I1(iv1), I2(iv2), I3(iv3)),
            O2(I1(iv1), I2(iv2), I3(iv3)),
            O3(I1(iv1), I2(iv2), I3(iv3)));
    }
};

零长包扩展(Zero-Length Pack Expansion)

  • 零长实参包的语法解释经常会失败,为了解释这点,考虑下面这个零实参的类
template<>
class Point : {
    Point() : { }
};
  • 这个代码是非法的,因为模板参数列表为空,空的基类和基类初始化列表前有一个迷路的冒号
  • 包扩展实际上是语义构造,并且任何大小的参数包的替换不影响包扩展(或其封装的可变参数模板)的解析。当一个包扩展扩展到一个空列表时,程序(语义上)表现得像列表不存在。实例化Point<>最终没有基类,它的默认构造函数也没有基类初始化器,但却符合格式。这种语义规则仍然成立,即使当零长包扩展的语法解释是well-defined code
template<typename T, typename... Types>
void g(Types... values) {
    T v(values...);
}
  • 可变参数模板g()创建一个值v,v从给出的值序列直接初始化,如果序列为空,v的声明看起来就是T v(),然而因为包扩展的替换是符合语义的,不会影响产生的实体的语法分析,v就会由零个实参初始化

折叠表达式(Fold Expression)

  • 程序中一个递归的模式是一个值序列操作的折叠
  • 如果没有这个特性,就可能要这样实现operator&&
bool and_all() { return true; }
template<typename T>
    bool and_all(T cond) { return cond; }
template<typename T, typename... Ts>
    bool and_all(T cond, Ts... conds) {
        return cond && and_all(conds...);
}
  • C++17中加入了折叠表达式的特性,可用于除了.,->和[]外的所有二元操作符
  • 给定一个未扩展的表达式牧师包和一个非模式表达式值,C++17允许写出如下operator op,注意圆括号不能省略
(pack op ... op value) // for a right fold of the operator (called a binary right fold)
(value op ... op pack) // for a left fold (called a binary left fold)
  • 使用这个特性,下面代码
template<typename... T> bool g() {
    return and_all(trait<T>()...);
}
  • 可重写为
template<typename... T> bool g() {
    return (trait<T>() && ... && true);
}
  • 折叠表达式是包扩展,如果包是空的,折叠表达式类型仍能由non-pack operand(value)决定,但这个特性的设计者还希望能省略value,因此C++17还允许另外两种写法,注意圆括号不能省略
(pack op ... ) // unary right fold
(... op pack) // unary left fold
  • 对于空扩展需要决定类型和值,空的一元折叠表达式通常会产生错误,除了三种例外情况
    • 一个&&的一元折叠的空扩展产生值true
    • 一个||的一元折叠的空扩展产生值false
    • 一个,的一元折叠空扩展产生一个void表达式
  • 如果以不寻常的方式重载这些特殊的操作符,将产生意想不到的结果
struct BooleanSymbol {
    ...
};

BooleanSymbol operator||(BooleanSymbol, BooleanSymbol);

template<typename... BTs>
void symbolic(BTs... ps) {
    BooleanSymbol result = (ps || ...);
    ...
}
  • 假如以继承自BooleanSymbol的类型调用symbolic,对空扩展将产生一个bool值,其他所有扩展,结果将产生一个BooleanSymbol值。因此通常应该避免使用一元折叠表达式,而推荐使用二元折叠表达式

友元

  • 友元声明的基本概念很简单:指定某些类或函数,让它们可以访问友元声明所在的类,但下面两个事实使这个概念复杂化了
    • 友元声明可能是某个实体的唯一声明
    • 友元函数的声明可以是一个定义

类模板的友元类

  • 友元类的声明不能是类定义,因此友元类通常不会出问题。涉及模板时唯一要考虑的是,可以把某个类模板的实例声明为友元
template <typename T>
class Node;

template <typename T>
class Tree {
    friend class Node<T>;
    ...
};
  • 在类模板的某一实例(如上述Node<T>)成为其他类或类模板(如上述Tree)的友元前,该类模板(Node)在声明的地方必须是已被声明且可见的。对一个普通类没有此要求
template <typename T>
class Tree {
    friend class Factory; // 正确,即使这是首次声明Factory
    friend class Node<T>; // 错误:未声明过Node
};
  • 一个应用是声明其他类模板实例化为友元
template<typename T>
class Stack {
public:
    ...
    // assign stack of elements of type T2
    template<typename T2>
    Stack<T>& operator= (Stack<T2> const&);
    // to get access to private members of Stack<T2> for any type T2:
    template<typename> friend class Stack;
    ...
};
  • C++11允许让一个模板参数为友元
template<typename T>
class Wrap {
    friend T; // valid for any type T, but ignored if T is not class type
    ...
};

类模板的友元函数

  • 函数模板实例可以成为一个友元,只需要在该函数模板名称后紧跟尖括号。尖括号可以包含模板实参,也可以通过调用参数来推断实参,如果全部实参都能推断获得,尖括号里可以为空
template <typename T1, typename T2>
void combine(T1, T2);

class Mixer {
    friend void combine<>(int&, int&); // T1 = int&, T2 = int&
    friend void combine<int, int>(int, int); // T1 = int, T2 = int
    friend void combine<char>(char, int); // T1 = char, T2 = int
    friend void combine<>(long, long) { ... } // 错误:不能在此定义
};
  • 不能在友元声明中定义模板实例(instances),模板实例只能由编译器产生,程序员只能定义特化(specializations)。因此命名一个实例的友元声明不能是定义式
  • 如果友元名称后没有紧跟一对尖括号只有两种情况
    • 如果名称不是受限的(未被::指定)就不会引用某个模板实例,如果无法在友元声明处匹配一个非模板函数(即普通函数),则这个友元声明就是这个函数的首次声明,这个声明可以是定义式
    • 如果名称是受限的(被::指定)就必须引用一个已定义的函数或函数模板,匹配时优先匹配普通函数再匹配函数模板,这个友元声明不能是定义
void multiply(void*); // 普通函数

template <typename T>
void multiply(T);

class Comrades {
    friend void multiply(int) {} // 定义了一个新函数::multiply(int)
    friend void ::multiply(void*); // 引用上面的普通函数
    friend void ::multiply(int); // 引用一个模板实例
    friend void ::multiply<double*>(double*); // 受限名称可以带尖括号,但模板在此必须可见
    friend void ::error() {} // 错误:受限的友元不能是定义式
  • 类模板中声明友元的规则仍然同上,且模板参数还可以用来标识友元函数
template <typename T>
class Node {
    Node<T>* allocate();
    ...
};

template <typename T>
class List {
    friend Node<T>* Node<T>::allocate();
    ...
};
  • 然而把友元函数定义于类模板中会引发一个错误,因为任何在模板中声明的实体都要等模板实例化后才会是具体的实体,此前该实体是不存在的,类模板的友元函数也是如此
template <typename T>
class Creator {
    friend void appear() { // 定义一个新函数::appear()
        ... // 只有当Creator实例化后该函数才存在
    }
};
Creator<void> miracle; // ::appear()此时被生成
Creator<double> oops; // 错误:第二次生成::appear()
  • 因此必须确保类模板的模板参数出现在内部定义的所有友元函数的类型定义中
template<typename T>
class Creator {
    friend void feed(Creator<T>) {  //every T instantiates a different function ::feed()
        ...
    }
};

int main()
{
    Creator<void> one;
    feed(one);                     // instantiates ::feed(Creator<void>)
    Creator<double> two;
    feed(two);                     // instantiates ::feed(Creator<double>)
}
  • 每个Creator实例都生成了一个不同的feed()函数,虽然这些函数是作为模板的一部分被生成的,但本身仍是普通函数而不是模板的实例
  • 由于这些函数定义在类中,它们都会被隐式内联,因此在不同的编译单元可以生成相同的函数

友元模板

  • 通常定义一个友元是一个函数模板的实例或类模板的实例时,可以明确指定一个实例作为友元。然而需要让模板的所有实例都成为友元时,需要通过友元模板机制实现
class Manager {
    template <typename T>
        friend class Task;

    template <typename T>
        friend void Schedule<T>::dispatch(Task<T>*);

    template <typename T>
        friend int ticket() {
            return ++Manager::counter;
        }
    static int counter;
};
  • 和普通友元声明一样,只有友元模板声明的是一个非受限的函数名称,且后面没有紧跟尖括号,该友元模板声明才能成为定义
  • 友元模板声明的只是基本模板和基本模板的成员,进行这些声明后,与该基本模板相关的模板局部特化和显式特化都会被自动视为友元

相关文章

网友评论

    本文标题:【C++ Templates(11)】深入模板基础

    本文链接:https://www.haomeiwen.com/subject/hszarftx.html