C++ Primer 学习笔记 第十六章 模板与泛型编程

泛型编程与OOP都能处理在编写程序时不知道类型的情况,但泛型编程在编译时就能获知类型了。

模板是泛型编程的基础,它是创建一个类或函数的蓝图或公式。vector为泛型类,find为泛型函数。

当多个函数除了形参类型外完全相同,可以用泛型函数将多个函数简化为一个,以下为一个函数模板,它可以比较任何定义了<号的类型:

template <typename T> int compare(const T &v1, const T &v2) {    //T没任何特殊含义,也可以使用其他名字代替
    if (v1 < v2) {
        return -1;
    }
    if (v2 < v2) {
        return 1;
    }
    return 0;
}

模板定义以template关键字开始,后跟一个模板参数列表,它含有多个模板参数,模板参数列表不能为空。我们使用该函数时,会隐式或显式地将模板实参绑定到模板参数上。

调用compare时,编译器通常用函数实参来推断模板实参:

compare(1, 0);    //编译器推断T为int

以上我们用编译器推断出的模板参数来为我们实例化一个特定版本的函数,这种编译器生成的版本通常被称为模板的实例。

类型参数可以用来当做函数返回类型和声明时使用的类型:

template <typename T> T foo(T *p) {
    T tmp = *p;
    return tmp;
}

类型参数前要么加typename,要么加class:

template <typename T, U> func() { }    //错误
template <typename T, class U> func() { }    //正确,此处的typename和class是同义词,可以互换使用

typename引入C++晚于class。

模板中还可以定义非类型参数,它表示一个值而非一个类型,我们通过一个特定的类型名来指定非类型参数,非类型参数被一个用户提供或编译器推断出来的值初始化,该值必须是常量表达式,从而允许编译器在编译时实例化模板:

template<unsigned N, unsigned M> int compare(const char (&p1)[N], const char (&p2)[M]) {    //不能拷贝数组,因此使用数组的引用
    return stecmp(p1, p2);    //比较p1和p2
}

调用它:

compare("hi", "mom");    //编译器会实例化出int compare(const char (&p1)[3], const char (&p2)[4])

非类型参数可以是整型,或者是一个指向对象或函数的指针或左值引用。如非类型参数是整型,那么整型必须是一个常量表达式;如非类型参数是引用或指针,那么绑定到它上的对象必须是static的或全局变量。指针参数也可以用值为0的常量表达式或nullptr来实例化。

模板中,非类型参数是一个常量值,可以用在需要常量表达式的地方,如数组大小。

函数模板也可以是inline或constexpr的,这两个关键字应放在模板参数列表后,函数返回类型前。

我们的compare模板参数是引用,这保证函数可以用于不能拷贝的类型。并且函数中只用了<,降低了对要处理类型的要求,它可以只定义<而不定义>。

我们可以使用函数对象less<T>()来扩展以比较两个指针在内存中的地址。

当我们使用模板时,编译器才生成代码,为生成一个实例化的版本,编译器需要模板的定义,因此模板的头文件既包含声明,也包含定义,而其他函数推荐将声明放在头文件,而定义放在源文件中。

编译器检查模板错误的三个阶段:
1.编译模板本身时,可以检查语法错误。
2.使用模板时,可以检查实参数目是否正确、实参类型是否匹配,对于类模板,还会检查是否定义了正确数目的模板实参。
3.模板实例化时,发现类型相关错误,这类错误可能发生在链接时。

编译器不能为类模板推断模板参数的类型。

使用类模板:

//StrBlob类的推广版本
template <typename T> class Blob{
public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_type;    //typename关键字用来指明vector<t>的size_type成员是一个类型而不是一个成员函数或数据成员
    //其余与StrBlob类相似
};

类模板实例化时尖括号括起来的是显式模板实参列表,编译器会实例化出一个独立的类,它重写模板类,将模板参数类型替换为我们提供的类型。

类模板的名字不是一个类型名。

类模板的成员函数可以定义在类外,在类外定义也要加template关键字:

//类内声明
ret-type member-name(parm-list);
//类外定义
template <typename T> ret_type Blob<T>::member-name(parm-list) { }

模板类的模板成员函数在用到时才会实例化,这一特性使得即使某种类型不能完全符合模板类的要求,也能用该类型实例化类。

在类模板的作用域中,可以直接使用类名而不提供模板实参。但在类模板外定义其成员时,直到遇到类名成员函数前的类名时,才进入了模板类的作用域:

template <typename T> BlobPtr<T> BlobPtr<T>::operator++(int) {    //函数名前的类名后,才进入模板类的作用域
    BlobPtr ret = *this;    //此处可以直接使用类名
    ++*this;
    return ret;
}

如类模板中有一个非类模板的友元类声明,那么该友元类可以访问所有实例化的模板类的非public成员。如果该友元类是模板类,类可以授权给所有友元模板实例,也可以只授权给特定实例。

