目录

第16章 模板和泛型编程

目录

面向对象编程和泛型编程都是处理程序编写时类型未知的情况。两者的区别是面向对象编程直到运行时类型才能知道,而泛型编程类型在编译期间知道。

容器,迭代器和算法都是泛型编程的例子。当编写一个泛型程序,我们用一种独立于任何类型的方式编写代码。当使用一个泛型程序,我们提供程序的实例将操作的类型或值。

模板是C++中泛型编程的基础。模板是一个创建类或函数的公式或蓝图。当我们使用一个泛型类型,我们提供将蓝图转换为指定类或函数所需的信息。这个转换发生在编译期间。

16.1 定义一个模板

想象我们要写一个函数比较两个值的大小。实际中,我们要定义多个这样的函数,每一个比较指定类型的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// returns 0 if the values are equal, -1 if v1 is smaller, 1 if v2 is smaller
int compare(const string &v1, const string &v2)
{
    if (v1 < v2) return -1;
    if (v2 < v1) return 1;
    return 0;
}
int compare(const double &v1, const double &v2)
{
    if (v1 < v2) return -1;
    if (v2 < v1) return 1;
    return 0;
}

这样的函数几乎相同:唯一不同是参数的类型不同,每一个函数体都相同。

16.1.1 函数模板

我们定义一个函数模板而不是为每一种类型定义一个新函数。一个函数模板是一个公式,从这个公式我们能够生成指定类型的函数。

1
2
3
4
5
6
7
template <typename T>
int compare(const T &v1, const T &v2)
{
    if (v1 < v2) return -1;
    if (v2 < v1) return 1;
    return 0;
}

模板定义开始于关键字template后面跟着模板参数列表。模板参数列表由<>括起来,逗号分隔的一个或多个模板参数组成。

注释 在模板定义中,模板参数列表不能为空。

模板参数列表的行为非常像函数的参数列表。模板参数表示用在类或函数定义中的类型或值。当我们使用一个模板,我们指定一个(隐式或显式)模板实参绑定到模板参数。

实例化函数模板

当我们调用一个函数模板,编译器使用函数调用的实参为我们推导模板实参。

1
cout << compare(1, 0) << endl;       // T is int

编译器使用推导的模板参数来为我们实例化指定版本的函数。当编译器实例化一个模板,它使用实际的模板实参创建一个新的模板实例代替相应的模板参数。

1
2
3
4
5
// instantiates int compare(const int&, const int&)
cout << compare(1, 0) << endl;       // T is int
// instantiates int compare(const vector<int>&, const vector<int>&)
vector<int> vec1{1, 2, 3}, vec2{4, 5, 6};
cout << compare(vec1, vec2) << endl; // T is vector<int>

模板类型参数

一般而言,我们可以和使用内置类型或类类型一样的方式使用类型参数作为类型说明符。特别地,类型参数可以用来命名一个返回类型,函数参数类型,变量声明或函数内类型转换。

1
2
3
4
5
6
7
// ok: same type used for the return type and parameter
template <typename T> T foo(T* p)
{
    T tmp = *p; // tmp will have the type to which p points
    // ...
    return tmp;
}

每一个类型参数之前必须有classtypename关键字。

1
2
// error: must precede U with either typename or class
template <typename T, U> T calc(const T&, const U&);

使用关键字typename指定模板类型参数比class似乎更直观。毕竟我们可以使用内置(非类类型)类型作为模板的类型实参。而且typename更加清晰地指示了跟在它后面的名字是一个类型名。但是,typename是在模板广泛使用之后才被加入C++的。

非类型模板参数

除了定义类型参数,我们还能定义非类型的模板参数。一个非类型参数代表一个值而不是类型。非类型参数由一个指定的类型名指定而不是classtypename关键字。

当模板实例化时,非类型参数被用户提供的或编译器推导的一个值替换。这些值必须是常量表达式,以允许编译器在编译期间实例化模板。

1
2
3
4
5
6
7
8
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
    return strcmp(p1, p2);
}

compare("hi", "mom")
int compare(const char (&p1)[3], const char (&p2)[4])

一个非类型参数可能是整型,对象或函数的指针或(左值)引用。绑定到非类型的整型参数的实参必须是常量表达式。绑定到指针或引用的非类型参数必须拥有静态生命周期。不能使用普通(非静态)局部对象或动态分配的对象作为模板实参传给指针或引用的非类型模板参数。指针参数也能用nullptr或0值常量表达式初始化。

注释 用于非类型模板参数的模板实参必须是常量表达式。

inline和constexpr函数模板

和非模板函数一样,函数模板可以被声明为inlineconstexprinlineconstexpr限定符跟在模板参数列表之后,函数返回类型之前。

1
2
3
4
// ok: inline specifier follows the template parameter list
template <typename T> inline T min(const T&, const T&);
// error: incorrect placement of the inline specifier
inline template <typename T> T min(const T&, const T&);

编写类型独立的代码

尽管简单,compare函数展示了编写泛型代码的两个重要原则:

  • 模板中的函数参数是const引用
  • 函数体中的比较只使用<

只使用<运算符,我们降低了对使用compare函数的类型的要求。这些类型只需要支持<,没有必要支持>。实际上,如果我们真正关心类型独立和可移植性,我们可能应该使用less定义函数。

1
2
3
4
5
6
7
8
// version of compare that will be correct even if used on pointers; see § 14.8.2 (p.
575)
template <typename T> int compare(const T &v1, const T &v2)
{
    if (less<T>()(v1, v2)) return -1;
    if (less<T>()(v2, v1)) return 1;
    return 0;
}

最佳实践 模板程序应该尽量减少作用在参数类型的要求的数量。

模板编译

当编译器看到模板的定义,并没有生成代码。只有当实例化一个指定的模板实例的时候生成代码。只有使用模板时生成代码的事实影响我们如何组织源代码和如何检测错误。

通常当我们调用一个函数,编译器只需要看到函数的声明。类似地,当我们使用类对象,类定义必须可用,但是成员函数的定义不需要提供。因此,我们把类定义和函数声明放在头文件而普通函数和成员函数放在源文件。

模板不一样:为了生成一个模板实例,编译器需要定义函数模板或类模板成员函数的代码。因此不像非模板代码,模板头文件既包含了定义也包含了声明。

注释 函数模板和类模板成员函数的定义通常放进头文件。

关键概念:模板和头文件 模板包含两种名字:

  • 那些不依赖模板参数的名字
  • 那些依赖模板参数的名字

由模板的提供者保证当模板使用时所有不依赖模板参数的名字可见。而且,模板的提供者必须保证当模板实例化的时候模板的定义,包括类模板成员的定义可见。由模板的用户保证用来实例化模板所有的函数,类型和类型相关的操作符可见。 模板的作者应该提供一个头文件,包含模板定义,连同所有类模板或成员定义中使用的名字的声明。模板用户必须包含模板头文件和任意用来实例化模板的类型。

实例化期间最常报的编译错误

第一个阶段是编译模板自己的时候。编译器在这个阶段通常不能找到很多错误。编译器能查找出语法错误,比如忘记标点或变量拼写错误。

