泛型与模板.md 31 KB

简单基础

函数模板

自变量推到

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

max(4, 7);      // Success
max(4, 7.2);    // Error

因为输入的a是int类型,但是b确实double类型,两个类型不同但是只有一个T,所以报错

  1. 解决方法:指定类型
max<double>(4, 7.2);
  1. 解决方法:不同参数不同类型
template <typename T1, typename T2>
inline T1 const& max(T1 const& a, T2 const& b)
{
	return a < b ? b : a;
}

模板参数

template<typename T>
inline T max(T a, T b)
  • 模板函数的两种参数
    • template<typename T> 中T就是模板参数
    • max(T a, T b)中的a、b就是调用参数
template <typename T1, typename T2, typename T3>
inline T1 const& max(T1 const& a, T3 const& b)
{
	return a < b ? b : a;
}

max(1, 2);

自变量推导机制并不对返回类型进行匹配,所以这里不能推导出T1的数据类型,需要手动指出max<int, double, double>(4, 4.2)

template <typename TR, typename T1, typename T2>
inline TR const& max(T1 const& a, T3 const& b)
{
	return a < b ? b : a;
}

max<double>(1, 2);

TR是手动指定的double,T1、T2是可以被自变量推导出来的

重载函数模板

// 传回两个 int 中的较大者
inline int const& max(int const& a, int const& b)
{
	return a < b ? b : a;
}
// 传回两任意类型的数值中的较大者
template <typename T>
inline T const& max(T const& a, T const& b)
{
	return a < b ? b : a;
}
// 传回三个任意类型值中的最大者
template <typename T>
inline T const& max(T const& a, T const& b, T const& c)
{
	return ::max(::max(a, b), c);
}

int main() {
	::max(7, 42, 68);       // 调用「接受三个自变量」的函数
	::max(7.0, 42.0);       // 调用 max<double>(经由自变量推导)
	::max('a', 'b');        // 调用 max<char>(经由自变量推导)
	::max(7, 42);           // 调用「接受两个 int 自变量」的 non-template 函数
	::max<>(7, 42);         // 调用 max<int>(经由自变量推导)
	::max<double>(7, 42);   // 调用 max<double>(无需自变量推导)
	::max('a', 42.7);       // 调用「接受两个 int 自变量」的 non-template 函数

	return 0;
}

一般来说,不同的重载形式之间最好不存在 绝对必要的差异,各重载形式之间应该只存在 参数个数不同参数类型的明确不同,所以不要出现一个参数是引用、另一个不是引用的情况

类模板

声明

声名类模板

template <typename T>
class Stack {
    private:
        std::vector<T> elems;   // 元素
    public:
        void push(T const&);    // push 元素
        void pop();             // pop 元素
        T top() const;          // 传回最顶端元素
        bool empty() const {    // stack 是否为空
            return elems.empty();
        }
}; 

template <typename T>
void Stack<T>::push (T const& elem)
{
    elems.push_back(elem);          // 追加(附于尾)
}

使用类模板

Stack<int> intStack;
intStack.push(7);

Stack<int>将类模板中的T替换成int,Stack内部使用可容纳int作为元素的vector

唯有被调用到的成员函数,才会实例化,这样做可以节省时间和空间;另一方面,可以实例一个类模板,并且实例化的类不需要完整支持模板类中与该类有关的所有操作

比如,一些自定义类没有实现 operator <,类模板中可能有一些函数中有比较大小的操作,那么只要不执行这些函数,以自定义类为T的类模板就不会报错

特化模板

可以用模板实参来特化类模板,与函数模板的重载类似,通过特化类模板可以是实现基于某种特定类型的实现,或者克服某种特定类型在实例化类模板时所出现的不足

如果要特化类模板,需要特化该类所有成员函数,虽然也可以指特化某个成员函数,但是这个作为没有特化整个类,也就没有特化类模板

template<>
class Stack<std::string> {
    private:
        std::deque<std::string> elems;  // 元素
    public:
        void push(std::string const&);  // push 元素
        void pop();                     // pop 元素
        std::string top() const;        // 传回 stack 最顶端元素
        bool empty() const {            // stack 是否为空
            return elems.empty();
        }
}; 