将一个特定的实例化声明为友元:

//前置声明,在Blob中声明友元所需要的
template <typename> class BlobPtr;
template <typename> class Blob;    //运算符==中的参数类型,需要先声明
template <typename T> bool operator==(const Blob<T> &, const Blob<T> &);

template <typename T> class Blob {
    friend class BlobPtr<T>;    //需要模板类的前置声明
    friend bool operator==<T>(const Blob<T> &, const Blob<T> &);    //需要模板类的前置声明
};

以上友元的声明用Blob的模板形参作为它们自己的模板形参,因此友好关系被限定在用相同类型实例化的Blob与BlobPtr相等运算符之间:

Blob<char> ca;    //友元只有BlobPtr<char>和operator==<char>
Blob<int> ia;    //ia与ca没有友元关系,相互都无法访问非public部分

将所有实例化都声明为友元:

template <typename T> class Pal;

class C {
    friend class Pal<C>;    //用C实例化的Pal是C的友元
    template <typename T> friend class Pal2;    //Pal2的所有实例化都是C的友元,这种情况下无需前置声明
};

template <typename T> class C2 {
    friend class Pal<T>;    //只有相同实例化的Pal才是C2对应实例化的友元
    template <typename X> friend class Pal2;    //Pal2的是所有实例化都是C2的友元,而不管C2是怎样实例化的,这种情况下无需前置声明,且必须使用与类模板本身不同的模板参数
};

C++11中,我们可以将模板类型参数声明为友元:

template <typename Type> class Bar {
    friend Type;    //将Type类型声明为Bar<type>这一实例的友元,允许与内置类型的友好关系
};

可以为类模板的一个实例定义类型别名:

typedef Blob<string> StrBlob;

但不能:

typedef Blob<T> TBlob;    //错误,因为Blob<T>是模板而不是类型

但C++11允许为类模板定义一个类型别名:

template<typename T> using twin = pair<T,T>;
twin<string> authors;    //authors是一个pair<string,string>,这样用户只用指定一次类型

template <typename T> using partNo = pair<T,unsigned>;
partNo<string> books;    //books是pair<string, unsigned>

模板类也可以有static成员,此时,只有相同实例化的模板类可以共享相同的static成员。如int实例化的所有模板类共享相同static成员,而double实例化的所有模板类共享另一个static成员,而int实例化产生的static成员不能被double实例化的类共享。

模板类static成员的类外定义:

template <typename T> size_t Foo<T>::ctr = 0;

访问static成员时可以通过类名访问:

Foo::count();    //错误
Foo<int>::count();    //正确

与模板类的其他成员函数一样,static成员函数也是在用到的时候实例化。

即使之前没有实例化double,也能使用Foo<double>的static成员:

template <typename T> class Foo {
public:
    static std::size_t count() {
        return ctr;
    }
private:
    static std::size_t ctr;
};

template <typename T> size_t Foo<T>::ctr = 1;

int main() {
    Foo<int> fi;
    cout << fi.count() << endl;
    cout << Foo<int>::count() << endl;
    cout << Foo<double>::count() << endl;
}

模板参数的作用域范围为在其声明之后,到模板声明或定义结束之前。模板参数会覆盖外层的相同名字:

typedef double A;

template <typename A, typename B> void f(A a, B b) {
    A tmp = a;    //tmp是模板参数的A类型,而非double
    double B;    //错误,不能重用模板参数名
}

一个模板参数名在一个特定模板参数列表中只能出现一次。

模板的声明必须包含模板参数:

template <typename T> T calc(const T &, const T &);

一个特定文件所需的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。

当编译器遇到模板类的成员时,它不会知道该成员是一个类型成员还是其他成员,直到实例化时才知道,但为了处理模板,编译器必须知道名字是否表示一个类型,如:

T::size_type *p;    

对于模板类,如遇到上述代码,有两种可能结果,一是size_type是T的类型成员,这就相当于声明了一个该类型指针;二是size_type可能是一个static数据成员,这就相当于该数据成员与p相乘。默认情况下,C++假定通过作用域运算符访问的名字不是类型,因此如我们想使用模板参数的类型成员,就必须显示告诉编译器该名字是一个类型:

template <typename T> typename T::value_type func() {
    return typename T::value_type();
}

C++11可以为模板提供默认模板实参,而更老的C++版本只允许为类模板提供。

template <typename T, typename F = less<T>> int compare (const T &v1, const T &v2, F f = F()) {
    if (f(v1, v2)) {
        return -1;
    }
    if (f(v2,v1)) {
        return 1;
    }
    return 0;
}

用户调用以上版本的compare函数时,可以提供自己的比较函数,但这不是必要的。而以下写法不允许用户提供自己的操作,用户提供的操作必须返回bool值,且接受的实参类型必须与compare的前两个参数类型匹配:

template <typename T> int compare(const T& v1, const T& v2, less<T> f = less<T>()) {
    if (f(v1, v2)) {
        return -1;
    }
    if (f(v2, v1)) {
        return 1;
    }
    return 0;
}

它没有将第三个参数的类型声明为模板形参,因此也就不能根据用户提供的操作推断出类型,从而只能使用less<T>来比较大小,此时就没有必要将第三个参数放在参数列表中了,应放在函数体中。

与函数默认实参一样,对于模板参数,只有当它右边所有模板参数都有默认模板实参时,它才可以有默认模板实参。

实例化类模板必须提供模板实参,当模板类的所有模板形参都有了默认的模板实参,我们也要使用一个空的尖括号对。

只有在模板定义时才能使用inline:

inline template <typename T> T foo(T, unsigned int *);    //错误,声明时不加inline
template <typename T> T foo(T, unsigned int *);    //正确
typedef int Ctype;
template <typename Ctype> Ctype f5(Ctype a);    //返回类型和形参a的类型都是模板参数的类型

无论是普通类还是模板类,都能包含模板函数成员,被称为成员模板,它不能是虚函数。

普通类的成员模板:

//函数对象类,可以对给定指针执行delete
class DebugDelete {
public:
    DebugDelete(std::ostream &s = std::cerr) : os(s) { }
    template <typename T> void operator()(T *p) const {
        os << "deleting unique_ptr" << std::endl;
        delete p;
    }
private:
    std::ostream &os;
};

DebugDelete d;
double *p = new double;
d(p);    //实例化成员模板
int *ip = new int;
d(ip);    //实例化成员模板

unique_ptr<int, DebugDelete> p(new int, DebugDelete());    //声明p的删除器类型为DebugDelete,unique_ptr的析构函数会调用DebugDelete的调用运算符,此时实例化成员模板

类模板的成员模板:

template <typename T> class Blob {
    template <typename It> Blob(It b, It e);    //接受不同类型的迭代器
};

//类外定义
template <typename T> template <typename It> Blob<T>::Blob(Itb, It e) : data(std::make_shared<std::vector<T>>(b, e)) { }

int ia[] = { 0,1,2,3,4 };
Blob<int> a(begin(ia), end(ia));    //同时实例化了模板类和模板成员

模板使用时才会实例化,这意味着当两个或多个独立编译的源文件使用了相同的模板并提供了相同的模板参数时,每个文件中都会有该模板的一个实例。这在大系统中的开销会很严重。C++11中我们可以显式实例化来避免这种开销:

extern template class Blob<string>;    //实例化声明
template int compare(const int &, const int &);    //实例化定义

当编译器遇到extern的模板声明时,它不会在本文件中生成实例化代码,还意味着表示承诺在程序的其他位置有该实例化的一个非extern声明(定义)。对一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。

编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何使用该实例化版本的代码之前:

//Application.cc
extern template class Blob<string>;
extern template int compare(const int &, const int &);
Blob<string> sa1, sa2(sa1);    //实例化会出现在其他位置
Blob<int> ia = { 0,1,2,3 };    //在此处实例化
Blob<int> ia2(ia);
compare(a[0], a[1]);

在编译后,Application.o将包含Blob<int>的实例和接受initializer_list参数的构造函数的实例以及拷贝构造函数的实例。

//templateBuild.cc
template int compare(const int &, const int &);
template class Blob<string>;

文件templateBuild.o中含compare的int实例和Blob的string实例。编译此应用程序时,必须把template.o和Application.o链接到一起。

类模板的普通实例化时,模板参数类型不一定适用于此模板类的所有成员函数,因为成员函数在使用时才实例化。但类模板的实例化定义时,由于不知道在其他文件中会用到该类模板的哪些成员函数,因此需要模板类型能用于所有成员函数,即在模板实例化定义时,需要实例化所有成员函数而不是用到时再实例化,因此该参数类型必须适用于所有成员函数。

下面哪些语句发生了模板的实例化:

template <typename T> class Stack { };

void f1(Stack<char>);    //实例化

class Exercise {
    Stack<double> &rsd;    //实例化
    Stack<int> si;    //实例化
};

int main() {
    Stack<char> *sc;
    f1(*sc);
    int iObj = sizeof(Stack<string>);    //实例化
}

unique_ptr的删除器是unique_ptr对象类型的一部分,而shared_ptr只需创建时或reset时传入一个可调用对象即可。

shared_ptr不是将删除器直接保存为一个成员,因为删除器的类型直到运行时才知道,并且可以随时改变,只要reset即可,而类成员的类型在运行时是不能改变的。假定shared_ptr将它管理的指针保存在成员p中,且删除器是通过一个成员del来访问的,则shared_ptr的析构函数会包含类似以下含义的语句:

del ? del(p) : delete p;    //伪代码,del绑定了删除器时,使用del,否则使用delete

由于删除器是间接保存的,删除器只能保存为一个指针或封装了指针的类,因此运行时需要通过指针跳转来调用删除器,这样有额外时间开销。

而unique_ptr的删除器类型是类类型的一部分,删除器的类型在编译时就已知,因而删除器可以直接保存在unique_ptr对象中,这样在它的析构函数中直接使用用户提供的删除器或delete即可:

del(p);

如果用户提供的删除器类似于DebugDelete,那么这个调用可能被编译为内联模式,因为我们使用的用户提供的删除器实际上是一个可调用对象,而可调用对象在调用时一般会发生函数跳转,但DebugDelete的调用运算符是一个类内定义的成员函数,因此它是隐式的内联函数,因此就不用函数跳转了。但析构函数中这个删除器调用也不一定为内联的,因为内联只是一个请求,编译器会根据发出内联请求的函数的长度、复杂程度等来确定是否真的要内联。

编译时绑定删除器的unique_ptr避免了间接调用删除器的运行时开销,而运行时绑定删除器的shared_ptr使用户重载删除器更方便。效率与灵活性不可得兼。

从函数实参来确定模板实参的过程被称为模板实参推断。若模板实参类型不是函数形参类型,则不能推断出模板实参类型,此时需要显式实例化模板函数:

template <typename F, typename T> F void func(T t);

int main() {
    func(1);    //错误
    func<double>(1);    //正确,使用显式模板实参,调用double func(int),显式模板实参的第一个模板实参与第一个模板参数匹配,以此类推
}

糟糕的设计:

template <typename T1, typename T2, typename T3> T3 func(T2, T1);

int i = 0;
long long ll = 0;
func<long long, int, long>(i, ll);

如果一个函数的形参类型使用了模板参数类型:

template <typename T> T fobj(T, T);    //实参被拷贝给形参
template <typename T> T fref(const T &, const T &);    //形参是实参的引用

string s1("a value");
const string s2("another value");

fobj(s1, s2);    //调用fobj(string, string)
fref(s1, s2);    //调用fref(const string &, const string &)

int a[10], b[42];

fobj(a, b);    //调用fobj(int *, int *)
fref(a, b);    //错误,两个数组类型不匹配

如果模板函数有两个模板参数类型的形参,那么在使用时,这两个形参必须传递相同类型的实参,只有两个例外,一是顶层const的转换,二是数据或函数向指针类型的转换,这两种转换例子如上。

template <class T> int compare(const T&, const T&);

int main() {
    compare("hi", "world");    //错误,因为字符串字面值的类型为const char[n],n为元素数,并且模板函数的参数类型为const的引用
    compare("bye", "dad");    //正确
    compare<string>("hi", "world");    //正确,都转换为了string
}
template <typename T> void f1(T, T) { }

int i = 0, j = 42;
const int *cp1 = &i, *cp2 = &j;

f1(cp1, cp2);    //正确,T可以推断为const int *

对于已经指定了模板实参的实例,可以进行普通类型转换:

template <class T> int compare(const T&, const T&);

long lng;
compare(lng, 1024);    //错误,两实参类型不同
compare<long>(lng, 1024);    //正确,调用compare(long, long)
compare<int>(lng, 1024);    //正确,调用compare(int, int)

标准库max函数接受两个相同类型的参数:

max(1, 1.0);    //错误
max<int>(1, 1.0);    //正确

调用make_shared时必须要提供类型,因为提供的模板实参没有用在函数形参中,而是用于返回类型。

如果我们想定义一个接受一对迭代器,处理完后返回迭代器所指元素的类型的函数:

template <typename T ,typename It> T &fcn(It beg, It end) {
    //处理序列
    return *beg;
}

但上例方法会让用户再输入返回类型,而返回类型就是迭代器所指类型的引用,可以使用decltype(*beg)来获取,但在返回类型出还没有出现beg,因此我们需要使用C++11引入的尾置返回类型:

template <typename It> auto fcn(It beg, It end) -> decltype(*beg) {    //此时beg就是已知的了
    //处理序列
    return *beg;
}

但当上例的返回值不是迭代器指向值的引用,而是迭代器指向的值的类型时,就不能这么做了,因为解引用迭代器获得的类型是它所指类型的引用。为获取元素类型,我们可以使用标准库的类型转换模板,它们定义在头文件type_traits中,这个头文件中的类常用于模板元程序设计中:
在这里插入图片描述
我们就可以使用remove_reference来去掉引用:

template <typename It> auto fcn(It beg, It end) -> typename remove_reference<decltype(*beg)>::type {    //type是一个类的成员,而该成员依赖于模板参数,因此返回类型前要加typename告知编译器type是一个类型
    //处理序列
    return *beg;
}

而如果返回类型为:

vector<int> size_type;    //不用在前面加typename,因为vector的实现上,size_type与模板参数没有关系

另一种消除引用的方法:

template <typename It> auto fcn(It beg, It end) -> decltype(*beg + 0) {    //返回类型是加法运算符的返回类型,而非该类型的引用,但要求这两个类型能相加
    //处理序列
    return *beg;
}

函数指针和模板实参推断:

template <typename T> int compare(const T &, const T &);
int (*pf)(const int &, const int &) = compare;    //pf指向实例int compare(const int &, const int &),pf中参数类型决定了compare的模板实参类型

void func(int (*)(const string &, const string &));
void func(int (*)(const int &, const int &));
func(compare);    //错误
func(compare<int>);    //正确,使用func的int版本

从左值引用函数参数推断模板实参类型:

template <typename T> void f1(const T &);

f1(i);    //i是int,T为int
f1(ci);    //ci是const int,T为const int
f1(5);    //T为int

从左值引用函数参数推断模板实参类型:

template <typename T> void f3(T &&);
f3(42);    //T为int
f3(i);    //i是int,T为int&

C++两个额外的绑定规则:
1.将一个左值实参传递给函数的右值引用类型的形参且此右值引用是模板类型的右值引用时,编译器推断模板实参为实参的左值引用类型,因此,我们调用f3(i)时,T的类型为int&,而非int,此时f3的函数参数类型看起来是一个左值引用的右值引用,通常,我们不能定义一个引用的引用,但通过类型别名或模板类型参数间接定义是可以的。

引用的引用:

int i - 0;
int &ref1 = i;    //正确,ref1是一个引用
int &ref2 = ref1;    //正确,ref2是一个引用
int &&ref3 = ref1;    //错误,ref3是一个引用的引用

类型别名间接定义引用的引用:

using intRef = int &;    //用typedef亦可
int a = 1;
intRef &rrefa = a;    //正确,rrefa是引用的引用

2.如果我们间接创建了引用的引用,那么这些引用形成了折叠,即会折叠成一个普通的左值引用类型。C++11中,折叠规则扩展到右值引用:
(1)X& &、X& &&、X&& &都折叠成X&。
(2)X&& &&折叠成X&&。

我们再回到:

template <typename T> void f3(T &&);
f3(i);    //i是int,T为int&

f3(i)的调用过程为,首先i是一个左值,并且传给了模板参数类型的右值引用形参,根据规则1,f3被实例化为:

void f3(int & &&);    //错误代码,只是为了说明过程

再根据规则2,将左值的右值引用折叠成:

void f3(int &);

综上,这两个规则导致了:
1.当一个函数形参类型是一个模板参数的右值引用(T &&)时,它可以被绑定到左值上。(普通的右值引用形参不能绑定在非右值上)并且2。
2.如实参是一个左值,则模板实参类型被推断为左值的引用,并最终被折叠成左值的引用。

template <typename T> void f3(T &&val) {
    T t = val;    //拷贝还是绑定一个引用?
    t = fcn(t);    //赋值只改变t还是既改变t又改变val?
    if (val == t);    //若T是引用,则一直为true
}

当我们用右值调用f3时,如42,T被推断为int,此时t类型也是int,且是拷贝的val的值来初始化的,此时我们对t赋值,val不变。

而当我们用一个int左值调用f3时,则T为int &,此时t类型为int &,即t为val的一个引用,此时对t赋值也会影响到val,导致if判断总为true。

输入不同,代码逻辑也不同,一般,使用模板类型的右值引用形参的模板通常会重载:

template <typename T> void f(T &&);    //绑定到非const的右值
template <typename T> void f(const T &);    //绑定到左值和const右值
template <typename T> void g(T &&val);
int i = 9;
const int ci = i;
g(ci);    //实例化为void g(const int &),引用的const是底层const,因此会被保留

//而如果g的参数不是T&&而是T,则
g(ci);    //实例化为void g(int);因为顶层const会被忽略

vector中不能存放引用,因此若上例函数g中有创建存放T的vector的声明语句,且传入g的参数为左值时,即T被推断为左值引用时,代码会报错。

move函数的定义:

template <typename T> typename remove_referedce<T>::type &&move(T &&t) {
    return static_cast<typename remove_reference<T>::type &&>(t);
}

move函数可以同时接受左值和右值的参数,如传递给move一个右值的string参数时:
1.推断出T的类型为string。
2.remove_reference用string实例化。
3.remove_reference<string>的type成员是string。
4.move的返回类型是string&&。
5.move的参数t的类型为string&&。
因此对于返回语句,t的类型已经是string&&,这个类型转换什么也不做,因此此调用的结果就是它所接受的右值引用。

当传给move一个左值string参数时:
1.推断出T的类型为string &。
2.remove_reference用string &实例化。
3.remove_reference<string &>的type成员是string。
4.move的返回类型是string &&。
5.move的参数t的类型被折叠为string &。
返回语句中,将string &转换为string &&,它的返回值为与传入的左值相绑定的右值。

