“C++ Primer”


定义模板

函数模板

一个函数模板就是一个公式,用来生成针对特定类型的函数版本,如:

template <typename T>	// 类型参数必须使用关键字 class 或 typename
int compare(const T &v1, const T &v2)
{
    if (std::less<T>()(v1, v2)) return -1;
    if (std::less<T>()(v2, v1)) return 1;
    return 0;
}

非类型模板参数

除了可以定义类型参数,还是可以定义带有非类型参数(nontype parameters)的模板。非类型参数表示一个值而不是类型。非类型参数通过使用特定类型名字而不是 class 或 typename 来指定。当模板被实例化时,非类型参数将被一个用户提供的值或者编译器推断的值替代。这些值必须是常量表达式(constant expressions),只有这样编译器才能在编译期间实例化模板。如:

template <unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
    cout << N << std::endl;
    cout << M << std::endl;
    return strcmp(p1, p2);
}
compare("hi", "mom");  // 编译出 int compare(const char (&p1)[3], const char (&p2)[4]);

inline 和 constexpr 函数模板

inline 和 constexpr 放在模板参数列表之后,返回类型之前

template <typename T> inline T min(const T &, const T &);

编写与类型无关的代码

编写泛型代码的原则:

  1. 模板中的函数参数是const的引用
  2. 函数体中的条件判断仅使用<比较运算

模板编译

当编译器遇到一个模板定义时,它并不生成代码,只有实例化出模板的一个特定版本时,编译器才会生成代码,当我们使用(而不是定义)模板时,编译器才生成代码。

模板和头文件

通常,当我们调用一个函数时,编译器只需要掌握函数的声明,类似的,当我们是用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现,因此,我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。

模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义,因此与非模板代码不同,模板的头文件通常既包括声明也包括定义

类模板

类模板(class template)是合成类的蓝本。与函数模板不同的是编译器不能推断出类模板的模板参数的类型。相反,使用类模板时必须提供额外的信息,这些信息将放在类模板名后的尖括号中。这些额外信息是模板实参列表(template arguments list),用于替换模板参数列表(template parameters list)。

实例化类模板

类模板的每个实例都构成完全独立的类。类型 Blob<string> 和其它的Blob类型之间没有任何关系,亦没有任何特殊的访问权限。

类模板的成员函数

类模板的每个实例都有其自己版本的成员函数,因此,类模板的成员函数具有和模板相同的模板参数,因此定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表,如:

template <typename T>
void Blob<T>::check(size_type i, const std::string &msg) const
{
    if (i >= data->size())
        throw std::out_of_range(msg);
}
// 构造函数
template <typename T>
Blob<T>::Blob(): data(std::make_shared<std::vector<T>()) { }

默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化,如果一个成员函数没有被使用,则它不会被实例化。

类模板与友元

当类定义中包含友元声明时,类和友元可以相互不影响的时模板或者不是模板。类模板可以有非模板的友元,授权友元访问其所有的模板实例。如果友元自身是模板,授权友元的类控制访问权限是授给模板的所有实例还是给特定的实例。

一对一友元

// 为了指定模板(类或函数)的特定实例,我们必须首先声明模板本身。模板的声明包括模板的模板参数列表。
template <typename> class BlobPtr;
template <typename> class Blob;
template <typename T>
bool operator==(const Blob<T>&, const Blob<T>&);

template <typename T> class Blob {
    // 每个 Blob 实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符
    friend class BlobPtr<T>;	
    friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
};

模板的类型别名

类模板的一个实例就是一个类类型,与任何别的类类型一样,可以定义一个 typedef 来作为实例化类的别名。如:

typedef Blob<string> StrBlob;

由于模板不是类型,所以不能定义typedef作为模板的别名。也就是说不能定义 typedef 来指向 Blob<T>。然而,在新标准下可以用using声明来指定类模板的别名。如:

template <typename T> using twin = pair<T, T>;
twin<string> authors; //authors 是一个 pair<string, string>

类模板的 static 成员

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

上例中Foo的每个类实例都有自己的静态成员实例。也就是说对于每个给定类型X,都有一个 Foo<X>::ctr 和一个 Foo<X>::count 成员,而 Foo<X> 的所有对象都共享相同的ctr对象和count函数。

与任何别的 static数据成员一样,每个类实例必须只有一个static数据成员的定义,但是,每个类模板的实例有一个完全不一样的对象,因而,在定义静态数据成员时与在类外定义成员函数类似。如:

template <typename T>
size_t Foo<T>::ctr = 0;	// 定义并初始化 ctr

模板参数

与函数参数名字类似,模板参数名字没有本质的含义。通常将类型参数记作 T,但可以使用任何名字:

template <typename Foo>
Foo calc(const Foo &a, const Foo &b)
{
    Foo temp = a;
    return temp;
}

默认模板实参

与可以给函数参数提供默认实参一样,可以提供默认模板实参(default template arguments),在新标准下可以给函数和类模板提供默认实参。早期的语言版本只允许给类模板提供默认实参。如:

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

无论何时使用类模板,都必须在模板名之后跟随尖括号。尖括号表示类是从一个模板实例化而来的。特别是,如果一个类模板给所有模板参数都提供了默认实参,并且我们也希望使用这些默认值,还是必须得在模板名字后提供一个空的尖括号对。如:

template <class T = int>
class Numbers {
public:
    Numbers(T v = 0):val(v) { }
private:
    T val;
};
Numbers<long double> lots_of_precision;
Numbers<> average_precision; //空<> 表示我们希望使用默认类型

成员模板

一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数,这种成员被称为成员模板(member template),成员模板不能是虚函数。

普通类(非模板)的成员模板

如:

class DebugDelete { // 函数对象类,对给定指针执行 delete
public:
    DebugDelete(std::ostream &s = std::cerr):os(s) { }
    template <typename T>
    void operator()(T *p) const
    {
        os << "delete unique_ptr" << std::endl;
        delete p;
    }
private:
    std::ostream &os;
};
double *p = new double;
DebugDelete d;
d(p);	// 调用 DebugDelete::operator()(double *) 释放 p
int *ip = new int;
DebugDelete()(ip);	// 在一个临时 DebugDelete 对象上调用 operator()(int*)

类模板的成员模板

对于类模板,我们也可以为其定义成员模板,在此情况下,类和成员各自有各自的独立的模板参数

当在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表,类模板的参数列表在前,后跟成员自己的模板参数列表:

template <typename T>	// 类的类型参数
template <typename It>	// 构造函数的类型参数
Blob<T>::Blob(It b, It e):data(std::make_shared<std::vector<T>>(b, e)) { }

控制实例化

当模板被使用时才会进行实例化意味着相同的实例可能出现在多个对象文件中,当练个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。

在大的系统中,在多个文件中实例化同一个模板的额外开销可能非常严重。在新标准中,可以通过显式实例化(explicit instantiation)来避免这个消耗。显式实例化的形式如下:

extern template declaration; //实例化声明
template declaration; //实例化定义

declaration 是一个类或函数声明,其中所有模板参数已被替换为模板参数,如:

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

当编译器看到extern模板声明时,它将不会生成在当前文件中生成实例代码。extern声明意味着在程序的某个地方存在着一个非extern的实例,程序中可以有多个extern声明,但是只能有一个定义

由于当使用模板时会自动实例化,所以 extern 声明必须出现在所有使用此实例的代码之前。

实例化定义会实例化所有成员

类模板的实例定义实例化模板的所有成员,包括内联成员函数。当编译器看到一个实例定义时,它无法知道到底哪个成员函数将会被程序使用,因此,与常规的类模板实例化不同的是,编译器将实例化类的所有成员。即便不使用某个成员,其也必须实例化。结果就是,我们只能显式实例化所有成员都可以使用的模板实例

模板实参推断

默认情况下编译器使用调用实参来决定函数模板的模板参数。这个过程称为模板实参推断(template argument deduction),在推断过程中编译器使用调用的实参类型来选择哪个生成的函数版本是最合适的。

类型转换与模板类型参数

与常规函数一样,传递给函数的模板的实参被用于初始化函数的参数。类型是模板类型参数的函数参数有特殊的初始化规则。只有非常有限的几个转换是自动运用于这种实参的。相比于转换实参,编译器会生成一个新的实例。

与之前的描述一样,不论是参数还是实参中的顶层const都会被忽略。在函数模板中会执行的有限转换分别是:

template <typename T> T fobj(T, T);
template <typename T> T fref(const T &, const T &);
int a[10], b[42];
fobj(a, b);	// 调用 fobj(int*, int*)
fref(a, b);	// 错误,数组类型不匹配

函数模板显式实参

在一些情形下根本不可能让编译器推断出模板的实参。在另外一些情形下,则是我们自己想控制模板的实例化。两者绝大多数时候发生在函数的返回类型与任何参数列表中的类型都不一样时。

指定显示模板参数

显式模板实参将被放在函数名之后的尖括号中,并且在实参列表之前。如:

template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);	// 编译器无法推断 T1,它未出现在函数参数列表中
// T1 是显式指定的,T2 和 T3 是从函数实参类型推断而来的
auto val3 = sum<long, long>(i, lng);	// long long sum(int, long)

尾置返回类型与类型转换

