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

文章目录

第16章 模板与泛型编程

定义模板

函数模板

template<typename T>
int compare(const T &v1, const T &v2)
{
    if(v1 < v2)
        return -1;
    if(v2 < v1)
        return 1;
    return 0;
}
  • 模板定义以关键字template开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用<>包围
  • 模板参数列表的作用很像函数参数列表,函数参数列表定义了若干特定类型的局部变量,但并未指出如何初始化它们。在运行时调用者提供实参来初始化形参
  • 类似的,模板参数在类或函数定义中用到的类型或值。当使用模板时,我们指定模板实参,将其绑定到模板参数上
  • 当我们调用一个函数模板时,编译器通常用函数实参来为我们推断模板实参。编译器用推断出的模板参数来为我们实例化一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个实例
// 实例化出int compare(const int&, const int&)
cout << compare(1,0) <<endl;
// 实例化出int compare(const vector<int>&, const vector<int>&)
vector<int> vec1{1,2,3}, vec2{4,5,6};
cout << compare(vec1, vec2) << endl;
  • 这些编译器生成的版本被称为模板的实例
  • 我们的compare函数有一个模板类型参数,一般来说,我们可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用,特别是,类型参数可以用来指定返回类型或函数的参数类型,以及函数体内用于变量声明或类型转换
// 正确:返回类型和参数类型相同
template <typename T> T foo(T* p)
{
    T tmp = *p;
    // ...
    return tmp;
}
  • 类型参数前必须使用关键字class或typename,在模板参数列表中,这两个关键字含义相同,可以互换使用。一个模板参数列表中可以同时使用两个关键字。但是typename关键字用来指定模板类型更直观
  • 除了定义类型参数,还可以在模板中定义非类型参数,一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字class或typename来指定非类型参数
  • 当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值多代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
    return strcmp(p1, p2);
}
compare("hi", "mom");
  • 一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或左值引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态生存期,不能用一个普通(非static)的局部变量或动态对象作为指针或引用非类型模板参数的实参。指针参数也可以nullptr或值为0的常量的表达式
  • 函数模板可以声明为inline或constexpr
template <typename T> inline T min(const T&, const T&);
  • 我们最初的compare函数虽然简单,但说明了编写泛型代码的两个重要原则:模板中的函数参数是const的引用;函数体中条件判断仅适用<运算符
  • 因为只适用<运算符,降低了该函数对要处理类型的要求,这个类型只要支持小于运算符即可
  • 实际上,可能用less来定义函数更有类型无关和可移植性
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;
}
  • 当编译器遇到一个模板定义时,它并不生成代码。只有当实例化出模板的一个特定版本时,编译器参会生成代码,当我们使用(而不是定义)模板时,编译器才生成代码,这一特点影响我们如何组织代码以及错误何时被检测到。
  • 通常,当我们调用一个函数,编译器只需要掌握函数的声明。类似,当我们使用一个类类型的对象时,类定义是必须的,但是成员函数不必定义。因此,我们将类定义和函数声明放在头文件中,而普通函数和成员函数的定义放在源文件中
  • 模板则不同,为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员的定义。因此,与非模板代码不同,模板的头文件通常即包括声明也包括定义
  • 模板设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。用户必须包含模板头文件,以及用来实例化模板的任何类型的头文件
  • 通常编译器会在三个阶段报告错误
    • 编译模板本身:检查语法错误
    • 模板使用时:实参数目,参数类型是否匹配,对于类模板,检查用户是否提供了正确数目的模板实参
    • 模板实例化时:发现类型相关错误,依赖于编译器如何管理实例化,这类错误可能在链接时才报告

类模板

  • 类模板是用来生成类的蓝图的,与函数模板不同的是,编译器不能为类模板推断,需要用户提供额外信息,用来代替模板参数的模板实参列表
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
template<typename T>
class Blob
{
    public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_type;
    Blob();
    Blob(std::initializer_list<T> il);
    size_type size() const
    {
        return data->size();
    }
    bool empty()const
    {
        return data->empty();
    }
    void push_back(const T &t)
    {
        data->push_back(t);
    }
    void push_back(T &&t)
    {
        data->push_back(std::move(t));
    }
    T& back();
    T& operator[](size_type i);
    private:
    std::shared_ptr<std::vector<T>> data;
    void check(size_type i, const std::string &msg) const;
};
  • 我们已经知道,使用类模板需要提供额外信息,这些信息是显式模板实参列表,它们被绑定到模板参数。编译器使用这些模板实参来实例化出特定的类
  • 一个类模板的每个实例都形成一个独立的类,类型Blob<string>与任何其他Blob类型都没有关联,也不会对任何其他Blob类型的成员有特殊访问权限
  • 类模板的成员函数是一个普通函数,但是,类模板的每个实例都有自己版本的成员函数,因此,类模板的成员函数具有和模板相同的模板参数。因而,定义在类模板之外的成员函数必须以关键字template开始,后接类模板参数列表。
  • 在类外定义成员,必须说明成员属于哪个类,而且,从一个模板生成的类的名字中必须包含其模板实参。当我们定义一个成员函数时,模板实参与模板形参相同。