第二个阶段是编译器看到模板的使用的时候。这个阶段也没有很多需要检查。对于一个函数模板调用,编译器只是检查参数的个数是否合适。也可以检查两个类型应该一样的参数是否真的相同。对于类模板,编译器能检查正确的模板参数个数。

第三个阶段是实例化的时候。只有这个时候类型相关的错误才能被发现。取决于编译器如何处理实例化,这些错误可能会在链接时报告。

警告 由调用者保证传递给模板的参数支持模板使用到的任意操作,且这些操作在模板使用的上下文环境表现正确。

16.1.2 类模板

类模板是产生类的蓝图。和函数模板不同,编译器不能为类模板推导模板参数。因此为了使用类模板,我们必须在模板名字后面的尖括号里面提供额外的信息。这些信息就是用来代替模板参数的模板实参列表。

定义一个类模板

和函数模板一样,类模板以template关键字开头,后跟一个模板参数列表。在类模板(和它的成员)的定义中,我们使用模板参数作为当模板使用时提供的类型或值的替身。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template <typename T> class Blob {
public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_type;
    // constructors
    Blob();
    Blob(std::initializer_list<T> il);
    // number of elements in the Blob
    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
    // add and remove elements
    void push_back(const T &t) {data->push_back(t);}
    // move version; see § 13.6.3 (p. 548)
    void push_back(T &&t) { data->push_back(std::move(t)); }
    void pop_back();
    // element access
    T& back();
    T& operator[](size_type i); // defined in § 14.5 (p. 566)
private:
    std::shared_ptr<std::vector<T>> data;
    // throws msg if data[i] isn't valid
    void check(size_type i, const std::string &msg) const;
};

实例化一个类模板

当我们使用一个类模板时,必须提供额外的信息。这些额外的信息是显式模板实参列表。编译器使用这些模板实参来实例化一个指定类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Blob<int> ia;                // empty Blob<int>
Blob<int> ia2 = {0,1,2,3,4}; // Blob<int> with five elements

template <> class Blob<int> {
    typedef typename std::vector<int>::size_type size_type;
    Blob();
    Blob(std::initializer_list<int> il);
    // ...
    int& operator[](size_type i);
private:
    std::shared_ptr<std::vector<int>> data;
    void check(size_type i, const std::string &msg) const;
};

当编译器从Blob模板实例化一个类时,它重写了Blob模板,用int替换掉每一个模板参数T。编译器为每一个我们指定的类型生成一个不同的类。

1
2
3
// these definitions instantiate two distinct Blob types
Blob<string> names; // Blob that holds strings
Blob<double> prices;// different element type

注释 每一个类模板的实例化构成了一个独立的类。类型Blob和任何其它的Blob类型没有关系,也没有任何特殊权限访问其它Blob类型的成员。

模板作用域中模板类型的引用

类模板用来实例化一个类型,且被实例化的类型总是包含模板实参。类模板中的代码通常不使用实际类型(或值)的名字作为模板实参。相反,我们经常使用模板自己的参数作为模板实参。

1
2
std::shared_ptr<std::vector<T>> data;
shared_ptr<vector<string>>

类模板成员函数

和任意其它类一样,我们能够在类里面或外面定义类模板的成员函数。定义在类里面的成员函数默认为inline。类模板成员函数本身是个普通函数。但是类模板每一个实例都拥有自己版本的成员。因此类模板的成员函数拥有和类模板相同的模板参数。所以定义在类模板外面的成员函数以关键字template开头,后接类模板参数列表。

1
2
template <typename T>
ret-type Blob<T>::member-name(parm-list)

check和元素访问成员

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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>
T& Blob<T>::back()
{
    check(0, "back on empty Blob");
    return data->back();
}

template <typename T>
T& Blob<T>::operator[](size_type i)
{
    // if i is too big, check will throw, preventing access to a nonexistent element
    check(i, "subscript out of range");
    return (*data)[i];
}

template <typename T> 
void Blob<T>::pop_back()
{
    check(0, "pop_back on empty Blob");
    data->pop_back();
}

Blob构造函数

和其它定义在类模板外面的成员函数一样,构造函数以声明模板参数开始。

1
2
3
4
5
6
template <typename T>
Blob<T>::Blob(): data(std::make_shared<std::vector<T>>()) { }

template <typename T>
Blob<T>::Blob(std::initializer_list<T> il):
              data(std::make_shared<std::vector<T>>(il)) { }

类模板成员函数实例化

默认地,类模板的成员函数只有在程序使用这个函数时实例化。

1
2
3
4
5
// instantiates Blob<int> and the initializer_list<int> constructor
Blob<int> squares = {0,1,2,3,4,5,6,7,8,9};
// instantiates Blob<int>::size() const
for (size_t i = 0; i != squares.size(); ++i)
    squares[i] = i*i; // instantiates Blob<int>::operator[](size_t)

成员只有在使用时实例化这个事实使我们可以用一个可能不满足某些模板操作要求的类型实例化一个类。

注释 默认地,一个实例化的类模板的成员只有当成员使用时才实例化。

类代码里面简化模板类名的使用

使用一个类模板类型必须提供一个模板实参有一个例外,在类模板作用域里面,我们可以不带模板实参使用模板名字。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// BlobPtr throws an exception on attempts to access a nonexistent element
template <typename T> class BlobPtr {
public:
    BlobPtr(): curr(0) { }
    BlobPtr(Blob<T> &a, size_t sz = 0):
            wptr(a.data), curr(sz) { }
    T& operator*() const
    { auto p = check(curr, "dereference past end");
      return (*p)[curr];  // (*p) is the vector to which this object points
    }
    // increment and decrement
    BlobPtr& operator++();        // prefix operators
    BlobPtr& operator--();
private:
    // check returns a shared_ptr to the vector if the check succeeds
    std::shared_ptr<std::vector<T>>
        check(std::size_t, const std::string&) const;
    // store a weak_ptr, which means the underlying vector might be destroyed
    std::weak_ptr<std::vector<T>> wptr;
    std::size_t curr;      // current position within the array
};

在类模板外面使用类模板名字

当我们在类模板外面定义成员时,必须记住我们不在类的作用域直到类名被看见。

1
2
3
4
5
6
7
8
9
// postfix: increment/decrement the object but return the unchanged value
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
    // no check needed here; the call to prefix increment will do the check
    BlobPtr ret = *this;  // save the current value
    ++*this;    // advance one element; prefix ++ checks the increment
    return ret;  // return the saved state
}

当我们没有提供模板实参,编译器假设我们使用成员实例化一样的类型。因此,ret的定义就好像:

1
BlobPtr<T> ret = *this;

注释 在类模板作用域里面,我们可以使用模板,而不指定模板实参。

类模板和友元

当一个类包含友元声明时,类和友元各自可以是模板或非模板。一个有非模板的友元的类模板授予其友元访问模板所有实例。当友元本身也是模板时,授予友元关系的类控制友元模板所有实例或指定实例友元关系。

一对一友元关系