void Stack<std::string>::push (std::string const& elem)
{
    elems.push_back(elem);              // 追加元素
}

这里使用deque来替代vector管理Stack内部元素,说明了特化的实现可以和基本类模板的实现完全不同

局部特化,偏特化

类模板可以被局部特化,在特定的环境下指定类模板的特定实现,并且要求某些模板参数仍然必须有用户类定义

template <typename T1, typename T2>
class MyClass {
    // ...
}; 

可以存在下面两种偏特化

template <typename T>
class MyClass<T,T> {
    // ...
};

// 偏特化:第二个类型为 int 
template <typename T> 
class MyClass<T,int> {
    // ...
}; 

// 偏特化 两个template parameter均为指针
template <typename T1, typename T2>
class MyClass<T1*, T2*> {
    // ...
}

各个模板的使用

MyClass<int,float>      mif;       // 使用 MyClass<T1,T2>
MyClass<float,float>    mff;       // 使用 MyClass<T,T>
MyClass<float,int>      mfi;       // 使用 MyClass<T,int>
MyClass<int*,float*>    mp;        // 使用MyClass<T1*,T2*>

如果多个局部特化同等程度地匹配某个声明,那么存在二义性,会报错

MyClass<int,int>    m;  // 错误:同时匹配 MyClass<T,T> 和 MyClass<T,int>
MyClass<int*,int*>  m;  // 错误:同时匹配 MyClass<T,T> 和 MyClass<T1*,T2*>

缺省模板参数(预设模板自变量)

可以为模板参数定义缺省值,这些值就被称为缺省模板参数,而且他们还可以引用之前的模板参数

template <typename T, typename CONT = std::vector<T>>
class Stack {
    private:
        CONT elems;             // 元素
    public:
        void push(T const&);    // push 元素
        void pop();             // pop 元素
        T top() const;          // 传回最顶端元素
        bool empty() const {    // stack 是否为空
            return elems.empty();
        }
};

template <typename T, typename CONT>
void Stack<T,CONT>::push (T const& elem)
{
    elems.push_back(elem);      // 追加元素
} 

可以像之前单一模板参数一样使用Stack<int>,也可以指定容器类型Stack<double, std::deque<double>>

非类型模板参数

对于钼函数模板和类模板,模板参数并不限于类型,普通值也可以作为模板参数

非类型的类模板参数

template <typename T, int MAXSIZE>
class Stack {
    private:
        T elems[MAXSIZE];       // 元素
        int numElems{0};        // 当前的元素个数
    public:
        Stack();                // 构造函数
        void push(T const&);    // push 元素
        void pop();             // pop 元素
        T top() const;          // 传回 stack 顶端元素
        bool empty() const {    // stack 是否为空
            return numElems == 0;
        } 
        bool full() const {     // stack 是否已满
            return numElems == MAXSIZE;
        }
}; 

Stack<int, 100> stack1;
Stack<int, 200> stack2;

这里MAXSIZE是新加入的第二个模板参数,类型为int,指定了数组最多可包含的栈元素

但是这里的stack1stack2属于不同的类型,而且这两种类型之间也不存在隐式或者显示的类型转换,也不能互相赋值

非类型的函数模板参数

也可以为函数模板定义非类型参数

template<typename T, int Val>
T addValue(T const &x){
    return x + Val;
}

int p = addValue<int, 10>(1);

非类型模板参数的限制

非类型模板参数是有限制的,通常可以是常整数(包括枚举)或者指向外部链接对象的指针

浮点数和类型对象是不允许作为非类型模板参数的

template<double Val>
double process(double v){
    return v * Val;
}

template<std::string name>
class MyClass{
    // ...
};

上述都是错误的使用方法

之所以不能使用浮点数(包括简单的常量浮点表达式)作为模板参数有历史原因存在(未来可能支持)

std::string由于字符串文字是内部链接对象,所以不能使用它们来作为模板参数

template<char const* name>
class MyClass{
    // ...
};