template <typename T>
return_type Blob<T>::member_name(param_list)
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)
{
    check(i,"subscript out of range");
    return (*data)[i];
}
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)){}
// 实例化Blob<int>和接受初始化列表的构造函数
Blob<int> squares = {0,1,2,3,4,5,6,7,8,9};
// 实例化Blob<int>::size() const
for(size_t i = 0; i != squares.size(); ++i)
    squares[i] = i * i; // 实例化[]运算符
  • 实例化了Blob<int>类和它的三个成员函数和一个构造函数

  • 如果一个成员函数没有被使用,则它不会被实例化,成员函数只有在被用到时才进行实例化,这一特性使得即使某种类型不能完全符合模板操作的要求,但是仍然能用该类型实例化类

  • 当我们使用使用一个类模板类型时必须提供模板实参,但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。

// 试图访问一个不存在的元素,类抛出异常
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];
    }
    BlobPtr& operator++();
    BlobPtr& operator--();
    private:
    std::shared_ptr<std::vector<T>> check(std::size_t, const std::string&) const;
    std::weak_ptr<std::vector<T>> wptr;
    std::size_t curr;
};
  • BlobPtr的前置递增和递减成员返回BlobPtr&,而不是BlobPtr<T>&。当我们处于一个类模板的作用域中时,编译器处理模板自身引用时就好像我们提供了与模板参数匹配的实参一样。
  • 当我们在类模板外定义其成员时,必须记住,我们并不在类的作用域中,直到遇到类名才表示进入类的作用域
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
    BlobPtr ret = *this;
    ++*this;
    return ret;
}
  • 由于返回类型位于类的作用域之外,我们必须指出返回类型是一个实例化的BlobPtr,它所用类型与类实例化所用的类型一致。在函数体内,我们已经进入类的作用域,因此在定义ret时,无需重复模板实参,如果不提供模板实参,则编译器假定我们使用的类型与成员实例化所有的类型一致。
  • 等价于
BlobPtr<T> ret = *this;
  • 类模板和友元
  • 当一个类包含一个友元声明时,类与友元各自是否是模板是相互无关的。如果一个类模板包含一个非模板友元,则友元被授权访问所有模板实例。如果友元是模板,类可以授权给所有友元模板实例,也可以授权给特定实例
  • 类模板与另一个模板间友好关系的最常见的形式是建立对应实例及其友元间的友好关系
  • 为了引用模板的一个特定实例,我们必须首先声明模板自身。一个模板声明包含模板参数列表
// 前置声明,在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
{
    // 每个Blob实例将访问权限授予相同类型实例化的BlobPtr和相等运算符
    friend class BlobPtr<T>;
    friend bool operator==<T> (const Blob<T>&, const Blob<T>&);
};
  • 友元的声明有Blob的模板形参作为它们自己的模板实参。因此友好关系被限定在用相同类型实例化的Blob与BlobPtr、相等运算符之间。
  • 一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元
// 前置声明,在将模板的一个特定实例声明为友元是用到、
template <typename T> class Pal;
class C
{
    friend class Pal<C>; // 用类C实例化的Pal是C的一个友元
    // Pal2的所有实例都是C的友元,这种情况无须前置声明
    template <typename T> friend class Pal2;
};
template <typename T> class C2
{
    // C2的每个实例将相同实例化的Pal声明为友元
    friend class Pal<T>; // Pal的模板声明必须在作用域之内
    // Pal2的所有实例都是C2的每个实例的友元,不需要前置声明
    template <typename X> friend class Pal2;
    // Pal3是一个非模板类,它是C2所有实例的友元
    friend class Pal3; // 不需要前置声明
};
  • 在新标准中,我们可以将模板类型参数声明为友元:
template <typename Type> class Bar
{
    friend Type;
};
  • 新标准允许我们为类模板定义一个类型别名
template<typename T> using twin = pair<T, T>;
twin<string> authors;
twin<int> win_loss;
twin<double> area;

// 当我们定义一个模板类型别名时,可以固定一个或多个模板参数
template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books;
partNo<Student> kids;
  • 类模板的static成员
template <typename T> class Foo
{
    public:
    static std::size_t count(){return ctr;}
    private:
    static  std::size_t ctr;
};
  • 这段代码中,Foo是类模板,一个count的静态成员函数和一个ctr的静态数据成员。每个Foo实例都有自己的static成员实例,即,对任意类型X,都有一个Foo<X>::ctr和一个Foo<X>::count成员。所有Foo<X>类型对象都共享相同的ctr对象和count 函数
  • 与其他静态数据成员相同,模板类的每个静态数据成员必须有且仅有一个定义,但是类模板的每个实例都有一个独有的静态对象。因此,与定义模板的成员函数类似,我们将静态数据成员也定义为模板