从一个类模板到另一个模板(类模板或函数模板)建立相应实例之间的友元关系是最常见友元关系形式。为了引用一个指定的模板实例(类模板或函数模板),我们必须先声明模板。一个模板声明包含模板参数列表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// forward declarations needed for friend declarations in Blob
template <typename> class BlobPtr;
template <typename> class Blob; // needed for parameters in operator==
template <typename T>
    bool operator==(const Blob<T>&, const Blob<T>&);
template <typename T> class Blob {
    // each instantiation of Blob grants access to the version of
    // BlobPtr and the equality operator instantiated with the same type
    friend class BlobPtr<T>;
    friend bool operator==<T>
           (const Blob<T>&, const Blob<T>&);
    // other members as in § 12.1.1 (p. 456)
};

Blob<char> ca; // BlobPtr<char> and operator==<char> are friends
Blob<int> ia;  // BlobPtr<int> and operator==<int> are friends

通用和指定模板友元

一个类也可以指定另一个模板的每一个实例为友元,或限制指定实例为友元:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// forward declaration necessary to befriend a specific instantiation of a template
template <typename T> class Pal;
class C {  //  C is an ordinary, nontemplate class
    friend class Pal<C>;  // Pal instantiated with class C is a friend to C
    // all instances of Pal2 are friends to C;
    // no forward declaration required when we befriend all instantiations
    template <typename T> friend class Pal2;
};
template <typename T> class C2 { // C2 is itself a class template
    // each instantiation of C2 has the same instance of Pal as a friend
    friend class Pal<T>;  // a template declaration for Pal must be in scope
    // all instances of Pal2 are friends of each instance of C2, prior declaration needed
    template <typename X> friend class Pal2;
    // Pal3 is a nontemplate class that is a friend of every instance of C2
    friend class Pal3;    // prior declaration for Pal3 not needed
};

为了允许所有的实例都是友元,友元声明必须使用跟类不一样的模板参数。

模板类型参数作为友元

在新标准下,我们可以将模板类型参数作为友元:

1
2
3
4
template <typename Type> class Bar {
friend Type; // grants access to the type used to instantiate Bar
    //  ...
};

值得注意的是尽管友元通常必须是一个类或函数,用内置类型实例化类Bar也可以,因此友元为内置类型在这种情况也允许。

模板类型别名

类模板的一个实例定义了一个类类型,和其它类类型一样,我们可以使用typedef引用它:

1
typedef Blob<string> StrBlob;

因为模板不是一个类型,我们不能使用typedef引用一个模板,即没有办法用typedef引用Blob。但是新标准允许我们为类模板定义一个类型别名:

1
2
template<typename T> using twin = pair<T, T>;
twin<string> authors; // authors is a pair<string, string>

模板类型别名是一个类家庭的同义词:

1
2
twin<int> win_loss;  // win_loss is a pair<int, int>
twin<double> area;   // area is a pair<double, double>

当我们定义一个模板类型别名,我们可以固定一个或多个模板参数:

1
2
3
4
template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books;  // books is a pair<string, unsigned>
partNo<Vehicle> cars;  // cars is a pair<Vehicle, unsigned>
partNo<Student> kids;  // kids is a pair<Student, unsigned>

类模板的静态成员

和其它类一样,类模板可以声明静态成员:

1
2
3
4
5
6
7
8
template <typename T> class Foo {
public:
   static std::size_t count() { return ctr; }
   // other interface members
private:
   static std::size_t ctr;
   // other implementation members
};

每一个Foo的实例都有自己静态成员。

1
2
3
4
// instantiates static members Foo<string>::ctr and Foo<string>::count
Foo<string> fs;
// all three objects share the same Foo<int>::ctr and Foo<int>::count members
Foo<int> fi, fi2, fi3;

和其它静态数据成员一样,类模板的每一个静态数据成员必须只有一个定义。

1
2
template <typename T>
size_t Foo<T>::ctr = 0; // define and initialize ctr

必须引用指定的实例来访问静态成员。

1
2
3
4
5
Foo<int> fi;                 // instantiates Foo<int> class
                             // and the static data member ctr
auto ct = Foo<int>::count(); // instantiates Foo<int>::count
ct = fi.count();             // uses Foo<int>::count
ct = Foo::count();           // error: which template instantiation?

和其它成员函数一样,静态成员函数只有在程序中使用时才实例化。

16.1.3 模板参数

和函数参数的名字一样,模板参数的名字没有实质意义。我们通常命名类型参数为T,也可以使用人用名字:

1
2
3
4
5
6
template <typename Foo> Foo calc(const Foo& a, const Foo& b)
{
    Foo tmp = a; // tmp has the same type as the parameters and return type
    // ...
    return tmp;  // return type and parameters have the same type
}

模板参数和作用域

模板参数遵循正常的作用域规则。模板参数的名字在声明之后直到模板声明或定义结束之前都能使用。和其它名字一样,模板参数名隐藏任何外围声明的名字。但是一个被用来作为模板参数的名字在模板内不能被重复使用。

1
2
3
4
5
6
typedef double A;
template <typename A, typename B> void f(A a, B b)
{
    A tmp = a; // tmp has same type as the template parameter A, not double
    double B;  // error: redeclares template parameter B
}

因为模板参数名不能重复使用,模板参数列表中的每一个模板参数名只能出现一次:

1
2
// error: illegal reuse of template parameter name V
template <typename V, typename V> // ...

模板声明

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

1
2
3
// declares but does not define compare and Blob
template <typename T> int compare(const T&, const T&);
template <typename T> class Blob;

和函数参数一样,模板参数名不需要和定义中的模板参数名一样:

1
2
3
4
5
6
// all three uses of calc refer to the same function template
template <typename T> T calc(const T&, const T&); // declaration
template <typename U> U calc(const U&, const U&); // declaration
// definition of the template
template <typename Type>
Type calc(const Type& a, const Type& b) { /* . . . */ }

当然,模板的每一个声明和定义参数的数量和种类(类型或非类型)必须一样。

最佳实践 一个文件需要的所有模板的声明通常应该一起出现在文件开头,在任何代码使用这些名字之前。

使用为类型的类成员

我们可以使用作用域操作符(::)访问static成员和类型成员。假设T是一个模板类型参数,当编译器看到T::mem时,在实例化前它不知道mem是一个类型还是静态数据成员。但是为了处理模板,编译器必须知道一个名字是否代表类型。比如下面这个例子:

1
T::size_type * p;

编译器需要知道这是定义一个变量p还是静态数据成员与p相乘。语言默认假设通过作用域操作符访问的名字不是一个类型。因此如果我们想要使用模板类型参数的类型成员,我们必须显式告诉编译器这个名字是一个类型。我们使用关键字typename来做这件事:

1
2
3
4
5
6
7
8
template <typename T>
typename T::value_type top(const T& c)
{
    if (!c.empty())
        return c.back();
    else
        return typename T::value_type();
}

注释 当我们通知编译器一个名字代表类型时,必须使用关键字typename,而不是class

默认模板实参