static_cast可以显式地把一个左值转化为一个右值引用,一旦这么做了,这个左值就不应该再使用了。而我们不能隐式地将左值转换为右值引用,防止意外地进行这种转换。

虽然我们也可以写类似于move功能的函数,但最好用move,这样找到截断左值的代码比较容易。

转发:将一个或多个实参连同类型、const、左右值等属性不变地转发给其他函数。

例子:

//调用传入的可调用对象,将两个额外参数逆序传给它
template <typename F, typename T1, typename T2> void flip1(F f, T1 t1, T2 t2) {
    f(t2, t1);
}

但当我们传入的可调用对象如下时:

void f(int v1, int &v2) {
    cout << v1 << " " << v2++ << endl;
}

f与flip1的功能就不同了:

f(42, i);    //i被改变了
flip1(f, i, 42);    //i未被改变,因为t1被推断为int,从而t1是拷贝而来的值,再传入f改变的就是t1而不是i了

解决以上问题:

template <typename F, typename T1, typename T2> void flip2(F f, T1 &&t1, T2 &&t2) {
    f(t2, t1);
}

我们将t1和t2声明为引用,这保证了const的传递,因为引用的const是底层const,会被保留下来,而模板参数类型的右值引用又保留下来了左右值信息,如传入左值,t1的类型为左值引用,此时如果f参数是左值引用,则可以通过f改变t1实参的值,如f参数不是左值引用,那么会将t1拷贝给f,从而不会改变t1实参的值,符合f的功能。

但问题解决的不够彻底,如果函数f接受的是一个右值:

void f(int &&i, int &j);
 
flip2(f, i, 42);    //错误,42将传递给t2,而t2在flip2中是一个类型为int&&的左值,t2又被传递给f的右值引用类型形参,而左值不能传递给右值引用类型形参

注:

void f(int&& i, int& j);

int main() {
    int i = 1;
    int &&a = std::move(i);
    f(a, i);    //错误,a是int&&类型左值,报错左值不能绑定在右值引用上
}

C++11中有一个名为forword的标准库设施,它定义在头文件utility中,这一点与move相同,并且它和move一样,最好使用std::forward它可以解决上述问题。

forword与move不同,它只能通过显式模板实参来调用。forword返回该显式实参类型的右值引用,即forward<T>返回类型为T &&。它用于一个模板参数类型的右值引用的函数形参时,forword会保持实参类型所有细节:

template <typename F, typename T1, typename T2> void flip(F f, T1 &&t1, T2 &&t2) {
    f(std::forward<T2>(t2), std::forward<T2>(t1));
}

当我们传递给t1一个左值int时,T1被推断为int &,通过forword返回一个int & &&,这被引用折叠为int &,类型不变;传递给t1一个右值int时,T1的类型被推断为int,forward返回一个int &&,类型不变。

模板函数可以重载,一个调用的候选函数包括模板实参推断成功的函数模板实例。候选的函数模板总是可行的,因为在模板实参推断时就排除了不可行的模板。之后可行函数按类型转换优先级排序。如多个函数提供一样好的匹配:
1.如有一个是非模板函数,选择它。
2.如没有非模板函数,但有多个函数模板都可行,且一个模板比其他模板更特例化,选择它。
3.否则,调用有歧义。

例子:

template <typename T> string debug_rep(const T &t) {    //T应具备输出运算符
    ostringstream ret;
    ret << t;    //调用T类型的输出运算符
    return ret.str();    //将T的输出运算符要打印的内容以string方式return
}

template <typename T> string debug_rep(T *p) {
    ostringstream ret;
    ret << "pointer: " << p;    //打印指针的值,不能用于char *,标准库的char *打印的是空字符前的字符串的内容
    if (p) {
        ret << " " << debug_rep(*p);    //打印p指向的值
    }
    else {
        ret << " null pointer";
    }
    return ret.str();
}

对于以下调用:

string s("hi");
cout << debug_rep(s) << endl;    //只有第一个可行,因为string无法转换为指针类型

对于以下调用:

cout << debug_rep(&s) << endl;

第一个可行实例:debug_rep(const string * &),T被绑定到string *,但此调用非精确匹配,因为增加了底层const。
第二个可行实例:debug_rep(string *),T被绑定到string,此调用是精确匹配。
第二个是精确匹配,第一个还进行了增加const的转换,因此会调用第二个。

对于以下调用:

const string *sp = &s;
cout << debug_rep(sp) << endl;

第一个可行实例:debug_rep(const string * &),T被绑定到string *,是精确匹配。
第二个可行实例:debug_rep(const string *),T被绑定到const string,是精确匹配。
两个都是精确匹配,但由于模板debug_rep(const T &)可以用于任何类型,比debug_rep(T *)更通用,因此会选择更特例化的版本。如没有这条规则,将无法对一个const的指针调用指针版本的函数,这样调用将是有歧义的。