template <typename T>
size_t Foo<T>::ctr = 0;

Foo<int> fi; // 实例化Foo<int> 类和static数据成员ctr
auto ct = Foo<int>::count(); // 实例化Foo<int>::count
ct = fi.count(); // 使用Foo<int>::count
ct = Foo::count(); // 错误。使用哪个模板实例的count?
  • 类似任何其他成员函数,一个静态成员函数只有在使用时才会实例化

模板参数

  • 类似函数参数的名字,一个模板参数的名字也没有什么内在含义,我们通常将类型参数命名为T,但实际上我们可以使用任何名字
template <typename Foo> Foo cal(const Foo& a, const Foo& b)
{
    Foo tmp = a;
    return tmp;
}
  • 模板参数遵循普通的作用域规则,一个模板参数名的可用范围在其声明之后,至模板声明或定义结束之前。与其他任何其他名字一样,模板参数会隐藏外层作用域中声明的相同名字。但是,与大多数其他上下文不同,在模板内不能重用模板参数名
typedef double A;
template <typename A, typename B> void f(A a, B b)
{
    A tmp = a; // tmp 类型为模板参数A的类型,而不是double
    double B; // 错误,重声明模板参数B
}
  • 由于参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次
template <typename V, typename V> // 错误
  • 模板声明必须包含模板参数:
// 声明但不定义compare和Blob
template <typename T> int compare(const T&, const T&);
template <typename T> class Blob;
  • 与函数参数相同,声明中的模板参数的名字不必与定义中相同
// 3个clac都指向相同的函数模板
template <typename T>T clac(const T&, const T&);
template <typename U>U clac(const U&, const U&);
template <typename Type>
Type clac(const Type& a. const Type& b){...}
  • 当然,一个给定模板的每个声明和定义都必须有相同数量和种类的参数

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

  • 假设T是一个模板类型参数,当编译器遇到类似T::mem这样的代码时,它不知道mem是类型成员还是静态数据成员,直至实例化时才知道,但是为了处理模板,编译器必须知道名字是否表示一个整型。例如,假定T是一个类型参数的名字,当编译器遇到T::size_type * p; 它需要知道我们正在定义一个名为p的变量还是将名为size_type静态成员与p变量相乘。

  • 默认情况下,假定通过作用域运算符访问的是名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。

template <typename T>
typename T::value_type top(const T& c)
{
    if(!c.empty())
        return c.back();
    else
        return typename T::value_type();
}
  • 它使用typename指明其返回类型并在c中没有元素时生成一个值初始化的元素返回给调用者

  • 我们也可以提供默认模板实参,在新标准中,我们可以为函数和类模板提供默认实参。

// compare有一个默认模板实参less<T>和一个默认函数实参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;
}
  • 我们为此模板参数提供了默认实参,并为其对应的函数参数也提供了默认实参。默认模板实参指出compare将使用标准库的less函数对象类,它是使用与compare一样的类型参数实例化的。默认函数实参指出f将是类型F的一个默认初始化的对象
bool i = compare(0,42);
Sales_data item1(cin), item2(cin);
bool j = compare(item1, item2, compareIsbn);
  • 第二个调用中,我们传递给compare三个实参:compareIsbn和两个Sales_data类型的对象。当传递给compare三个实参时,第三个实参的类型必须是一个可调用对象,该可调用对象的返回类型必须能转换为bool值,且接受的实参类型必须与compare的前两个实参的类型兼容,与往常一样,模板参数的类型从它对应的函数实参推断而来。在此调用中,T的类型被推断为Sales_data,F被推断为compareIsbn的类型
  • 无论何时使用一个类模板,我们都必须在模板名之后接上<>,就算类模板为所有模板参数都提供了默认实参,且我们使用这些默认实参,那也需要在模板名之后跟一个空的<>
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;

成员模板

  • 一个类可以包含本身是模板的成员函数,这种成员称为成员模板,成员模板不能是虚函数
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对象都有一个ostream成员,用于写入数据,还包含一个自身是模板的成员函数,我们可以用这个类代替delete;
double *p = new double;
DebugDelete d;
d(p); // 调用DebugDelete::operator()(double*)
int *ip = new int;
// 在一个临时DebugDelete对象上调用operator()(int*)
DebugDelete()(ip);
  • 由于调用一个DebugDelete对象会delete其给定的指针,我们也可以将DebugDelete用作unique_ptr的删除器,为了重载unique_ptr的删除器,我们在尖括号内给出删除器类型,并提供这个类型对象给unqiue_ptr的构造函数
// 销毁p所指的对象
// 实例化DebugDelete::operator()<int>(int *)
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
unique_ptr<string, DebugDelete> sp(new string, DebugDelete());
  • 对于类模板,我们也可以为其定义成员模板,在此情况下,类和成员各自有自己的。独立的模板参数