就像我们能够给函数参数提供默认实参,我们也能提供默认模板实参。新标准下,我们能给函数模板和类模板提供默认实参。早先的版本只允许类模板有默认实参。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// compare has a default template argument, less<T>
// and a default function argument, F()
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;
}

bool i = compare(0, 42); // uses less; i is -1
// result depends on the isbns in item1 and item2
Sales_data item1(cin), item2(cin);
bool j = compare(item1, item2, compareIsbn);

模板默认实参和类模板

不管什么时候使用类模板,我们必须在模板名后面跟尖括号。尖括号指示类必须从模板实例化。特别地,如果一个类模板为所有模板参数提供了默认实参,且我们使用这些默认值,我们必须在模板名后面放一对空的尖括号:

1
2
3
4
5
6
7
8
9
template <class T = int> class Numbers {   // by default T is int
public:
    Numbers(T v = 0): val(v) { }
    // various operations on numbers
private:
    T val;
};
Numbers<long double> lots_of_precision;
Numbers<> average_precision; // empty <> says we want the default type

16.1.4 成员模板

一个类(不管是普通类还是类模板)可能有本身是模板的成员函数。这种成员被称为成员模板。成员模板不能是虚函数。

普通类的成员模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// function-object class that calls delete on a given pointer
class DebugDelete {
public:
    DebugDelete(std::ostream &s = std::cerr): os(s) { }
    // as with any function template, the type of T is deduced by the compiler
    template <typename T> void operator()(T *p) const
      { os << "deleting unique_ptr" << std::endl; delete p;
}
private:
    std::ostream &os;
};

和其他模板一样,一个成员模板以它自己的模板参数列表开始。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
double* p = new double;
DebugDelete d;    // an object that can act like a delete expression
d(p); // calls DebugDelete::operator()(double*), which deletes p
int* ip = new int;
// calls operator()(int*) on a temporary DebugDelete object
DebugDelete()(ip);

// destroying the the object to which p points
// instantiates DebugDelete::operator()<int>(int *)
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
// destroying the the object to which sp points
// instantiates DebugDelete::operator()<string>(string*)
unique_ptr<string,DebugDelete> sp(new string, DebugDelete());

unique_ptr的析构函数会调用DebugDelete的调用操作符。因此,当unique_ptr析构函数实例化时,DebugDelete的调用操作符也会实例化。

1
2
3
// sample instantiations for member templates of DebugDelete
void DebugDelete::operator()(int *p) const { delete p; }
void DebugDelete::operator()(string *p) const { delete p; }

类模板的成员模板

我们也可以定义一个类模板的成员模板。这种情况下,类和成员都有它们自己的独立的模板参数。

1
2
3
4
template <typename T> class Blob {
    template <typename It> Blob(It b, It e);
    // ...
};

不同于类模板的普通函数成员,成员模板是函数模板。当我们在类模板外面定义一个成员模板,我们必须为类模板和函数模板提供模板参数列表。类模板参数列表在前,后面跟成员模板参数列表:

1
2
3
4
5
template <typename T>     // type parameter for the class
template <typename It>    // type parameter for the constructor
    Blob<T>::Blob(It b, It e):
              data(std::make_shared<std::vector<T>>(b, e)) {
}

成员模板的实例化

为了实例化一个类模板的成员模板,我们必须为类模板和成员模板的模板参数提供实参。和以前一样,类模板参数的实参由调用成员模板的对象类型决定。同样,编译器通过传递给成员模板的实参推导出成员模板参数的实参。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int ia[] = {0,1,2,3,4,5,6,7,8,9};
vector<long> vi = {0,1,2,3,4,5,6,7,8,9};
list<const char*> w = {"now", "is", "the", "time"};
// instantiates the Blob<int> class
// and the Blob<int> constructor that has two int* parameters
Blob<int> a1(begin(ia), end(ia));
// instantiates the Blob<int> constructor that has
// two vector<long>::iterator parameters
Blob<int> a2(vi.begin(), vi.end());
// instantiates the Blob<string> class and the Blob<string>
// constructor that has two (list<const char*>::iterator parameters
Blob<string> a3(w.begin(), w.end());

16.1.5 控制实例化

当模板被使用时才生成实例意味着同一个实例可能出现在多个对象文件。当两个或更多单独编译的源文件用相同的模板实参使用同一个模板时,在每一个文件中有一个模板实例。

在大型系统中,在多个文件中实例化同一个模板的开销将变得非常明显。在新标准下,我们可以使用显式实例化避免这种开销。显式实例化具有形式:

1
2
extern template declaration; // instantiation declaration
template declaration;        // instantiation definition

declaration是一个类或函数声明,其模板参数被模板实参替换。

1
2
3
// instantion declaration and definition
extern template class Blob<string>;             // declaration
template int compare(const int&, const int&);   // definition

当编译器看到一个extern模板声明,它不会在那个文件生成实例化代码。将一个模板实例声明为extern承诺程序的其它地方存在一个nonextern的实例。对于一个指定的实例可以有多个extern声明但是只能存在一个实例的定义。

因为编译器使用模板时自动实例化,extern声明必须出现在任何使用实例的代码之前:

1
2
3
4
5
6
7
8
9
// Application.cc
// these template types must be instantiated elsewhere in the program
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sa1, sa2; // instantiation will appear elsewhere
// Blob<int> and its initializer_list constructor instantiated in this file
Blob<int> a1 = {0,1,2,3,4,5,6,7,8,9};
Blob<int> a2(a1);  // copy constructor instantiated in this file
int i = compare(a1[0], a2[0]); // instantiation will appear elsewhere定义

警告 对于每一个实例声明,在程序的其它地方必须有一个显式实例定义。

实例定义实例化所有成员

一个类模板的实例定义实例化其所有成员包括内联成员函数。当编译器看到一个实例定义,它不知道哪一个成员函数会被用到,因此编译器实例化所有成员。

注释 实例定义只能用在类模板所有成员函数都能使用的类型。

16.1.6 效率和灵活性

标准库的智能指针提供了一个关于模板设计者面临的设计选择的很好说明。

16.2 模板实参推导

在模板实参推导期间,编译器使用实参的类型查找模板参数并生成最符合的函数。

16.2.1 转换和模板类型参数

使用模板类型参数的函数参数具有特殊的初始化规则。只有非常有限数量的转换被自动应用到这种参数。编译器生成一个新的实例而不是转换实参。

和以往一样,不管是形参还是实参的上层const都被忽略。在一个函数模板调用中执行的唯一其它转换有:

  • const转换:引用(或指针)const对象的函数参数可以传递引用(或指针)非const对象。
  • 数组到指针或函数到指针的转换:如果函数参数不是引用类型,则普通指针转换将被应用到参数为数组或函数类型上。数组参数被转换为指向第一个元素的指针。类似地,函数参数被转换为指向函数类型的指针。

其它转换比如算术转换,派生类到基类转换和用户自定义的转换都不会执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template <typename T> T fobj(T, T); // arguments are copied
template <typename T> T fref(const T&, const T&); // references
string s1("a value");
const string s2("another value");
fobj(s1, s2); // calls fobj(string, string); const is ignored
fref(s1, s2); // calls fref(const string&, const string&)
              // uses premissible conversion to const on s1