// 尾置返回类型允许我们在参数列表之后声明返回类型
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
	return *beg; // 返回序列中一个元素的引用
}

函数指针和实参推断

当使用函数模板来初始化或赋值函数指针时,编译器使用指针的类型来推断模板的实参。如:

template <typename T>
int compare(const T &, const T &);
// pf1 指向实例 int compare(const int&, const int&)
int (*pf1)(const int &, const int &) = compare;

模板实参推断和引用

如果函数的参数是模板类型的引用,需要记住的是:常见的引用绑定规则依然有效(左值只能绑定到左值,右值只能绑定到右值);并且此时const是底层const而不是顶层const

从左值引用函数参数推断类型

当一个函数参数是模板类型参数的左值引用如:T&,绑定规则告诉我们只能传递左值过去,实参可以有const修饰,如果实参是const的,那么 T 将被推断为const类型。如:

template <typename T> void f1(T&);
f1(i); // i is an int; T is int
f1(ci); // ci is a const int; T is const int
f1(5); // 错误,传递给一个 & 

如果一个函数参数是 const T& 的,那么绑定规则告诉我们可以传递任何类型(const 或非 const 对象、临时量或字面量)的实参过去。由于函数参数本身是 const 的,T 推断出来的类型将不在是 const 的了,因为,const 已经是函数参数类型的一部分;因而,它将不必在是模板参数的一部分。如:

template <typename T> void f2(const T&); // 可以接受一个右值
// f2 中的参数是 const &,实参中的 const 是无关的
// 在每个调用中,f2 的函数参数都被推断为 const int&
f2(i);	// i 是一个 int,模板参数 T 是 int
f2(ci);  // ci 是一个 const int,模板参数 T 是 int
f2(5);  // 一个 const & 参数可以绑定到一个右值,T 是 int

从右值引用函数参数推断类型

template <typename T> void f3(T&&);
f3(42); // 实参是一个 int 类型的右值,模板参数 T 是 int

理解std::move

标准库定义 move 函数如下:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    return static_cast<remove_reference<T>::type&&>(t);
}
string s1("hi!"), s2;
s2 = std::move(string("bye!")); // 正确,从一个右值移动数据
s2 = std::move(s1); // 正确,但在赋值之后,s1 的值是不确定的

转发

一些函数需要其一个或多个实参以类型保持完全不变的方式转发(forwarding)给另外一个函数。在这种情况下,我们需要保存转发实参的所有信息,包括实参是否为 const 或者实参是左值还是右值。如以下函数:

// 接受一个可调用对象和另外两个参数的模板
// 对翻转的参数调用给定的可调用对象
// flip1 是一个不完整的实现:顶层 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) // v2 是一个引用
{
    cout << v1 << " " << ++v2 << endl;
}
f(42, i);	// f 改变了 i
flip1(f, j, 42); // 通过 flip1 调用 f 不会改变 j

如果以此函数去调用 flip1 函数,那么 f 将不能改变原始参数,改变的将是被复制的参数。为了达到转发的目的非得将 flip1 的参数改成右值引用形式,只有这样才能保持参数的类型信息。通过将其参数定义为模板类型参数的右值引用(T&&)来保持实参的整个类型信息(引用以及 const 性质)。通过将参数定义为引用可以保持参数的 const 性质,这是因为 const 在引用类型中是底层的。通过引用折叠,如果将函数参数定义为 T1&& 或 T2 &&,将可以保留 flip1 函数实参的左值/右值属性。修改代码如下:

template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
    f(t2, t1);
}
// 这个版本的 flip2 只解决了问题的一半,flip2 函数可以调用以左值引用为参数的函数,
// 但是对以右值引用为参数的函数就无能为力了。如:
void g(int &&i, int &j)
{
    cout << i << " " << j << endl;
}
flip2(g, i, 42); // 错误,不能从一个左值实例化 int&&
// 原因在于任何函数参数与变量都是左值,即便其初始值是右值。
// 因而,在 flip2 中调用 g 将传递左值给 g 的右值引用参数。

如果一个函数参数是指向模板类型参数的右值引用(如 T&&),它对应的实参的 const 属性和左值/右值属性将得到保持。

在调用中使用 std::forward 保持类型信息(c++11)

forward 可以保持给定实参的左值/右值属性

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

重载与模板

函数模板可以被别的模板或非模板函数重载。与往常一样,同名的函数必须在参数的数目或类型上有所差异。

如果涉及函数模板,函数匹配规则会在以下几个方面受到影响

template <typename T> string debug_rep(const T &t)
{
    ostringstream ret;
    ret << t;
    return ret.str();	// 返回 ret 绑定的 string 的一个副本
}