由于编译器会选择更特例化的版本,处于同样原因,当有模板函数和非模板函数同等匹配的情况将选择非模板函数。

但输入C风格字符串时:
1.第一个实例化为debug_rep(const T &),T被绑定到char[10],即,debug_rep(const char (&p)[10])。
2.第二个实例化为debug_rep(T *),T被绑定到const char,数组向对应指针类型的转换是精确匹配。
3.非模板函数debug_rep(const string &),要求C风格字符串向string转换。
第三个首先排除,它有类类型转换,而前两个都是精确匹配,但第二个是更特例化的匹配,因此调用第二个。如希望像string一样处理C风格字符串,即调用接受string的非模板重载函数版本,可以定义两个非模板的重载函数:

template <typename T> string debug_rep(const T &t);
template <typename T> string debug_rep(T *p);
string debug_rep(const string &);    //必须存在,如不存在,下面函数将使用上面的模板的实例
string debug_rep(char *p) {
    return debug_rep(string(p));
}

C++11中有可变参数模板,它是接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包,存在两种参数包:模板参数包(表示0个或多个模板参数)、函数参数包(表示0个或多个函数参数):

//Args是模板参数包,rest是函数参数包
//Args表示零个或多个模板类型参数
//rest表示零个或多个函数参数
template <typename T, typename... Args> void foo(const T &t, const Args& rest);    //错误,函数形参列表中如一个参数类型为模板参数包,那么它也必须是一个函数参数
template <typename T, typename... Args> void foo(const T &t, const Args&... rest);    //正确

编译器从函数实参推断模板参数类型,如果是可变参数模板,还会推断出参数包中参数的数目:

int i = 0;
double d = 3.14;
string s = "aaa";
foo(i, s, 42, d);    //包中有三个参数,实例化为void foo(const int &, const string &, const int &, const double &);
foo("hi");    //空包,实例化为void foo(const char[3] &);

获取包中参数个数:

template <typename ... Args> void g(Args ... args) {
    sizeof...(Args);    //模板参数包中参数的个数
    sizeof...(args);    //函数参数包中参数的数目
}

我们可以使用initializer_list来定义一个可接受可变数目实参的函数,但要求initializer_list中的所有实参类型必须与initializer_list的类型匹配。

打印包中所有参数:

template <typename T> ostream &print(ostream &os, const T &t) {    //第一个函数必须声明在第二个之前,否则会一直调用第二个
    return os << t;    //最后打印的元素后不加分隔符
}

tempalte <typename T, typename ... Args> ostream &print(ostream &os, cosnt T &t, const Args& ... rest) {
    os << t << ", ";
    return print(os, rest...);
} 

当我们传递2个以上参数给print时,会调用第二个版本,它将参数中除otstream外的第一个参数打印,并将剩下的参数包再次传入print,如参数包中元素数为两个以上时,重复以上过程,直到参数包中只有一个元素时,两个版本的print都是可行的,但第一个是更特例化的版本,因此选第一个。

包扩展:将包扩展为其构成的元素,通过在模式右边放一个省略号来触发扩展操作:

tempalte <typename T, typename ... Args> ostream &print(ostream &os, cosnt T &t, const Args& ... rest) {    //扩展Args
    os << t << ", ";
    return print(os, rest...);    //扩展rest
} 

对Args的扩展中,编译器将模式const Args&应用到模板参数包Args的每个元素,此模式的扩展结果是一个逗号分隔的零个或多个类型的列表,每个类型都形如const type &。

对rest的扩展中,扩展出一个由包中元素组成的、逗号分隔的列表。

扩展的模式也可以是函数调用:

print(os, debug_rep(rest)...);    //正确
print(os, debug_rep(rest...));    //错误,debug_rep只接受固定个数的参数

C++11中,我们可以转发模板参数包,实现将模板参数不变地传递给其他函数。

标准库容器的emplace_back成员是一个可变参数模板,它用其实参在容器管理的内存空间中直接构造一个元素。我们为StrVec类也设计一个emplace_back成员。

保持类型信息分两个阶段,首先为了保存实参的类型信息,必须将emplace_back的函数参数定义为模板参数的右值引用:

class StrVec {
public:
    //模板参数包扩展的模式是右值引用,每个参数都是指向其实参的右值引用
    template <typename ... Args> void emplace_back(Args &&...){
        chk_n_alloc();
        alloc.construct(first_free++, std::forward<Args>(args)...);
    } 
};

如这样使用:

svec.emplace_back(10, 'c');

则construct扩展为:

alloc.construct(first_free++, std::forward<int>(10), std::forward<char>('c'));