template <typename T> class Blob
{
    template <typename It> Blob(It b, It e);
};
  • 与类模板的普通成员不同,成员模板是函数模板,当我们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表,类模板的参数列表在前, 后跟着成员自己的模板参数列表
template <typename T>
template <typename It>
Blob<T>::Blob(It b, It e):
data(std::make_shared<std::vector<T>>(b,e)){}
  • 为了实例化一个类模板的成员模板,我们必须同时提供类和函数模板的实参,与往常一样,在哪个对象上调用成员模板,编译器就根据该对象类型推断类模板参数的实参,与普通函数模板相同,编译器通常根据传递给成员模板的函数实参来推断它的模板实参。
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"};
// 实例化Blob<int>类以及接受两个int*参数的构造函数
Blob<int> a1(begin(ia),end(ia));
Blob<int> a2(vi.begin(),vi.end());
Blib<string> a3(w.begin(),w.end());

控制实例化

  • 当模板被使用时才进行实例化,这一特性意味着,相同的实例可能出现在多个对象文件中, 当两个或多个独立编译的头文件使用相同的模板并提供相同的模板参数时,每个文件都有该模板的一个实例。在大系统中,这种额外开销会非常严重,在新标准中,我们可以通过显式实例化来避免这种开销
extern template declaration; // 实例化声明
template declaration; // 实例化定义

extern template class Blob<string>; // 声明
template int compare(const int&,const int &); // 定义
  • 当编译器遇到extern模板声明时,它不会再本文件中生成实例化代码,将一个实例化声明为extern就表示承诺在其他位置有该实例化的一个非extern声明(定义)。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
  • 由于编译器在使用一个模板时自动对其实例化, 所以声明应该出现在本文件的任何使用用此实例化版本的代码之前
// 这些模板类型必须在程序的其他位置进行初始化
extern template class Blob<string>;
extern template int compare(const int&,const int&);
Blob<string> sa1,sa2; // 实例化出现在其他位置
// Blob<int>及其接受initializer_list的构造函数在本文件中实例化
Blob<int> a1 = {0,1,2,3,4,5,6,7,8,9};
Blob<int> a2(a1); // 拷贝构造函数在本文件中实例化
int i = compare(a1[0],a2[0]); // 实例化出现在其他位置
template int compare(const int &,const int&);
template class Blob<string>; // 实例化类模板的所有成员
  • 一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数,当编译器遇到一个实例化定义时,它不知道程序使用那些成员函数,因此,与处理类模板的蹼泳实例化不同,编译器会实例化该类的所有成员,即使我们不使用某个成员,也会被实例化,因此我们用来显式实例化的一个类模板的类型,必须能用于模板的所有成员

效率与灵活性

  • shared_ptr的生存期中可以随时改变删除器类型,只要创建或reset指针时传递给他一个可调用对象即可。unique_ptr必须在定义时以显式模板实参的形式提供删除器类型。
  • 在运行时绑定删除器
  • shared_ptr可能将管理的指针和删除器指针分别保存两个成员中,析构时有类似这样的语句
del ? del(p) : delete p; // del(p)需要运行时跳转到del的地址
  • 在编译时绑定删除器
  • unique_ptr可能工作方式是由两个模板参数,一个表示所管理的指针,另一个表示删除器的类型,删除器直接保存到unqiue_ptr对象中
// del在编译时绑定,直接使用调用实例化的删除器
del(p); // 无运行时额外开销

模板实参推断

从函数实参来确定模板实参的过程被称为模板实参推断

类型转换与模板类型参数

  • 如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。只有有限的几种类型转换会自动应用于这些实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例
  • 与往常一样,顶层const无论是在形参中还是在实参中,都会被忽略。在其他类型转换中,能在调用中应用于函数模板的包括两项:const转换:将一个非const对象的引用或指针传递给一个const的引用或指针形参;数组或函数指针的转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针,类似的,一个函数实参可以转换为一个该函数类型的指针
  • 其他类型转换,如算术转换,派生类向基类的转换以及用户定义的转换,都不能应用于函数模板
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);const被忽略
fref(s1, s2); // 调用fref(const string&, const string&)
int a[10], b[42];
fobj(a, b); // 调用f(int*, int*);
ferf(a, b); // 错误,数组类型不匹配
long lng;
conpare(lng, 1024); // 不能实例化compare(long, int);

// 实参类型可以不同,但必须兼容
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); // 调用flexibleCompare(long, int)

函数模板显式实参

// 编译器不能推断T1,它未出现在函数列表中
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
// 在本例中,没有任何函数实参的类型可以用来推断T1的类型,每次调用sum时调用者都要为T1提供一个显式模板实参
auto val3 = sum<long long>(i, lng);