int a[10], b[42];
fobj(a, b); // calls f(int*, int*)
fref(a, b); // error: array types don't match

注释 const转换和数组或函数到指针的转换是模板类型唯一的实参到形参自动转换。

使用相同模板参数类型的函数参数

一个模板类型参数可以被多个函数参数使用。因为转换非常有限,这种参数的实参必须具有一样的类型。如果推导的参数类型不匹配,那么调用出错。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
long lng;
compare(lng, 1024); // error: cannot instantiate compare(long, int)

// argument types can differ but must be compatible
template <typename A, typename B>
int flexibleCompare(const A& v1, const B& v2)
{
    if (v1 < v2) return -1;
    if (v2 < v1) return 1;
    return 0;
}
long lng;
flexibleCompare(lng, 1024); // ok: calls flexibleCompare(long, int)

应用到普通实参的正常转换

函数模板可以有普通类型的参数,就是与模板类型参数无关的参数。这些参数不需要特殊处理,它们的转换规则和以前一样。

1
2
3
4
5
6
7
8
template <typename T> ostream &print(ostream &os, const T
&obj)
{
    return os << obj;
}
print(cout, 42); // instantiates print(ostream&, int)
ofstream f("output");
print(f, 10);    // uses print(ostream&, int); converts f to ostream&

16.2.2 函数模板显式参数

在某些情况下,编译器不可能推导出模板参数的类型。另一些情况,我们想要允许用户控制模板实例化。当一个函数返回类型不同于参数列表中的类型时,这两种情况经常出现。

指定一个显式模板参数

1
2
3
// T1 cannot be deduced: it doesn't appear in the function parameter list
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);

这个例子中,没有用来推导T1类型的实参,调用者必须提供一个显式模板实参。我们可以像定义类模板实例那样,给一个函数调用提供显式模板实参。显式模板实参在尖括号中指定,在函数名之后,参数列表之前:

1
2
// T1 is explicitly specified; T2 and T3 are inferred from the argument types
auto val3 = sum<long long>(i, lng); // long long sum(int, long)

显式模板实参从左到右匹配相应的模板参数,

1
2
3
4
5
6
7
8
// poor design: users must explicitly specify all three template parameters
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1);

// error: can't infer initial template parameters
auto val3 = alternative_sum<long long>(i, lng);
// ok: all three parameters are explicitly specified
auto val2 = alternative_sum<long long, int, long>(i, lng);

显式指定参数应用普通转换

和普通类型参数允许普通转换的原因一样,显式指定参数也可以应用其实参的普通转换:

1
2
3
4
long lng;
compare(lng, 1024);       // error: template parameters don't match
compare<long>(lng, 1024); // ok: instantiates compare(long, long)
compare<int>(lng, 1024);  // ok: instantiates compare(int, int)

16.2.3 尾返回类型和类型转换

使用显式模板参数来表示模板函数的返回类型工作得很好。另外一些情况下,需要有一个显式模板参数给用户强加了负担。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
template <typename It>
??? &fcn(It beg, It end)
{
    // process the range
    return *beg;  // return a reference to an element from the range
}

vector<int> vi = {1,2,3,4,5};
Blob<string> ca = { "hi", "bye" };
auto &i = fcn(vi.begin(), vi.end()); // fcn should return int&
auto &s = fcn(ca.begin(), ca.end()); // fcn should return string&

// a trailing return lets us declare the return type after the parameter list is seen
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
    // process the range
    return *beg;  // return a reference to an element from the range
}

类型转换标准库模板类

有时候我们不能直接访问到我们需要的类型。比如我们可能想要写一个类似fcn的函数返回一个元素的值而不是引用。编写这样的函数我们面临的问题是我们几乎不了解我们传递的参数类型。为了获取元素类型,我们可以使用库类型转换模板。这些模板定义在type_traits头文件。通常头文件type_traits中的类被用来所谓的元编程。

For Mod<T>, Mod is If T is Then Mod<T>::type is
remove_reference X& or X&&
otherwise
X
T
add_const X&, const X, or function
otherwise
T
const T
add_lvalue_reference X&
X&&
otherwise
T
X&
T&
add_rvalue_reference X& or X&&
otherwise
T
T&&
remove_pointer X*
otherwise
X
const T
add_pointer X& or X&&
otherwise
X*
T*
make_signed unsigned X
otherwise
X
T
make_unsigned signed type
otherwise
unsigned T
T
remove_extent X[n]
otherwise
X
T
remove_all_extents X[n1][n2]…
otherwise
X
T

在这个例子中,我们可以使用remove_reference获取元素类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
remove_reference<decltype(*beg)>::type

// must use typename to use a type member of a template parameter; see § 16.1.3 (p.
670)
template <typename It>
auto fcn2(It beg, It end) ->
    typename remove_reference<decltype(*beg)>::type
{
    // process the range
    return *beg;  // return a copy of an element from the range
}

注意type是一个依赖模板参数的类成员。因此我们必须在返回类型的声明中使用typename来告诉编译器type代表一个类型。

每一个上表中描述的类型转换模板和remove_reference类似工作。每一个模板都有一个公开的type成员代表一个类型。如果不可能(或没必要)转换一个模板参数,则type成员就是模板参数本身。

16.2.4 函数指针和实参推导

当我们从一个函数模板初始化或赋值一个函数指针时,编译器使用函数指针的类型推导模板实参。

1
2
3
template <typename T> int compare(const T&, const T&);
// pf1 points to the instantiation int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;

如果从函数指针类型不能决定模板实参则出错:

1
2
3
4
// overloaded versions of func; each takes a different function pointer type
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare); // error: which instantiation of compare?

我们可以使用显式模板实参使得调用无二义性:

1
2
// ok: explicitly specify which version of compare to instantiate
func(compare<int>);  // passing compare(const int&, const int&)

注释 当一个函数模板实例的地址被使用时,其应用环境必须是只允许唯一类型或值可以确定每一个模板参数。

16.2.5 模板实参推导和引用

为了理解下例函数类型推导,需要记住两点,一是正常的引用绑定规则,二是const是低层次不是高层次的。

1
template <typename T> void f(T &p);

左值引用函数参数类型推导

当一个函数参数是普通的左值引用模板参数,我们只能传递左值给它。如果实参是const,则T被推导为一个const类型。

1
2
3
4
5
template <typename T> void f1(T&);  // argument must be an lvalue
// calls to f1 use the referred-to type of the argument as the template parameter type
f1(i);   //  i is an int; template parameter T is int
f1(ci);  //  ci is a const int; template parameter T is const int
f1(5);   //  error: argument to a & parameter must be an lvalue

如果一个函数参数是const T&,则可以传递任何种类给它。如果实参是const,则T被推导为普通类型。