extern char const s[] = "hello";
MyClass<s> x;   // ok

全局字符数组s由hello初始化,并且是extern的外部链接对象

技巧性基础知识

typename

模板中引入typename是为了说明:模板内部的标识符可以是一个类型

template <typename T>
class MyClass{
    typename T::SubType *ptr;
    // ...
}

此处使用typename被用来说明:SubType是定义域类T内部的一种类型,因此ptr是指向T::SubType类型的指针

如果不使用typename,那么SubType会被认为是一个静态成员,那么T::SubType * ptr会被认为是T::SubTypeptr的乘积

通常而言,当某个依赖于模板参数的名称是一个类型时,就应该使用typename

.template

template<int N>
void printBitset(std::bitset<N> const& bs)      // success
{
	std::cout << bs.template to_string<char>();
}

template<int N>
void printBitset(std::bitset<N> const& bs)      // error
{
	std::cout << bs.to_string<char>();
}

这里出现了一个东西.template,这个东西一般用在模板函数中,它表示后面对象调用的另一个模板函数to_string<char>()中的<不是小于号,而是模板实参列表的起始符号

只有当.->之前的对象构建去角色某个模板参数列表时,才需要注意这个问题(该问题在VS2019 C++14上并未出现,但是在旧版本和GCC上出现)

使用 this->

对于具有基类的类模板,自身使用名称x并不一定等同于this->x

template<typename T>
class Base{
    public:
        void BaseFunc(){
            std::cout << "Base" << std::endl;
        }
};

template<typename T>
class Derived : Base<T>{
    public:
        void foo(){
            BaseFunc();
        }
};

Derived<int> PP;
PP.foo();       // Error: “BaseFunc”: 找不到标识符	EmptyCpp

对于那些在基类中声明,并且依赖于模板参数的符号(函数或者变量等),都应该在其之前使用this->或者Base<T>::限定

成员模板

类成员也可以是模板。嵌套类和成员你函数都可以作为模板

Stack<float> x, y;
Stack<int> z;

x = y;      // OK 类型相同
x = z;      // Error 类型不同

也可以通过定义一个身为模板的赋值运算符

template <typename T>
class Stack {
private:
    std::deque<T> elems;   // 元素
public:
    template<typename T2>
    Stack<T>& operator = (Stack<T2> const&);
};

template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator=(Stack<T2> const& Other)
{
    if ((void*)this == (void*)&Other) {   // 判断是否赋值给自己
        return *this;
    }
    Stack<T2> tmp(Other);               // 建立 Other 的一份拷贝
    elems.clear();                      // 移除所有现有元素
    while (!tmp.elems.empty()) {              // 复制所有元素
        elems.push_back(tmp.elems.front());
        tmp.elems.pop_front();
    }
    return *this;
}

这里代码没啥大问题,只一点在operator=中,无法访问Other的elems属性,因为它是私有的。因为Stack<int>Stack<flaot>是两种不同的类型

但是,如果是相同类型,因为自己是自己的友元,所以类型相同时是可以访问彼此的私有成员属性的

这里可以把elems改为public,或者提供对外访问接口

模板的模板参数

template <typename T, typename CONT = std::vector<T>>
class Stack {
    private:
        CONT elems;             // 元素
    public:
        void push(T const&);    // push 元素
        void pop();             // pop 元素
        T top() const;          // 传回最顶端元素
        bool empty() const {    // stack 是否为空
            return elems.empty();
        }
};

Stack<int, std::vector<int>> S1;    // Success
Stack<int, std::vector> S2;         // Error

如果想要使用一个和缺省值不同的内部容器,必须两次指定元素类型

然而借助模板的模板参数,就可以只指定容器的类型,不用指定容器所含元素类型

template <typename T,
    template <typename ELEM> class CONT = std::deque>
class Stack_V {
private:
    CONT<T> elems;              // 元素
public:
	void push(const T& val);
    bool empty() const {        // stack 是否为空
        return elems.empty();
    }
};

template <typename T,
    template <typename> class CONT>
void Stack_V<T, CONT>::push(const T& val)
{
    elems.push_back(val);
}