// 糟糕的设计,用户必须指定所有三个模板参数
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1);
// 错误,不能推断前几个模板参数
auto val3 = alternative_sum<long long>(i, lng);
// 正确,显式指定三个参数
auto val2 = alternative_sum<long long, int, long>(i, lng);

long lng;
compare(lng, 1024); // 错误:模板参数不匹配
compare<long>(lng, 1024); // 正确,实例化compare(long, long)
compare<int>(lng, 1024); // 正确,实例化compare(int, int),实现类型转换

尾置返回类型与类型转换

  • 我们可能希望编写一个函数,接受一对表示序列的迭代器返回序列中一个元素的引用,但是我们并不知道结果的类型,但知道所需的类型是所处理的序列的元素类型。我们可以用decltype(*beg)来获取元素的类型,但是,在编译器遇到参数列表之前,beg是不存在的,所以,我们必须使用尾置返回类型
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
    return *beg;
}
  • 返回一个元素的值,并非引用。为了获取元素类型,我们可以使用标准库的类型转换模板。这些模板定义在头文件type_traits中。
  • 在本例中,我们可以使用remove_reference来获得元素类型。remove_reference模板有一个模板类型参数和一个名为type的类型成员。如果我们用一个引用类型实例化remove_reference,则type将表示被引用 的类型。
remove_reference<decltype(*beg)>::type;
template <typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
    // 处理序列
    return *beg;
}
对Mod<T>,其中Mod为 若T为 则Mod<T>::type为
remove_reference X& 或 X&& X
add_const X& const X 或函数为T,否则 consf T
add_lvalue_reference X&&
否则
X&
T&
add_rvalue_reference X& 或X&&
否则
T
T&&
remove_pointer X* X
add_pointer X& X&&
否则
X*
T*
make_signed unsigned X X
make_unsigned 带符号类型 unsigned X
remove_extent X[n] X
remove_all_extents X[n1][n2] X

函数指针和实参推断

  • 当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参
template <typename T> int compare(const T&, const T&);
// pf1指向实例int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
  • pf1中的参数类型决定了T的模板实参的类型。如果不能从函数指针类型中确定模板实参,则产生错误
// func的重载版本,每个版本接受一个不同的函数指针类型
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare); // 错误,使用哪个compare呢
  • 这段代码的问题在于,通过func的参数类型无法确定模板实参的唯一类型,对于func的调用既可以接受int版本,又可以接受string版本,所以导致编译失败
  • 我们可以通过使用显式模板来消除func调用的歧义
func(compare<int>);

模板实参推断和引用

template <typename T> void f1(T&); // 实参必须是一个左值
f1(i); // T:int
f1(ci); // T:const int
f1(5); // 错误

template <typename T> void f2(const T&);
// 都被推断为const int&,f2 de const和实参中的const无关
f2(i);
f2(ci);
f2(5); // const&可以绑定右值

template <typename T> void f3(T&&);
f3(42);
  • 我们可能认为f3(i)这样调用是不合法的,毕竟i是一个左值,我们通常不能将一个右值引用绑定到左值上,但是C++在正常的绑定规则之外定义了两个例外规则,允许这种绑定。这是move这种标准库设施正确工作的基础
  • 第一个例外规则影响右值引用参数推断如何进行。当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断模板类型参数为实参的左值引用类型。因此f3(i)调用将T推断为int&
  • T推断为int&看起来意味着f3的函数参数应该是一个类型int& 的右值引用。通常我们不能定义一个引用的引用。但是,通过类型别名或通过模板类型参数间接定义是可以的
  • 在这种情况下,我们可以使用第二个例外绑定规则:如果我们间接创建一个引用的引用,则这些引用产生了折叠,除一种情况外,其他所有情况都会折叠为左值引用。只有右值引用的右值引用才会折叠为右值引用
  • X& & 、X& &&和X&& &折叠成类型X&
  • X&& && 折叠为X&&
  • 这两个规则导致了两个重要结果:如果一个函数参数是一个指向模板类型参数的右值引用,则它可以被绑定到一个左值;且如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个普通的左值引用参数
  • 另外值得注意的是,我们可以将任意类型的实参传递给T&& 类型的函数参数,对于这种类型的参数,我们可以传递右值可以传递左值
  • 模板参数可以推断为一个引用类型可能对模板内代码产生巨大影响
template <typename T> void f3(T&& val)
{
    T t = val; // 拷贝还是绑定一个引用
    t = fcn(t); // 赋值只改变t还是既改变t又改变val
    if(val == t){} // 若T是引用类型,则一直为true
}
  • 处理这种情况使用类型转换类可能会有帮助。但是实际中右值引用通常用于两种情况:模板转发其实参 或 模板被重载
  • 目前应该注意的是,使用右值引用的函数模板通常使用重载
template <typename T> void f(T&&); // 绑定非const右值
template <typename T> void f(const T&); // 左值和const右值

理解std::move