模板的通用版本有时可能对某些类型是不合适或不能用的,需要给它们定义一个函数模板的特例化版本。

重载的版本:

template <typename T> int compare(const T &, const T &);    //可以比较任意两个类型
template <size_t N, size_t M> int compare(const char (&)[N], const char(&)[M]);    //处理字符串字面常量,只有形参为字符串字面值常量或数组时,才使用它

const char *p1 = "hi", *p2 = "mom";
compare(p1, p2);    //调用第一个模板,因为我们无法将一个指针转换为数组的引用
compare("hi", "mom");    //调用第二个模板

而特例化一个函数模板时,必须为原模板中每个模板参数都提供实参,为指出我们在实例化一个模板,应使用template后跟一个空尖括号对<>,空尖括号对指出我们将为原模板的所有模板参数提供实参:

template <> int compare(const char * const &p1, const char *const &p2) {
    return strcmp(p1, p2);
}

以上的特例化必须与一个先前声明的模板中对应的类型匹配,我们特例化的是:

template <typename T> int compare(const T &, const T &);    //形参类型为const类型的引用

而我们特例化的版本形参应该是const char *,被特例化的版本形参是特定类型的const引用,而被特例化版本中的const是顶层const,而特例化版本的const是底层const,因此最终的形参为const char *const &。

特例化的本质是实例化了一个模板,而非重载,因此不影响函数匹配。

当我们同时定义了compare的重载版本和特例化版本时,对于形参为字符串字面值和字符数组的情况会选择重载版本(接受字符数组参数的版本)而忽视特例化版本,因为它更特例化。

为特例化一个模板,在定义它时,原模板的声明也必须在作用域中。并且在使用特例化版本前,特例化版本的声明也必须出现在作用域中。如忘记写特例化版本的声明,编译器会实例化原模板。因此,模板及其特例化版本的声明应该放在同一个头文件中,并且对于一组特例化了的函数模板,原模板的声明放前面,特例化版本的声明放后面。

类模板也能特例化,作为例子,我们将标准库hash模板定义一个特例化版本,可以用它将Sales_data对象保存在无序容器中,默认,无序容器使用hash<key_type>组织其元素。

我们必须在原模板所在的命名空间中特例化它:

namespace std {    //打开std命名空间,以便向命名空间中添加成员
    //中间任何定义都是std的一部分
    template <> struct hash<Sales_data> {
        //散列一个无序容器的类型必须要定义以下类型
        typedef size_t result_type;
        typedef Sales_data argument_type;    //此类型需要==
        size_t operator()(const Sales_data &s) const;
    };

    size_t hash<Sales_data>::operator()(const Sales_data &s) const {
        return hash<string>()(s.bookNo) ^ hash<unsigned>()(s.units_sold) ^ hash<double>()(s.revenue);
        }  
}    //关闭std命名空间

我们hash计算的三个数据成员的哈希值与Sales_data的==运算符是一样的,==运算符也是根据这三个成员判断的。默认,无序容器会使用key_type特例化的hash和key_type上的相等运算符来处理。

由于hash<Sales_data>使用了Sales_data的私有成员,因此要将hash<Sales_data>声明为Sales_data类的友元:

template <class T> class std::hash;    //友元声明所需要的,用于友元声明时,模板必须要先声明,而普通的类不用

class Sales_data {
    friend class std::hash<Sales_data>;    //仅特殊实例hash<Sales_data>是友元
};

为了让Sales_data的用户能使用hash的特例化版本,应在Sales_data的头文件中定义该实例化版本。

类模板能部分实例化,与模板函数的实例化不同,类模板不用为所有模板参数提供实参。一个类模板的部分特例化本身也是一个模板。

remove_reference通过一系列特例化版本完成其功能:

template <class T> struct remove_reference {
    typedef T type;
};

template <class T> struct remove_reference<T &> {    //用于左值引用
    typedef T type;
};

template <class T> struct remove_reference<T &&> {    //用于右值引用
    typedef T type;
};

int i = 0;
remove_reference<decltype(42)>::type a;    //实参为int,使用原始模板
remove_reference<decltype(i)>::type b;    //实参为int &,使用接受左值的版本
remove_reference<decltype(声太大::move(i))>::type c;    //实参为int &&,使用接受右值的版本

特例化的模板的模板参数与原始模板中的参数按位置对应。

可以只特例化模板类中的一个函数,而不特例化类:

template <typename T> struct Foo{
    Foo(const T &t = T()) : mem(t) { }
    void Bar() { /* ... */ }
    T mem;
};

template <> Foo<int>::Bar() { /* ... */ }

Foo<string> fs;
fs.Bar();    //使用Foo<string>::Bar()
Foo<int> fs;
fs.Bar();    //使用特例化的Foo<int>::Bar()
发布了221 篇原创文章 · 获赞 11 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/tus00000/article/details/105524811
今日推荐