不同之处在于第二个模板参数被声明为一个模板template <typename ELEM> class CONT,缺省值也从std::vector<T>变为std::vector,定义从CONT elems变为CONT<T> elems

作为模板参数的声明时,通常可以用typename来替换关键字class,但是这里不行,因为这里CONT时为了定义一个类,因此只能使用class

又因为template <typename ELEM> class CONT中的ELEM一般来说并不会用到,所以可以省略写法

template <typename T,
    template <typename> class CONT = std::deque>
class Stack_V {
    // ...
};

然后就是,函数模板并不支持模板的模板参数

零初始化

对于int、double或者指针等基本类型,并不存在用一个有用的缺省值对他们进行初始化的缺省构造函数,相反任何未被初始化的局部变量都有一个不确定值

void foo(){
    int x;  // 不确定值
    int* p; // p 指向某块未知内存
}

那么在模板中,一般来说都希望模板类型的变量都可以使用缺省值初始化,可是内建类型并不能满足需求

template<typename T>
void foo(){
    T x;    // x如果是内建类型,则x本身就是个不确定值
}

所以应该显示的调用内建类型的缺省构造函数,并把缺省值设置为0

比如int()就是0

template<typename T>
void foo(){
    T x = T();
}

对于模板类,在用某种类型实例化该模板后,为了确定所有的成员都已经初始化完毕,需要定义一个缺省构造函数

template<typename T>
class MyClass{
    private:
        T x;
    public:
        MyClass() : x(){

        }
}

使用字符串作为函数模板的实参

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

std::string s = "peach";
::max("peach", "apple");	// OK:类型相同
::max("tomato", "apple");	// ERROR:类型不同
::max("apple", s);	        // ERROR:类型不同

"peach"const char[6]"apple"const char[6]"tomato"const char[7],所以很明显的类型不同报错

但是如果声明的不是引用参数

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

std::string s = "peach";
::max("peach", "apple");	// OK:类型相同
::max("tomato", "apple");	// OK:退化为相同的类型
::max("apple", s);	        // ERROR:类型不同

对于非引用类型的参数,在实参演绎的过程中,会出现数组到指针(array-to-pointer)的类型转换

template<typename T>
void ref(const T& x) {
    std::cout << "x is ref " << typeid(x).name() << std::endl;
}
template<typename T>
void unref(T x) {
    std::cout << "x is unref " << typeid(x).name() << std::endl;
}

ref("hello");
unref("hello");

x is ref char const [6]
x is unref char const *

如果遇到关于字符数组和字符串指针之间不匹配的问题,可以往这方面考虑,根据实际情况有不同的解决方法

  1. 使用非引用参数,取代引用参数(会出现无用的拷贝)
  2. 重载,编写接收引用参数和非引用参数的两个重载函数
  3. 对具体类型进行重载
  4. 强制要求程序员使用显示类型转换
  5. 重载数组类型
template<typename T, int N, int M>
T const* max(T const(&a)[N], T const(&b)[M]) {
	return a < b ? b : a;
}

简单实战

包含模型

绝大多数的程序员这样组织代码结构

  1. 类和其他类型的声明放在头文件(.h .hh .hxx .hpp)
  2. 对于全局变量和非内联函数,只声明在头文件中,定义则位于(.c .cc .cxx)文件

但是,模板不行,当模板的声明和定义不在同一个文件中,会触发链接错误

因为调用模板时,模板的定义还没被实例化,为了使模板真正被实例化,编译器必须知道应该实例化哪个定义以及要基于哪个模板实参来进行实例化,可是这两部分信息位于分开编译的不同文件里面

对于上面的问题,通常采取对待宏内联函数的解决方法

  1. 将实现.cpp文件添加到声明.h的末尾
  2. 在每个使用模板的文件中,都包含进实现的.cpp文件
  3. 将定义和声明写在一起
#pragma once

// 声明
template<typename T>
void print_typeof(T const&);

// 实现
template<typename T>
void print_typeof(T const& x){
    std::cout << typeif(x).name() << std::endl;
}