1
2
3
4
5
6
template <typename T> void f2(const T&); // can take an rvalue
// parameter in f2 is const &; const in the argument is irrelevant
// in each of these three calls, f2's function parameter is inferred as const int&
f2(i);  // i is an int; template parameter T is int
f2(ci); // ci is a const int, but template parameter T is int
f2(5);  // a const & parameter can be bound to an rvalue; T is int)

右值引用函数参数类型推导

当一个函数参数是右值引用,正常绑定规则告诉我们可以传递一个右值给函数参数。类型推导的行为和普通左值引用类似。被推导的类型T是右值的类型。

1
2
template <typename T> void f3(T&&);
f3(42); // argument is an rvalue of type int; template parameter T is int

引用折叠和右值引用参数

C++在正常绑定规则上定义了2种特例,允许将右值引用绑定到左值上。

第一种特例是关于右值引用如何推导的。当传递一个左值给右值引用模板参数,编译器推导模板参数类型为实参的左值引用。

1
2
f3(i);  // argument is an lvalue; template parameter T is int&
f3(ci); // argument is an lvalue; template parameter T is const int&

第二种特例是如果我们间接地创建了一个引用的引用,则这些引用“折叠”。 对于一个给定类型X:

  • X& &, X& &&和X&& &折叠为X&
  • X&& &&折叠为X&&

注释 引用折叠仅仅应用在间接创建了一个引用的引用,比如通过类型别名或模板参数。

这两个特例产生两个重要结果:

  • 一个是右值引用的函数模板参数可以绑定到左值上
  • 如果实参是一个左值,则模板实参类型被推导为左值引用,且函数参数被实例化为左值引用参数

这也意味着可以传递任意类型给T&&函数参数。

编写右值引用参数模板函数

模板参数可以被推导为引用类型这个事实,对于在模板内编写代码有惊人的冲击:

1
2
3
4
5
6
template <typename T> void f3(T&& val)
{
    T t = val;  // copy or binding a reference?
    t = fcn(t); // does the assignment change only t or val and t?
    if (val == t) { /* ... */ } // always true if T is a reference type
}

实际上,右值引用参数被用在2个场景,一个是模板转发它的实参,另一个是模板重载。

1
2
3
4
template <typename T> void f(T&&);      // binds to nonconst
rvalues
template <typename T> void f(const T&); // lvalues and const
rvalues

16.2.6 理解std::move

库函数move是一个展示右值引用模板参数的好例子。

std::move是如何定义的

标准库定义move如下:

1
2
3
4
5
6
7
8
// for the use of typename in the return type and the cast see § 16.1.3 (p. 670)
// remove_reference is covered in § 16.2.3 (p. 684)
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    // static_cast covered in § 4.11.3 (p. 163)
    return static_cast<typename remove_reference<T>::type&&>(t);
}

std::move是如何工作的

1
2
3
string s1("hi!"), s2;
s2 = std::move(string("bye!")); // ok: moving from an rvalue
s2 = std::move(s1);  // ok: but after the assigment s1 has indeterminate value

第一个赋值,传递给move的实参是一个右值:

  • T推导的类型为string
  • remove_reference::type是string
  • move的返回类型是string&&
  • move的函数参数t类型为string&& 因此这个调用实例化的move是string&& move(string&& t)

第二个赋值,传递给move的实参是一个左值:

  • T推导的类型为string&
  • remove_reference<string&>::type是string
  • move的返回类型是string&&
  • move的函数参数t类型为string& &&,折叠为string& 因此这个调用实例化的move是string&& move(string& t)

static_cast一个左值引用为右值引用是允许的

不能隐式将左值引用转换为右值引用,必须使用static_cast。

16.2.7 转发

有些函数需要转发一个或多个参数给其他函数。这种情况下,我们需要保存被转发参数的所有信息。

1
2
3
4
5
6
7
8
// template that takes a callable and two parameters
// and calls the given callable with the parameters ''flipped''
// flip1 is an incomplete implementation: top-level const and references are lost
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
    f(t2, t1);
}

这个模板工作得很好直到调用一个有引用参数的函数:

1
2
3
4
void f(int v1, int &v2) // note v2 is a reference
{
    cout << v1 << " " << ++v2 << endl;
}

f改变了v2的值,但是如果通过flip1调用f,则f做的改变不影响原来的实参:

1
2
f(42, i);        // f changes its argument i
flip1(f, j, 42); // f called through flip1 leaves j unchanged

定义保留类型信息的函数参数

我们可以定义函数参数为模板类型的右值引用保留实参的所有类型信息。使用引用参数可以保存const属性,通过引用折叠可以保存左值或右值属性。

1
2
3
4
5
template<typename F, typename T1, typename T2>
void flip2(F f, T1&& t1, T2&& t2)
{
    f(t2, t1);
}

注解 函数的一个右值引用的模板类型参数保留实参的const属性和左值或右值属性。

flip2函数只解决了一半问题。flip2函数当函数参数为左值引用时工作很好,但是不能调用有右值引用参数的函数,比如:

1
2
3
4
5
6
void g(int &&i, int& j)
{
    cout << i << " " << j << endl;
}

flip2(g, i, 42); // error: can't initialize int&& from an lvalue

函数参数和任何其他变量一样,是一个左值表达式。结果flip2中的g调用传递一个左值给g的右值引用参数。

使用std::forward在函数调用种保留类型信息

我们可以使用一个新的库工具forward传递给flip2的函数参数并保留类型信息。和move一样,forward定义在utility头文件。与move不一样的是,forward调用时必须显示指定模板实参。forward返回其模板实参的右值引用,即forward返回类型为T&&

通常我们使用forward来传递定义为右值引用模板类型的函数参数。通过返回类型的引用折叠,forward保留了指定实参的类型:

1
2
3
4
5
template <typename Type> intermediary(Type&& arg)
{
    finalFcn(std::forward<Type>(arg));
    // ...
}

注解 当使用函数参数为右值引用模板类型时,forward保留实参类型的所有信息

使用forward,我们可以重写flip函数为:

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

注解 和std::move一样,不提供using声明使用std::forward是个好主意

16.3 重载和模板

函数模板可以被其他模板或普通的非模板函数重载。和以往一样,同名函数的参数个数或类型必须不一样。

函数匹配受函数模板的影响,表现为下面几点:

  • 候选函数包含任何成功实例化的模板函数。
  • 候选函数模板总是可行的,因为模板实参会去除任何不可行的模板。
  • 和以往一样,可行函数(模板和非模板)通过转换评级。
  • 如果仅有一个函数比其他更匹配,则这个函数被选中。如果有多个函数匹配,则:
  1. 如果只有一个非模板函数,则该函数被选中。
  2. 如果没有非模板函数,且其中某个比其他更特例,则该函数被选中。
  3. 调用有歧义。

警告 正确定义一组重载函数模板需要很好地理解类型之间的关系和函数模板参数转换的限制。

编写重载模板

作为一个例子,我们将定义一组调试时有用的函数

1
2
3
4
5
6
7
// print any type we don't otherwise handle
template <typename T> string debug_rep(const T &t)
{
    ostringstream ret; // see § 8.3 (p. 321)
    ret << t; // uses T's output operator to print a representation of t
    return ret.str(); // return a copy of the string to which ret is bound
}