// 在返回类型和类型转换中也要用到typename
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    return static_cast<typename remove_reference<T>::type&&>(t);
}
  • 首先,move的函数参数T&&是一个指向模板类型参数的右值引用,通过引用折叠,此参数可以与任何类型的实参匹配。

在std::move(string(“bye”))中

  • 推断出T的类型为string
  • remove_reference用string进行实例化
  • remove_reference<string>的type成员是string
  • move的返回类型是string&&
  • move的函数参数t的类型为string&&
  • 实例化为:string&& move(string &&t)
  • 函数体返回static_cast<string&&>(t)。什么也不做

std::move(s1);

  • 推断出T的类型是string&
  • remove_reference用string&进行实例化
  • type成员是string
  • move返回类型是string&&
  • move的函数参数t实例化为string& &&,折叠为string&
  • 实例化为:string&& move(string &t)
  • static_cast将左值引用转换为右值引用
  • 通常,static_cast只能用于其他合法类型转换,但是有一条针对右值引用的特许规则,虽然不能隐式将左值转换为右值引用,但是可以显式转换

转发

  • 某些函数需要将其一个或多个实参连同类型,不变地转发给其他函数,在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是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)
{
    cout << v1 << " " << ++v2 << endl;
}
  • f改变了绑定到v2的实参的值。但是,如果通过flip1调用f,f所做的改变就不会影响实参,问题在于调用flip1(f,j,42)时,j是一个int,不是int&,flip1实例化为void flip1(void(*fcn)(int,int &),int t1, int t2)
  • j的值被拷贝到t1中,f的引用绑定到t1,而不是j,从而不会影响j
  • 通过一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。而是用引用参数使得我们可以保持const属性,因为在引用类型中const都是底层的。如果我们将函数参数定义为T1&& T2&&,通过引用折叠就可以保持翻转实参的左右值属性
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
    f(t2, t1);
}
  • 这个版本解决了一半问题,它对于接受一个左值引用的函数工作的很好,但是不能用于接受右值引用参数的函数,例如:
void g(int &&i, int &j)
{
    cout << i << " " << j << endl;
}
flip2(g, i, 42); // 错误,不能从一个左值实例化int&&
  • 传递给g的将是flip2中t2参数,与变量一样,它是左值表达式。因此flip2传递给g的右值引用参数一个左值
  • 我们可以使用一个名为forward的新标准库设施来传递flip2的参数,它能保持原始实参的类型。定义在utility头文件中。必须通过显式模板实参调用。返回显式实参类型的右值引用。forward<T>返回T&&
template <typename Type> intermediary(Type &&arg)
{
    finalFcn(std::forward<Type>(arg));
    // ...
}
  • 本例中我们使用Type作为forward的显式模板实参类型,它是从arg推断出来的。由于arg是一个模板类型参数的右值引用,Type将表示传递给arg的实参的所有类型信息。
  • 如果实参是一个右值, 则Type是一个普通类型,forward<Type>返回Type&&。
  • 如果实参是一个左值,通过引用折叠,Type本身是一个左值引用类型。再次对forward<Type>的返回类型进行引用折叠,返回一个左值引用类型
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的一个副本
}

// 打印指针的值,后跟着指针所指的对象
// 注意:不能用于char*
template <typename T> string debug_rep(T *p)
{
    ostringstream ret;
    ret << "pointer: " << p;
    if(p)
        ret << " " << debug_rep(*p);
    else
        ret << " null pointer";
    return ret.str();
}

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

cout << debug_rep(&s) << endl;
  • 如果用一个指针调用debug_rep,两个函数都可以生成可行的实例:
  • debug_rep(const string*&)
  • debug_rep(string*)
  • 第二个版本debug_rep的实例是此调用的精确匹配,第一个版本的实例需要进行普通指针到const指针的转换。
// 考虑另一个调用
const string *sp = &s;
cout << debug_rep(sp) << endl;
  • 两个模板都是可行,且都是精确匹配
  • debug_rep(const string*&)
  • debug_rep(const string*)
  • 在此情况下,正常函数匹配规则无法区分这两个函数,但是根据重载函数的特殊规则,此调用被解析为debug_rep(T*),更特例化的版本
  • 模板debug_rep(const T&)本质上可以用于任何类型,包括指针类型,此模板更通用,后者只能用于指针类型
// 打印双引号包围的string
string debug_rep(const string &s)
{
    return '"' + s + '"';
}
string s("hi");
cout << debug_rep(s) << endl;
  • 有两个同样好的可行函数
  • debug_rep<string>(const string&)
  • debug_rep(const string&)
  • 当存在多个同样好的函数模板时,编译器选择最特例化的版本,非模板函数比函数模板更好