将上面的这种组织方式称为包含模型

包含模型明显增加了包含头文件的开销,这也是包含模型最大的不足之处

显式实例化

分离模型

一些术语

  • 类模板:该类是一个模板
  • 模板类(通常由下面三种含义)
    • 作为类模板的同义词
    • 从模板产生的类
    • 具有一个template-id名称的类
template<typename T1, typename T2>  // 基本的类模板
class MyClass{      
    // ...
}

template<>                          // 显式特化
class MyClass<std::string, float>{
    // ...
}

template<typename T>                // 局部特化
class MyClass<T, T> {
    // ...
}
class C;            // 类C的声明
void f(int p);      // 函数f的声明
extern int v;       // 变量v的声明
template<typename T, int N>
class ArrayInClass{
    public:
        T array[N];
};

ArrayInClass<int, 10> temp;
  • 模板参数是指:位于模板声明或定义内部,关键字template后面所列举的名称(比如上面的N和T)
  • 模板实参是指:用替换模板参数的各个对象(上面的int和10)

深入模板

深入基础

参数化声明

C++支持两种基本类型的模板:类模板和函数模板

template<typename T>
class MyList {							// 命名空间内的类模板
public:
    template<typename T2>				// 成员函数模板
    MyList(MyList<T2> const&);			// 构造函数
    // ...
};


template<typename T>
template<typename T2>					 
MyList<T>::MyList(MyList<T2> const&)	// 位于类外部的成员
{										// 函数模板的定义
}

template<typename T>
int Length(MyList<T> const&);			// 位于外部命名空间作用域的函数模板

class Collection {						
    template<typename T>				// 位于类内部的成员类模板
    class Node {
        // ...
    };

    template<typename T>				// 另一个作为成员的类模板
    class Handle;

    template<typename T>				// 位于类内部的成员函数模板的定义
    T* allco() {						// 显式内联函数
        // ...
    }
};

template<typename T>					// 类外部定义的成员类模板
class Collection::Handle {
    // ...
};

除了上面的,联合模板也是允许的(通常被当作类模板)

template<typename T>
union AllocChank{
    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 cosnt & = T())

不过使用T()去初始化缺省调用实参也可能存在问题,比如下面的代码

class Value{
    public:
        Value(int);
}
void init(Array<Value> *arr){
    Value zero(0);
    fill(arr, zero);    // 正确的
    fill(arr);          // 错误的 因为不存在Value()的无参构造函数
}

除了上述两种基本类型的模板之外,还可以使用相似的符号来参数化其他的3种声明

  1. 类模板的成员函数的定义
  2. 类模板的嵌套类成员的定义
  3. 类模板的静态数据成员的定义
template <int I>
class CupBoard {
    void open();                //译注:隶属于 CupBoard class template
    class Shelf;                //译注:隶属于 CupBoard class template
    static double total_weight; //译注:隶属于 CupBoard class template
    // ...
};

template <int I>
void CupBoard<I>::open()
{
    // ...
}

template <int I>
class CupBoard<I>::Shelf {
    // ...
}; 

template <int I>
double CupBoard<I>::total_weight = 0.0;

成员函数不能被声明为虚函数,因为虚函数调用机制的普遍实现都使用了一个大小固定的表,每个虚函数都对对应表的一个入口

但是,成员函数模板的实例化个数,要等到整个程序都翻译完毕才能确定,这就和表的大小(固定的)发生了冲突

不过,类模板的普通成员函数可以是虚函数,因为当类被实例化之后,他们的个数是固定的

template<typename T>
class TestClass
{
public:
	virtual ~TestClass();               // Success

	template<typename T2>
	virtual void Copy(const T2&);       // Error : 成员函数模板不能是虚拟的
};

模板参数

3种模板参数

  1. 类型参数
  2. 非类型参数
  3. 模板的模板参数

类型参数是通过关键字typename或者class引入的,关键字后面必须是一个简单的标识符,使用逗号隔开多个标识符,等号(=)表示缺省模板实参