这个函数可以用来生成任何具有输出操作符类型的字符串表示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// print pointers as their pointer value, followed by the object to which the pointer points
// NB: this function will not work properly with char*; see § 16.3 (p. 698)
template <typename T> string debug_rep(T *p)
{
    ostringstream ret;
    ret << "pointer: " << p;         // print the pointer's own value
    if (p)
        ret << " " << debug_rep(*p); // print the value to which p points
    else
        ret << " null pointer";      // or indicate that the p is null
    return ret.str(); // return a copy of the string to which ret is bound
}

这是一个指针版本。

1
2
string s("hi");
cout << debug_rep(s) << endl;

对于这个调用,只有第一个版本可行。

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

对于这个调用,两个函数都可行:

  • debug_rep(const string* &),第一个函数实例化,T推导为string*
  • debug_rep(string*),第二个函数实例化,T推导为string

第二个版本实例化对于这个调用完全匹配,第一个版本需要将普通指针转换为const。

多个可行模板

作为另一个例子,考虑以下调用:

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

这里2个模板都可行且都完全匹配:

  • debug_rep(const string* &),T推导为const string*
  • debug_rep(const string*),T推导为const string

这种情况下,普通函数匹配不能区分这2个调用。然而由于重载函数模板的特殊规则,这个调用确认为debug_rep(T*),即这个更特殊的模板。理由是,如果没有这个规则,将没有办法调用指针版本的debug_rep。

注解 当有多个重载模板对于一个调用提供同样好的匹配时,更特殊的模板版本被选择。

非模板和模板重载

我们定义一个普通的非模板版本的debug_rep:

1
2
3
4
5
// print strings inside double quotes
string debug_rep(const string &s)
{
    return '"' + s + '"';
}

当我们使用string调用debug_rep时:

1
2
string s("hi");
cout << debug_rep(s) << endl;

有两个可行的函数:

  • debug_rep(const string&),第一个模板T绑定到string
  • debug_rep(const string&),普通的非模板函数

这种情况下,普通的非模板函数被选择。出于同样的原因,同样好的匹配的函数,更特殊的被选择。

注解 当一个非模板函数对于一个调用提供和函数模板一样好的匹配时,非模板函数被选择。

重载模板和转换

有一种情况还没有谈到,指向C风格字符串的指针和字符串字面值。考虑下面这个调用:

1
cout << debug_rep("hi world!") << endl; // calls debug_rep(T*)

有3个debug_rep函数可行:

  • debug_rep(const T&),T绑定到char[10]
  • debug_rep(T*),T绑定到const char
  • debug_rep(const string&),需要将const char*转换到string

2个模板版本的函数提供完全匹配,非函数模板需要用户定义的转换,因此更特殊的模板版本被选择。

如果像将字符指针处理为string,可以再定义2个非模板重载函数:

1
2
3
4
5
6
7
8
9
// convert the character pointers to string and call the string version of debug_rep
string debug_rep(char *p)
{
    return debug_rep(string(p));
}
string debug_rep(const char *p)
{
    return debug_rep(string(p));
}

缺失声明可导致程序表现异常

值得注意的是为了char版本的debug_rep正常工作,当char版本函数定义时,debug_rep(const stirng&)声明必须在作用域内。否则模板版本将被调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
template <typename T> string debug_rep(const T &t);
template <typename T> string debug_rep(T *p);
// the following declaration must be in scope
// for the definition of debug_rep(char*) to do the right thing
string debug_rep(const string &);
string debug_rep(char *p)
{
    // if the declaration for the version that takes a const string& is not in scope
    // the return will call debug_rep(const T&) with T instantiated to string
    return debug_rep(string(p));
}

提示 定义任何函数之前声明重载集中每一个函数。这样就不用担心编译器看到你想用的函数之前实例化一个函数。

16.4 变长模板

C++ 11: 变长模板是一个模板函数或类拥有可变数量的参数。可变参数被称为参数包。有两种参数包,模板参数包表示0个或多个模板参数,函数参数包表示0个或多个函数参数。

我们使用…表示模板或函数参数包。在模板参数列表中,class…或typename…指示接下来的参数表示0个或多个类型;类型名后面跟…表示0个或多个非类型参数列表。在函数参数列表中,类型为模板参数包的参数是一个函数参数包:

1
2
3
4
5
// Args is a template parameter pack; rest is a function parameter pack
// Args represents zero or more template type parameters
// rest represents zero or more function parameters
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);

通常,编译器从函数实参中推导模板参数类型。对于一个变长模板,编译器同样推导出参数包中的参数个数:

1
2
3
4
5
int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d);    // three parameters in the pack
foo(s, 42, "hi");    // two parameters in the pack
foo(d, s);           // one parameter in the pack
foo("hi");           // empty pack

编译器将实例化4个不同的foo:

1
2
3
4
5
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…操作符

C++ 11: 当我们想要直到参数包中有多少个元素时,可以使用sizeof…操作符。和sizeof一样,sizeof…返回一个常量,而且不会计算实参:

1
2
3
4
template<typename ... Args> void g(Args ... args) {
    cout << sizeof...(Args) << endl;  // number of type parameters
    cout << sizeof...(args) << endl;  // number of function parameters
}

编写变长函数模板

我们定义一个变长函数print,打印实参列表的值。变长函数通常是递归的,第一个调用处理参数包中第一个参数,然后用剩下的参数调用自己。为了终止递归,我们需要定义一个非变长的print函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// function to end the recursion and print the last element
// this function must be declared before the variadic version of print is defined
template<typename T>
ostream &print(ostream &os, const T &t)
{
    return os << t; // no separator after the last element in the pack
}
// this version of print will be called for all but the last element in the pack
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest)
{
    os << t << ", ";           // print the first argument
    return print(os, rest...); // recursive call; print the other arguments
}

警告 当print的变长版本定义时,print的非变长版本必须在作用域中,否则变长函数将无限递归。

包展开

除了获取包的大小,对参数包可以做的唯一的另外一件事就是包展开。当我们展开一个包时,我们也可以提供一个模式用在每一个被展开的元素。将…放在模式右边触发包展开。

例如,print函数包含2个展开:

1
2
3
4
5
6
template <typename T, typename... Args>
ostream& print(ostream &os, const T &t, const Args&... rest)// expand Args
{
    os << t << ", ";
    return print(os, rest...); // expand rest
}

Args的展开应用模式const Args&到每一个元素。第二个展开应用模式函数参数包的名字。

理解包展开

print的函数参数包展开仅仅是将包展开为它的组成部分。更复杂的模式也是可能的,比如:

1
2
3
4
5
6
7
// call debug_rep on each argument in the call to print
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
    // print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an)
    return print(os, debug_rep(rest)...);
}

print的调用中使用模式debug_rep(rest)。这个模式对函数参数包的每一个元素调用debug_rep。

注解 展开的模式单独地作用于包中的每一个元素

转发参数包