template <typename T> string debug_rep(T *p)
{
    ostringstream ret;
    ret << "pointer: " << p;	// 打印指针本身的值
    if (p)
        ret << " " << debug_rep(*p);	// 打印 p 指向的值
    else
        ret << " null pointer";	// 返回 ret 绑定的 string 的一个副本
}
string s("hi"); 
cout << debug_rep(s) << endl;	// 调用第一个版本的 debug_rep
cout << debug_rep(&s) << endl;	// 进行调用则两个模板函数都是可行的实例。第一个是 debug_rep(const string* &) 其中 T 被推断为 string*;第二个是 debug_rep(string*); 其 T 被推断为 string ;第二个是精确匹配,因此选第二个

多个可行模板

const string *sp = &s;
cout << debug_rep(sp) << endl;
//第一个将被实例化为 debug_rep(const string* &),其 T 将绑定到 string*;
//第二个将被实例化为 debug_rep(const string*) 其 T 将绑定到 const string 上。
//此时常规的函数匹配将无法区别哪一个调用是更优的。然而,由于新加的关于模板的规则,将调用 debug_rep(T*) 函数,这个函数是更加特化的模板。

非模板和模板重载

string debug_rep(const string &s)
{
    return '"' + s + '"';
}
string s("hi");
cout << debug_rep(s) << endl;
// 第一个模板:debug_rep<string>(const string&) 其 T 绑定到 string
// 此处的非模板函数,编译器将选择非模板版本

可变参数模板(C++11)

可变参数模板(variadic template)是新加的一种函数模板或类模板,这种模板可以接收可变数量的模板参数。这种可变参数被称为参数包(parameter pack)。语言允许两种参数包:模板参数包(template parameter pack)表示 0 或多个模板参数,以及函数参数包(function parameter pack)表示 0 个或多个函数参数。

语言使用省略号(ellipsis)来表示模板或函数参数包。对于模板参数列表,class...typename... 表示接下的参数表示一系列 0 个或多个类型;省略号后的名字表示其参数包的任何类型的名字。在函数参数列表中,如果一个参数的类型是一个模板参数包,那么它就是一个函数参数包。如:

// Args is a template parameter pack; rest is a function parameter pack
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);
int i = 0; double d = 3.14; string s= "how now brown cow";
foo(i, s, 42, d); // 包中有三个参数
foo(s, 42, "hi"); // 包中有两个参数
foo(d, s); // 包中有1个参数
foo("hi"); // 空包
// 编译器会为 foo 实例化出四个不同的版本:
void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);

sizeof… 操作符

当希望知道参数包中有多少个元素时,可以使用 sizeof... 操作符,与 sizeof 一样,其返回一个常量表达式并且不会对其实参进行求值:

template <typename... Args> void g(Args... args) {
    cout << sizeof...(Args) << endl;	// 类型参数的数目
    cout << sizeof...(args) << endl;	// 函数参数的数目
}

包扩展

除了获取包的大小,另外一件可以做的事是对参数进行展开。当展开包时,可以提供一个模式(pattern)给它使用在每个展开的元素上。展开一个包将使得所有元素称为连续的逗号分隔的列表,并且将模式运用于每个元素上。使用省略号来进行包扩展。如 print 函数就有两个扩展。

// 用来终止递归并打印最后一个元素的函数,必须在可变参数版本的 print 定义之前声明
template <typename T>
ostream &print(ostream &os, const T &t)
{
    return os << t;
}

// 包中除了最后一个元素外的其他元素都会调用这个版本的 print
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest) // 扩展 Args
{
    os << t << ", ";
    return print(os, rest...); // 扩展 rest
}

模板特例化

一个特例化版本就是模板的一个独立的定义,在其中一个或多个模板参数被指定为特定的类型

定义函数模板特例

当定义函数模板特例时,需要给原模板中所有的模板参数提供实参。为了表示我们的确是在特例化一个模板,需要使用关键字 tempalte 后跟随一个空的尖括号 <> ,空的尖括号表示给原模板中的所有模板参数都提供了实参。如:

template <>	// compare 的特殊版本,处理字符数组的指针
int comapre(const char* const &p1, const char* const &p2)  // (3)
{
    return strcmp(p1, p2);
}

函数重载 vs 模板特例化

当定义函数模板特例时,我们是在抢编译器的工作。就是说我们给原始模板的特定实例提供我们自己的定义。这里特别需要留意的是特例是一个实例;它不是重载;由于特例实例化一个模板;它不重载这个模板,因而,特例不会影响函数匹配过程。

最佳实践:模板和它的特例应该定义在同一个头文件中,而且同一个名字的所有模板都应该出现在前面,后面跟随这些模板的特例。