在模板声明内部,类型参数的作用类似于typedef。如果T是一个模板参数,就不能使用注入class T等形式的修饰名称,即使T是一个要被class类型替换的参数也不可以

template<typename Allocator>
class List{
    class Allocator* allocator;     // Error
    friend class Allocator;         // Error
};

非类型参数表示的是 在编译器或链接期可以确定的常值

整个常值必须是

  1. 整数或者枚举类型
  2. 指针类型
  3. 引用类型

所有其他的类型都不允许作为非类型参数使用

template<typename T,                            // 类型参数
            typename T::Allocator* Allocator>   // 非类型参数

template<int buf[5]>
class Lexer;
// 上面等价于下面
template<int *buf>
class Lexer;

template<int const length>
class Buffer;
// 上面等同于下面 const是无效的
template<int length>
class Buffer;

非类型模板参数只能是右值,不能被取地址,不能被赋值

模板的模板参数是代表类模板的占位符,不能使用关键字class或union

template<template<typename X> class C>  // Success
void f(C<int>* p);

template<template<typename X> struct C> // Error
void f(C<int>* p);

template<template<typename X> union C>  // Error
void f(C<int>* p);

模板的模板参数的参数可以具有缺省模板实参

template<template<typename T,
                    typename A = MyLoocator> class Container>
class Adaptation{
    Container<int> storage; // 隐式等同于 Container<int, MyLoocator>
    // ...
}

对于模板的模板参数而言,它的参数名称只能被自身其他参数的声明使用

template<template<typename T, T*> class Buf>
class Lexer{
    static char storage[5];
    Buf<char, &Lexer<Buf>::storage[0]> buf;
    // ...
};

template<template<typename T, T*> class List>
class Node{
    static T* storage;  // Error 模板的模板参数的参数不能被用在这里
    // ...
}

任何类型的模板参数都可以拥有一个缺省实参,只要该缺省实参能够匹配整个参数就可以。显然缺省实参不能依赖于自身的参数,但可以依赖于前面的参数

template <typename T, typename Allocator = allocator<T>>
class List;

与缺省的函数调用实参的约束一样,对于任意一个模板参数,只有在之后的模板参数都提供了缺省实参的情况下,才能具有缺省模板实参

template <typename T1, typename T2, typename T3, typename T4 = char, typename T5 = char>    // Success
class MyClass1;

template <typename T1, typename T2, typename T3, typename T4 = char, typename T5>           // Error
class MyClass1;

template <typename T1 = char, typename T2, typename T3, typename T4, typename T5>           // Error
class MyClass1;

除此之外,缺省实参不能重复声明

template<typename T = void>
class Value;

template<typenaem T = void>
class Value;            // 重复出现的缺省实参

模板实参

模板实参是指:在实例化模板时,用来替换模板参数的值

  1. 显式模板实参:紧跟在模板名称后面,尖括号内部的显式模板实参值。所组成的整个实体称为template-id
  2. 注入式类名称:对于具有模板参数P1、P2...的类模板X,在它的作用域中,模板名称X等同于template-id
  3. 缺省模板实参
  4. 实参演绎:对于不是显式指定的函数模板实参,可以在函数的调用语句中,根据函数调用实参的类型来演绎出函数模板实参

函数模板实参

对于函数模板的模板实参,可以显式的指定它们,也或者借助模板的使用方式对他们进行实参演绎

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

int main(){
    max<double> (1.0, 2.0); // 显式指定模板实参
    max(1.0, 2.0);          // 实参演绎
    max<int>(1.0, 2.0);     // 显式的
    return 0;
}

但是某些模板实参永远也得不到演绎的机会,所以最好时把实参所对应的参数放在模板参数列表的开始处,从而可以显示的指定这些参数,而其他的参数仍然可以进行实参演绎

template<typename DstT, typename SrcT>
inline DstT implicit_cast(SrcT const& x){   // SrcT可以被演绎 但是DstT不可以
    return x;
}

double val = implicit_cast<double>(1);

比如把implicit_cast改为template<typename SrcT, typename DstT>就必须显式的指定两个模板实参

由于函数模板可以被重载,所以显式提供所有的实参并不足以表示每一个函数