// 考虑这个调用
cout << debug_rep("hi world!") << endl;
  • 有三个版本是可行的
  • debug_rep(const T&), T -> char[10]
  • debug_rep(T*) T -> const char
  • debug_rep(const string&)从const char* -> string
  • 两个模板都提供精确匹配——第二个模板需要进行一次数组到指针的转换,而对于函数匹配来说,这种转换被认为是精确匹配。非模板版本是可行的,但是需要进行一次类型转换,因此它不是精确匹配。与之前一样T*版本更特例化,编译器选择它
  • 如果我们希望将字符指针按string处理,可以定义另外两个模板重载版本
string debug_rep(char *p)
{
    return debug_rep(string(p));
}
string debug_rep(const char *p)
{
    return debug_rep(string(p));
}
  • 值得注意的是,为了使char*版本的debug_rep正确工作,在定义此版本时,debug_rep(const 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));
}
  • 通常,如果使用一个忘记声明的函数,代码会编译失败,但是对于重载函数模板的函数而言,则不是这样。如果编译器可以从模板实例化出与调用匹配的版本。

可变参数模板

  • 一个可变参数模板就是一个接受可变数目参数的模板函数或模板类,可变数目的参数被称为参数包。存在两种参数包:模板参数包表示0或多个模板参数,函数参数包,表示0个或多个函数参数
  • 在一个模板参数列表中,class…或typename…指出接下来的参数表示0个或多个类型的列表,一个类型名后面跟一个省略号表示0个或多个给定类型的非类型参数的列表,在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包
// Args是一个模板参数包,rest是一个函数参数包
// Args表示0个或多个模板类型参数
// rest表示0个或多个函数参数
template <typename T, typename... Args>
void foo(const T &t, const Args&... rest);
  • 声明了foo是一个可变参数函数模板,它有一个名为T的类型参数,和一个名为Args的模板参数包。这个包表示0个或多个额外的类型参数,foo的函数参数列表包含一个const&类型的参数,指向T的类型,还包含一个名为rest的函数参数包,此包表示0个或多个函数参数
  • 与往常一样,编译器从函数的实参推断模板参数类型,对于一个可变参数模板,编译器还会推断包中参数的数目
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);
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]&);

template <typename ... Args> void g(Args ... args)
{
    cout << sizeof...(Args) << endl;
    cout << sizeof...(args) << endl;
}

编写可变参数函数模板

  • 我们可以使用一个initializer_list来定义一个可接受可变数目实参的函数,但是,所有实参必须具有相同的类型。当我们不知道想要处理什么类型的实参和数目时,可变参数函数是很有用的
  • 可变参数函数通常是递归的,第一个调用处理包中的第一个实参,然后用剩余实参调用自身。我们的print函数也是这样的模式,每次递归调用将第二个实参打印到第一个实参表示的流中,为了终止递归,我们还需要定义一个非可变参数的print函数,它接受一个流和一个对象
template<typename T>
ostream &print(ostream &os, const T &t)
{
    return os << t;
}
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest)
{
    os << t << ", ";
    return print(os, rest...);
}
  • return print(os, rest…);
  • 我们的可变参数版本的print函数接受三个参数:一个ostream& 一个const T& 和一个参数包,而此调用只传递了两个实参,其结果是rest中的第一个实参被绑定到t,剩余实参形参下一个print调用的参数包。因此,在每个调用中,包中的第一个实参被移除,成为绑定到t的实参
  • 当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中,否则,可变参数版本会无限递归

包扩展

  • 对于一个参数包,除了获取其大小外,我们能对它做的唯一的事情就是扩展,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号来触发扩展操作
  • 在上面的print函数中有两个扩展
  • 第一个扩展操作扩展模板参数包,为print生成函数参数列表。第二个扩展操作出现在对print的调用中,此模式为print调用生成实参列表
  • 对Args的扩展中,编译器将模式const Args&应用到模板参数包Args中的每个元素。因此,此模式的扩展结果是一个逗号分个的0个或多个类型的列表,每个类型都形如const type&
  • print(cout, i, s, 42)
  • 实例化为:ostream &print(ostream&, const int&, const string&, const int&);
  • print中的函数参数包扩展仅仅将包扩展为其构成元素,C++语言还允许更复杂的扩展模式。例如,我们可以编写第二个可变参数函数,对每个实参调用debug_rep,然后调用print打印结果
// 在print调用中对每个实参调用debug_rep
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
    return print(os, debug_rep(rest)...);
}
  • 这个print调用使用了模式debug_rep(rest)。此模式表示我们希望对函数参数包rest中的每个元素调用debug_rep。扩展 结果是一个逗号分隔的debug_rep调用列表
  • errorMsg(cerr, fcnName, code.num(), otherData, “other”, item);
  • print(cerr, debug_rep(fcnName), debug_rep(code.num()), debug_rep(otherData), debug_rep(“other”), debug_rep(item));
  • 与之相对,下面的模式会编译失败
  • print(os, debug_rep(rest…));

转发参数包

  • 如我们所见,保持类型信息是一个两阶段的过程,首先,为了保持实参中的类型信息,必须将emplace_back的函数参数定义为模板类型参数的右值引用