在新标准下,我们可以使用变长模板和forward一起编写函数,将实参不变地传递给其他函数。作为展示,我们将添加一个emplace_back成员函数到StrVec类。标准库容器的emplace_back成员是一个变长的模板成员函数,它使用其实参在容器的内存空间直接构造元素。

我们知道,保存类型信息有2个步骤。第一,为了保存实参的类型信息,必须定义函数参数为右值模板参数。

1
2
3
4
5
class StrVec {
public:
    template <class... Args> void emplace_back(Args&&...);
    // remaining members as in § 13.5 (p. 526)
};

第二,当emplace_back将实参传递给构造函数时,必须使用forward保存实参的原有类型

1
2
3
4
5
6
template <class... Args>
inline void StrVec::emplace_back(Args&&... args)
{
    chk_n_alloc(); // reallocates the StrVec if necessary
    alloc.construct(first_free++, std::forward<Args>(args)...);
}

建议: 转发和变长模板 变长函数通常转发它们的参数给其他函数。这种函数一般具有和emplace_back函数类似的形式:

1
2
3
4
5
6
7
8
// fun has zero or more parameters each of which is
// an rvalue reference to a template parameter type
template<typename... Args>
void fun(Args&&... args) // expands Args as a list of rvalue references
{
    // the argument to work expands both Args and args
    work(std::forward<Args>(args)...);
}

16.5 模板特化

一个模板并不总是适合每一个模板实参。某些情况下,通用的模板定义对于一个类型是错的,可能不能编译或做的事情是错的。有时候我们可能想利用一些特有的知识来写比模板实例化更有效的代码。当我们不能(或不想)使用模板版本时,我们可以模板的特殊版本。

compare函数是一个通用函数模板定义不能满足特定类型的很好例子。

1
2
3
4
5
// first version; can compare any two types
template <typename T> int compare(const T&, const T&);
// second version to handle string literals
template<size_t N, size_t M>
int compare(const char (&)[N], const char (&)[M]);

带非类型模板参数版本的compare只会被字符串字面值或字符数组调用,如果使用字符指针调用compare,则第一个版本被调用:

1
2
3
const char *p1 = "hi", *p2 = "mom";
compare(p1, p2);      // calls the first template
compare("hi", "mom"); // calls the template with two nontype parameters

为了处理字符指针,我们可以定义第一个compare版本的一个模板特化。模板特化是模板的一个独立定义,它的一个或多个模板参数被指定为特定类型。

定义函数模板特化

当我们特化一个函数模板时,必须提供原模板中每个模板参数的实参类型。我们使用template <>表示模板特化。空<>指示将提供所有模板参数的实参:

1
2
3
4
5
6
// special version of compare to handle pointers to character arrays
template <>
int compare(const char* const &p1, const char* const &p2)
{
    return strcmp(p1, p2);
}

当我们定义一个特化时,函数模板参数类型必须匹配之前声明的模板。

函数重载VS模板特化

当我们定义一个函数模板特化时,本质上我们接管了编译器的工作。特化是一个实例,它不是函数名的重载。

注解 特化实例化一个模板,而不是重载函数。因此特化不影响函数匹配。

关键概念: 普通作用域规则适用于特化 为了特化一个模板,原模板的声明在作用域中必须可见。而且,特化的声明必须在任何使用该模板实例的代码作用域之前可见。

最佳实践 模板和它的特化应该在同一个头文件中声明。模板通用声明应该先出现,后跟任意模板特化。

类模板特化

除了特化函数模板,我们也可以特化类模板。作为一个例子,我们定义库hash模板的一个特化,使得可以将Sales_data对象存到一个unordered容器。特化的hash类必须定义:

  • 一个重载调用操作符,接收容器的键类型对象并返回size_t
  • 2个类型成员,result_type和argument_type,它们是调用操作符的返回类型和实参类型。
  • 默认构造函数和赋值操作符,可被隐式定义。

定义hash特化唯一复杂之处是,当我们特化一个模板时,必须和原有模板在同一个命名空间。因此我们必须先打开命名空间:

1
2
3
// open the std namespace so we can specialize std::hash
namespace std {
}  // close the std namespace; note: no semicolon after the close curly

任何在花括号之间的定义将成为命名空间std的一部分。下面定义对于Sales_data类型hash的一个特化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// open the std namespace so we can specialize std::hash
namespace std {
template <>  // we're defining a specialization with
struct hash<Sales_data> // the template parameter of Sales_data
{
    // the type used to hash an unordered container must define these types
    typedef size_t result_type;
    typedef Sales_data argument_type; // by default, this type needs ==
    size_t operator()(const Sales_data& s) const;
    // our class uses synthesized copy control and default constructor
};
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);
}
} // close the std namespace; note: no semicolon after the close curly

hash<Sales_data>定义以template<>开始,它指示我们定义一个完全特化的模板。

注解 为了使Sales_data的用户可以使用hash的特例,我们应该在Sales_data的头文件中定义这个特化。

类模板偏特化

和函数模板不同的是,类模板特化可以不必提供每一个模板参数的实参。我们可以指定一部分而不是全部。一个类模板偏特化本身是一个模板。用户必须提供那些没有固定在特化中的模板参数。

注解 我们只能部分特化一个类模板,不能部分特化一个函数模板。

标准库remove_reference类型就是通过一系列特化工作的:

1
2
3
4
5
6
7
8
9
// original, most general template
template <class T> struct remove_reference {
    typedef T type;
};
// partial specializations that will be used for lvalue and rvalue references
template <class T> struct remove_reference<T&>  // lvalue references
{ typedef T type; };
template <class T> struct remove_reference<T&&> // rvalue references
{ typedef T type; };

因为模板偏特化是一个模板,我们还是以定义模板参数开始。模板偏特化的参数列表和原模板是一样的。在类名后,我们指定特化模板的实参。这些实参包括在<>之中,且和圆模板的参数位置对应。

模板偏特化的参数列表是原模板的一个子集。

1
2
3
4
5
6
7
int i;
// decltype(42) is int, uses the original template
remove_reference<decltype(42)>::type a;
// decltype(i) is int&, uses first (T&) partial specialization
remove_reference<decltype(i)>::type b;
// decltype(std::move(i)) is int&&, uses second (i.e., T&&) partial specialization
remove_reference<decltype(std::move(i))>::type c;

特化成员而不是类

我们可以仅仅特化指定的成员函数,而不是特化整个类模板。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
template <typename T> struct Foo {
    Foo(const T &t = T()): mem(t) { }
    void Bar() { /* ... */ }
    T mem;
    // other members of Foo
};
template<>           // we're specializing a template
void Foo<int>::Bar() // we're specializing the Bar member of Foo<int>
{
    // do whatever specialized processing that applies to ints
}

这里我们仅仅特化Foo类的一个成员,其他成员由Foo模板提供:

1
2
3
4
Foo<string> fs;  // instantiates Foo<string>::Foo()
fs.Bar();        // instantiates Foo<string>::Bar()
Foo<int> fi;     // instantiates Foo<int>::Foo()
fi.Bar();        // uses our specialization of Foo<int>::Bar()