template <typename Func, typename T>
void apply(Func func_ptr, T x)
{
	func_ptr(x);
}
template <typename T> void single(T);
template <typename T> void multi(T);
template <typename T> void multi(T*);

void foo() {
	apply(single<int>, 1);  // success
	apply(multi<int>, 1);   // error T 或者 T* 都可以被匹配
}

另外,在函数模板种,显式的指定模板实参可能会试图构造一个无效的C++类型

template<typename T>
RT1 test(typename T::X const *);

template<typename T>
RT2 test(...);

表达式test<int>会让第一个函数模板毫无意义,因为int中没有名为X的类型,但是它可以匹配第二个函数模板。因此,表达式&test<int>能够表示一个唯一函数地址(第二个函数地址) 所以不能用int来替换第一个模板的参数,并不意味着错误,替换失败并非错误(substitution-failure-is-not-an-error, SFINAE)

typedef char RT1;
typedef struct { char a[2]; } RT2;

#define type_has_memeber_type_X(T) (sizeof(test<T>(0)) == 1)

template<typename T>
RT1 test(typename T::X const*) {
	return 1;
}

template<typename T>
RT2 test(...) {
	return 0;
}

void foo_temp() {
	std::cout << type_has_memeber_type_X(int) << std::endl;
	std::cout << type_has_memeber_type_X(TestClass) << std::endl;
}

于是,就可以在编译器检查给定类型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>; // ERROR:除数为 0
}

非类型实参

非类型模板实参是那些提花你非类型参数的值,必须是下面的某一种

  1. 某一个觉有正确类型的非类型模板参数
  2. 一个编译器整形常值或枚举值。这只有在参数类型和值类型能够进行匹配,或值的类型隐式地转换为参数类型的前提下,才是合法的
  3. 漆面有单目运算符&(取地址)的外部变量或者函数的名称。对于函数或者数组变量,&运算符可以省略
  4. 对于引用类型的非类型模板参数,前面没有&运算符的外部变量和外部函数也是可取的
  5. 一个指向成员的指针常量
template <typename T, T nontype_param>
class C;

C<int, 33>* c1;                         // 整数型(integer type)

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

void f();
void f(int);
C<void (*)(int), f>* c3;                // 一个函数名称。重载解析规则在此选用 f(int)。& 会被隐寓加入。

class X {
    public:
        int n;
        static bool b;
};
C<bool&, X::b>* c4;                     // static class members 都可接受

C<int X::*, &X::n>* c5;                 // pointer-to-member 常数亦可接受

template<typename T>
void templ_func();

C<void (), &templ_func<double> >* c6;   // function template 具现体也是一种函数

模板实参的一个普遍约束是在程序创建的时候编译器或者链接器要能够确定实参的值

另外,有一些常值不能作为有效的非类型实参

  1. 空指针常量
  2. 浮点值
  3. 字符串

友元

友元类的声明不能是类定义,在引入模板之后,友元类声明的唯一变化只是:可以命名一个特定的类模板实例为友元

class TestClass
{
};

template<typename T>
class Node {
};

template<typename T>
class Tree {
	friend TestClass;   // OK
	friend Node<T>;     // OK
};
template<typename T>
class Tree {
	friend TestClass;   // Error
	friend Node<T>;     // Error
};

class TestClass
{
};

template<typename T>
class Node {
};

显然,如果要把类模板的实例声明为其他类或者类模板的友元,该类模板在声明的地方必须是可见的

友元函数

template <typename T1, typename T2>
void combine(T1, T2);

class Mixer {
    friend void combine<>(int&, int&);          // OK:T1 = int&, T2 = int&
    friend void combine<int, int>(int, int);    // OK:T1 = int, T2 = int
    friend void combine<char>(char, int);       // OK:T1 = char T2 = int
    friend void combine<char>(char&, int);      // ERROR:与 combine() template 不匹配
    friend void combine<>(long, long) { ... }   // ERROR:不能在此处定义函数
}; 

不能在友元声明中定义一个模板实例,所以命名一个实例的友元声明不是不行的