class StrVec
{
    public:
    template <class... Args> void emplace_back(Args&&...);
};
  • 其次,当emplace_back将这些实参传递给construct时,我们必须使用forward来保持实参的原始类型
template <class... Args>
inline
void StrVec::emplace_back(Args&&... args)
{
    chk_n_alloc();
    alloc.construct(first_free++, std::forward<Args>(args)...);
}
  • 它既扩展了模板参数包Args,也扩展了函数参数包args,此模式生成如下形式的元素
  • std::forward<Ti>(ti)
  • 可变参数函数通常将它们的参数转发给其他函数,这种函数通常具有与我们的emplace_back函数一样的形式。
template<typename... Args>
void fun(Args&&... args)
{
    work(std::forward<Args>(args)...);
}

模板特例化

// 第一个版本,比较任意两个类型
template <typename T> int compare(const T&, const T&);
// 处理字符串字面常量
template <size_t N, size_t M>
int compare(const char(&)[N], const char(&)[M]);
  • 只有当我们传递给compare一个字符串字面常量或一个数组时,编译器才会调用第二个非类型模板参数的版本,如果传递给它字符指针,就会调用第一个版本。因为无法将指针转换为数组的引用
  • 为了处理字符指针,可以为第一个版本的compare定义一个模板特例化版本。一个特例化版本就是模板的一个独立的定义,在其中一个或或多个模板参数被指定为特定的类型
  • 当我们特例化一个函数模板时,必须为原模板中的每一个模板参数都提供实参,为了指出我们正在实例化一个模板,应使用关键字template后跟一个<>。指出我们将为所有模板参数提供实参
template <>
int compare(const char* const &p1, const char* const &p2)
{
    return strcmp(p1, p2);
}
  • 我们希望定义此函数的一个特例化版本,其中T为const char*,我们的函数要求一个指向此类型const版本的引用。一个指针类型的const版本是一个常量指针而不是指向const类型的指针。我们需要在特例化版本中使用的类型是const char* const &,一个指向const char的const指针的引用
  • 特例化的本质是实例化一个模板,而不是重载函数,因此,特例化不影响函数匹配
  • 模板及其特例化版本应该声明在同一个头文件中,所有同名模板的声明应该放在前面,然后是这些模板的特例化版本
  • 除了特例化函数模板,我们还可以特例化类模板。例如
  • 一个特例化hash类必须定义:
  • 一个重载的调用运算符,接受一个容器关键字类型的对象,返回一个size_t
  • 两个类型成员,result_type和argument_type,分别调用运算符的返回类型和参数类型
  • 默认构造函数和拷贝赋值运算符
  • 我们可以向命名空间中添加成员,首先需要打开命名空间
namespace 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);
    }
}
  • 在本例中,我们将定义一个好的哈希函数的工作交给了标准库,对每个数据成员都生成一个哈希值进行异或操作,形成给定的对象的完整的哈希值
  • 假定我们的特例化版本在本作用域中,当将Sales_data作为容器的关键字类型时,编译器会自动使用此特例化版本
// 使用hash<Sales_data>和Sales_data中的operator==
unordered_multiset<Sales_data> SDset;
  • 与函数模板不同,类模板的特例化不必为所有模板参数提供实参,我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性,一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参
// 原始的通用的版本
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;};
  • 由于一个部分特例化版本本质是一个模板,与往常一样,我们首先定义模板参数,类似其他任何其他特例化版本,部分特例化版本的名字与原模板相同。对每个未完全确定类型的模板参数,在特例化版本的模板的模板参数列表中都有一项与之对应。在类名之后,我们为要特例化的模板参数指定实参,这些实参列于模板名之后的尖括号中。这些实参与原始模板中的参数按位置对应。
  • 部分特例化版本的模板参数列表是原始模板参数列表的一个子集或者是一个特例化版本。在本例中,特例化版本的模板参数的数目与原始模板相同,但是类型不同,两个特例化版本分别用于左值引用和右值引用
  • 我们可以只特例化特定成员函数而不是特例化整个模板
template <typename T> struct Foo
{
    Foo(const T &t = T()):mem(t){}
    void Bar(){}
    T mem;
};
template<>
void Foo<int>::Bar()
{
    // 进行应用于int的特例化处理
}

术语表

  • 类模板
  • 默认模板实参
  • 显式实例化
  • 显式模板实参
  • 函数参数包
  • 函数模板
  • 实例化
  • 实例
  • 成员模板
  • 非类型参数
  • 包扩展
  • 参数包
  • 部分特例化
  • 模式
  • 模板实参
  • 模板实参推断
  • 模板参数
  • 模板参数列表
  • 模板参数包
  • 模板特例化
  • 类型参数
  • 类型转换
  • 可变参数模板
上一篇:C Primer Plus(第六版)第八章 编程练习答案


下一篇:《C++primer》学习:第一章(笔记及程序设计练习)