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

16 模板与泛型编程

OOP,能处理类型在程序运行之前都未知的情况;泛型编程,在编译时能获取类型。
模板是泛型编程的基础。本章学习如何定义自己的模板。

16.1 定义模板

问题引出:假设希望编写一个函数来比较2个值,并指出第一个值是<, > or == 第二个值。实际编程中,可能想要定义多个重载函数,每个函数比较一种给定类型的值。这样就会写很多函数体一样的函数,而仅仅是函数类型不同,很繁琐。我们使用函数模板解决这个问题。

// 下面2个函数用于比较v1和v2大小,仅仅是函数参数类型不一样,函数体完全一样
// string版本
int compare(const string &v1, const string &v2) {
    if (v1 < v2) return -1;
    else if(v1 > v2) return 1;
    return 0;
}
// double版本
int compare(const double&v1, const double&v2) {
    if (v1 < v2) return -1;
    else if(v1 > v2) return 1;
    return 0;
}

16.1.1 函数模板

以形如template 开始,为函数定义类型参数,可以实例化出特定函数的模板,就是函数模板。
类型参数T,可以看作类型说明符,作为函数返回值类型或者形参类型。会在调用函数时,编译器利用实参类型推断出T代表的类型。

什么叫(函数模板)实例化?

当调用一个函数时,编译器通常用函数实参来推断模板实参,用此函数实参类型代替模板实参创建出一个新的“实例”,即一个可调用的函数,这个过程叫实例化(instantiate)函数模板。该函数是得到的一个特定版本的函数。
编译器生成的函数版本,通常称为模板的实例。

// 定义compare的函数模板
// compare声明了类型为T的类型参数
// template关键字,<typename T>是模板参数列表,typename和class关键字等价,都可以使用,T是模板参数,以逗号分隔其他模板参数
template <typename T> 
int compare(const T &v1, const T &v2) {
    if (v1 < v2) return -1;
    else if (v1 > v2) return 1;
    return 0;
}

// 调用模板,将模板实参绑定到模板参数(T)上
// 调用函数模板时,编译器根据函数实参(1,0)来推断模板实参
cout << compare(1, 0) << endl;  // 推断T为int

// 编译器会根据调用情况,推断出T为int,从而生成一个compare版本,T被替换为int
int compare(const int &v1, const int &v2) {
    if (v1 < v2) return -1;
    else if (v1 > v2) return 1;
    return 0;
}

模板类型参数

类型参数可以看做类型说明符,像内置类型或类类型说明符一样使用,单仅限于定义模板的函数返回类型、参数类型、函数体内变量声明、类型转换。
类型参数前必须使用关键字typename或class,两者等价,可互换。(仅限于模板参数列表中)

template <typename T> T foo(T *p) {
    T tmp = *p; // tmp类型T,是指针p指向的类型
    // ...
    return tmp;
}

// 错误使用示例
template <typename T, U> T calc(const T&, const U&); // U 之前必须加typename或class
// 正确
template <typename T, class U> T calc(const T&, const U&); 

非类型模板参数

非类型参数表示一个值,而非一个类型。通过一个特定类型名而非(typename/class)来指定非类型参数。
当一个模板实例化时,非类型参数被用户提供的,或编译器推断出的值所代替。这些值必须是常量表达式。

注意:

  • 绑定到非类型整型参数的实参必须是一个常量表达式;
  • 绑定到指针或引用非类型参数的实参必须具有静态生存期;
template <unsinged N, unsigned M> // N, M是非类型整型参数
int compare(const char &(p1)[N], const char &(p2)[M]) {
    return strcmp(p1, p2);
}

// 调用compare时,编译器会用字面量大小来替代非类型参数N和M
compare("hi", "mom"); // N = 3, M = 4,注意编译器会自动在字符串末尾添加"\0"作为终结符

inline和constexpr的函数模板

声明inline或constexpr的函数模板,inline/constexpr说明符要放在模板参数列表之后,返回类型之前:

// 正确,inline放在template模板参数之后,返回值类型之前
template <typename T> inline T min(const T&, const T&);
// 错误,inline放到了template之前
inline template <typename T> T min(const T&, const T&);

编写类型无关的代码

前面compare函数,说明了编写泛型代码的2个重要原则:

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

但是,代码如果只使用了<运算符,就降低了compare对要处理类型的要求,从而不必支持>。
如果真的关心类型无关和可移植性,可能需要用到less来定义compare函数。
TIPS:less<T>是STL预定义的模板函数(头文件 ),用于比较2个元素大小,通常只用到<运算符。如sort库函数就经常用到less<T>greater<T>

// 实际上less函数也用到了<,并没有起到更良好定义的作用
template <typename T> int compare(const T &v1, const T&v2) {
    if (less<T>()(v1, v2)) return -1; // <=> if(v1 < v2)
    else if(less<T>()(v2, v1)) return 1;
    return 0;
}

模板编译

编译器在模板定义时,不生成代码。只有实例化出模板的一个特定版本时,编译器才会生成代码。

函数模板和类模板成员函数的定义通常放在头文件中。

16.1.2 类模板

可以实例化出特定类的模板,叫类模板。
类模板是用来生成类的蓝图的。与函数模板的区别是,编译器不能为类模板推断模板参数类型。

template <typename T> class Blob { // 类型为T的模板类型参数
public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_type;
    // 构造函数
    Blob();
    Blob(std::initializer_list<T> il);

    // Blob中的元素数目
    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));}
    void pop_back();

    // 元素访问
    T& back();
    T& operator[](size_type i);

private:
    std::shared_ptr<std::vector<T>> data;
    // 若data[i]无效,则抛出msg异常信息
    void check(size_type i, const std::string &msg) const;
};

实例化类模板

要使用类模板,必须提供额外信息,即显示模板实参列表,绑定到模板参数。编译器可以用这些模板实参实例化出特定的类。

一个类模板的每个实例都是一个独立的类,比如Blob与Blob没有任何关联,也不会对任何其他Blob类型的成员有特殊访问权限。

// 使用特定类型版本的Blob(即Blob<int>),必须提供元素类型
Blob<int> ia; // 构建空Blob<int>
Blob<int> ia2 = {0,1,2,3,4}; // 构建包含5个元素的Blob<int>

// 使用Blob<string> 版本
Blob<string> names;
// 使用Blob<double> 版本
Blob<double> prices;

编译器实例化出一个与下面定义等价的类:

// 注意:所有模板参数T都被编译器根据显式模板实参,替换为对应的类型
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;
}

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

类模板的名字不是一个类型名。类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。
也就是说,template class Blob{...} 这里模板名称Blob不是一个类型名,而模板参数T当做被使用模板的实参。

简而言之,就是类模板参数T,可以在类内部成员定义时使用,而T所代表的类型取决于实例化Blob传入的类型(xxx)。

// data定义,使用了Blob的类型参数T,来声明data是一个share_ptr的实例
std::shared_ptr<std::vector<T>> data;

// 实例化特定类型Blob<string>后,data成为
shared_ptr<vector<string>> data;

类模板的成员函数

类模板的成员函数是一个普遍函数,每个实例化的类,都有自己版本的成员函数。
如check, back, operator[]

template<typename T>
void Blob<T>::check(Blob::size_type i, const std::string &msg) const { // 检查当前位置i是否合法
    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[](Blob::size_type i) {
    // 如果i太大,check抛出异常,阻止访问不存在的元素
    check(i, "subscripte out of range");
    // return data[i]; // 错误,data是一个指向vector<T>的shared_ptr,vector下标访问需要先解引用
    return (*data)[i];
}

template<typename T>
void Blob<T>::pop_back() { // 弹出末尾元素
    // 检查data指向的vector是否为空
    check(0, "pop_back on empty Blob");
    data->pop_back();
}

构造函数

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对象就能像下面这样构造
Blob<string> articles = {"a", "an", "the"};

类模板成员函数的实例化

默认情况下,类模板成员函数只有当程序用到它时才实例化。

类内、类外使用模板类名

类的作用域内,可以直接使用模板名而不必指定模板实参.。

// 注意模板名后面的类型参数列表<T>

// 类内可以使用简化名称
Blob &Blob(Blob &&); // 移动构造函数
Blob &operator++(); // 前置自增 <=> Blob<T> &operator()

// 类外定义成员时,不在类的作用域,要指出类型参数T
template <typename T>
Blob<T> Blob<T>::operator++(int);  // 后置自增

类模板和友元

当一个模板类包含一个友元声明时,类与友元各自是否模板无关?
如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。 如果友元自身是模板,类可以授权所有友元模板实例,也可以只授权给特定实例。

一对一友好关系
引用(类或资源)模板的一个特定实例
步骤:

  1. 声明模板自身;
  2. 在类内声明友元关系;
// 注意1对1友元关系中,友元声明和类模板本身不同之处

// 前置声明,在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>; // 每个Blob实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符
    freind bool operator==<T>(const Blob<T> &, const Blob<T> &);
    //其他成员定义
    ...
};

通用和特定的模板友好关系
一个类将另一个类声明为友元,情况分为两大类:
1.非模板类中,声明友元类:声明的类可以是模板类,也可以是非模板类(普通友元声明);
2.模板类中,声明友元类:声明的类可以模板类,也可以是非模板类;

// 前置声明,在C和C2中声明友元所需
template <typename T> class Pal; 
// 注意这里没有Pal2的前置声明

class C{ // C是一个普遍非模板类
    friend class Pal<C>; // (用类C)实例化的Pal是C的一个友元,1对1友元关系。

    template <typename T> friend class Pal2; // Pal2所有实例都是C的友元, 因为已经包含了模板参数列表, 不需前置声明
};

template <tyepname T>  class C2 { // C2是一个模板类
    friend class Pal<T>;  // C2的每个实例,将相同实例化的Pal声明为友元

    template <typename X> friend class Pal2; // Pal2的所有实例都是C2的友元,不需要前置声明。这里X代表Pal2使用的模板参数,跟C2使用的T不一样

    friend class Pal3; // Pal3是非模板类,是C2所有实例的友元。不需要前置声明
};

令模板自己的类型参数成为友元

模板类可以将自己的类型参数,声明为友元

template <typename T> class Bar {
    friend T; // 将类的访问权限,授予用来实例化的Bar类型 (模板类实例化后的类)
  // ...
};

模板类型别名

用typedef定义引用实例化的类的别名,用using定义引用模板类的别名。

typedef Blob<string> StrBlob;  // 正确,引用的是Blob<string>,属于模板的一个实例,StrBlob是Blob<string>的别名

typedef Blob<T> StrBlob;       // 错误,由于Blob<T>模板不是一个类型,不能用typedef引用一个模板类

template <typename T> using StrBlob = Blob<T>; // 正确,StrBlob是模板类的别名
StrBlob<int> b1;    // <=> Blob<int>
StrBlob<double> b2; // <=> Blob<double> 
StrBlob<string> b3; // <=> Blob<string>

类模板的static成员

所有实例化的类,都包含自己的static成员。

如下面的类模板,一个给定的实例化的类Foo,包含一个共同的static 成员。不同的实例化的类,包含不同的static成员。

template <typename T> class Foo {
public:
    static std::size_t count() { return ctr; }   // static函数成员
    // ...
private:
    static std::size_t ctr; // static数据成员
    // ...
};

16.1.3 模板参数

类似函数参数的名字,模板参数的名字只是一个符号,没有什么含义,T只是习惯上的命名。

模板参数与作用域

模板参数的作用域从声明之后,到模板声明/定义结束之前。而且,模板内不能重用模板参数名。

typedef double A;
template <typename A, typename B> void f(A a, B b) // 模板参数A,B的作用域从声明之后,到模板声明/定义结束之前
{ 
    A tmp = a; // 覆盖了typedef对A的定义,A代表的类型由函数模板实例化决定
    double B;  // 错误:模板参数名不能重用
};

模板声明

声明,但不定义模板,不过必须包含模板参数。声明和定义中的参数名称,不必相同。

// 声明,但不定义模板
template <typename T> int compare(const T&, const T&);
template <typename T> class Blob;

// 3个声明/定义都指向相同的函数模板
template <typename T> T calc(const T&, const T&); // 声明
template <typename U> U calc(const U&, const U&); // 声明

template <typename X> X calc(const X& a, const X& b) { // 定义
// ...
}

使用类的类型成员

如何通过类模板参数T,使用实例化之后T内定义的类型?
如果直接用作用域运算符(::)这样做,编译器无法判断是想使用T的静态成员value_type,还是想使用T内类型valuetype。

解决办法:通过typename显示告诉编译器,该名字是一个类型,而非static成员。
通知编译器一个名字表示类型时,只能用typename, 不能用class

// 声明类型的错误方式
T::value_type

// 声明类型的正确方式
template <typename T> // 这里typename <=> class
typename T::value_type top(const T& c) { // 注意这里typename T::value表明这是一个类型(而不是T的成员),无法用class替换
    if(!c.empty()) return c.back();
    else return typename T::value_type(); // 疑问:这里如果T::value_type表示类型,为何会带一个"()"? 答案是这里的 "类型+()",会调用默认构造函数(对类)或者内置的初始化方法(对内置类型,如int,初值一般为0)
};

默认模板实参

可以像指定函数默认实参一样,为模板参数提供默认实参。

template <typename T, typename F = less<T>> // F默认值是less<T>,一个模板类,重载了函数调用运算符(operator())
int compare(const T &v1, const T &v2, F f = F()) { // 这里F(),是相当于调用less<T>(),也就是less<T>的函数调用重载版本
    if (f(v1, v2)) {
        return -1;
    }
    if (f(v2, v1)) {
        return 1;
    }
    return 0;
}

// 调用函数模板实例
auto i = compare(0, 42); // i = -1

模板默认实参与类模板

类似于为函数模板指定默认模板实参,也可以为类模板指定默认模板实参。

template <class T = int> class Numbers { // T默认为int
public:
    Numbes(T v = 0) : val(v) { }
private:
    T val;
};

// 客户端, 定义模板类
Numbers<long double> lots_of_precision; // 指定模板类型参数
Numbers<> average_precision; // 空<>表示希望使用默认类型

16.1.4 成员模板

一个类(普通类或类模板)可以包含本身是模板的成员函数,这种成员函数称为成员模板(member template)
成员模板不能是virtual函数。why?
因为编译器要在编译期决定virtual table大小,就要知道一个class有多少virtual函数。一个成员模板的调用,就可能生成一个成员函数,因此,要在编译期知道一个class有多少成员模板的调用,几乎是不可能的。因此,禁止成员模板为virtual函数是更好的选择。

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

我们以定义一个类似于unique_ptr的类DebugDelete,重载函数调用运算符operator()。与unique_ptr默认删除器相比,DebugDelete会在删除器执行时打印一条信息。由于希望删除器适用于任何类型,故定义为成员模板:

class DebugDelete {
public:
    DebugDelete(std::ostream &s = std::cerr) : os(s) {}
    template <typename T> void operator()(T *p) const {
        os << "deleting unique_ptr" << endl;
        delete p;
    }
private:
    std::ostream &os;
};

// 客户端,使用DebugDelete管理对象
double *p = new double;
DebugDelete d;
d(p); // 调用DebugDelete::opeartor()(double *)释放p
int* ip = new int;
DebugDelete()(ip); // 在临时DebugDelete对象上调用operator()(int *)

也可以将DebugDelete作为函数对象,用作unique_ptr的删除器。

// 实例化DebugDelete::opeartor()<int>(int *)
unique_ptr<int, DebugDelete> p(new int, DebugDelete());

// 实例化DebugDelete::opeartor()<string>(string *)
unique_ptr<string, DebugDelete> sp(new string, DebugDelete());

// unique_ptr的析构函数会自动调用DebugDelete调用运算符
// DebugDelete成员模板实例化样例
// 下面的函数对程序员透明,由编译器生成
void DebugDelete::opeartor()(int *p) const { delete p; }
void DebugDelete::operator()(string *p) const { delete p; }

类模板的成员模板

类和成员都各有自己的独立模板参数。
如,将Blob类定义一个构造函数,参数为2个迭代器,表示要拷贝的元素范围。由于希望支持不同类型序列的迭代器,因此可以将构造函数定义为模板:

// 类的模板参数与构造函数成员的模板参数各不相同

// 类模板内定义成员模板
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 ai[] = {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>类及其接受2个int* 参数的构造函数
Blob<int> a1(begin(ia), end(ia));
// 实例化Blob<int>类及其接受2个vector<long>::iterator 参数的构造函数
Blob<int> a2(vi.begin(), vi.end());
// 实例化Blob<string>类及其接受2个list<const char*>::iterator 参数的构造函数
Blob<string> a3(w.begin(), w.end());

注:a2和a1共用实例化了的类Blob,但不共用同一个构造函数版本。

16.1.5 控制实例化

当模板被使用时才会进行实例化。而相同的实例可能出现在多个对象文件中,当多个独立编译的源文件使用相同的模板,并提供相同模板参数时,每个文件中就都会有该模板的一个实例。
这样,一旦系统较大,势必会造成多个文件实例化相同模板的开销严重。
C++11以后,可以通过extern表明显示实例化(explicit instantiation)来避免这种开销:

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

extern声明必须出现在任何使用此实例化版本的代码之前:

// Application.cc
// 实例化声明,定义在其他文件位置
extern template class Blob<string>;
extern template int compare(const int&, const int &);

Blob<string> sa1, sa2; // 实例化会出现在其他位置
Blob<int> a1 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // 接受initializer_list的构造函数在本文件中实例化
Blob<int> a2(a1); // 拷贝构造函数在本文件中实例化
int i = compare(a1[0], a2[0]); // 实例化出现在其他位置

// templateBuild.cc
// 实例化文件,为其他文件中声明为extern的类型和函数提供一个定义
template class Blob<string>;
template int compare(const int&, const int &);

注意:对每个实例化声明,在程序中某个位置必须有其显式的实例化定义。

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

一个类模板的实例化定义会实例化该模板的所有成员,包括inline成员函数。由于编译器遇到一个实例化定义时,并不了解程序使用哪些成员函数,与因此处理类模板的普通实例化不同,编译器会实例化该类所有成员模板,即使我们不使用某个成员。

16.1.6 效率与灵活性

STL智能指针shared_ptr与unique_ptr明显区别:
1)前者允许用户共享指针所有权;后者独占指针。
2)允许用户重载默认删除器的方式不同。

shared_ptr与unique_ptr不同,shared_ptr的删除器在创建或reset指针时传递给它一个可调用对象即可。unique_ptr是在定义时,以显式模板实参的形式提供删除器类型,同时在创建指针时传入一个可调用对象。在析构时,shared_ptr和unique_ptr调用删除器释放资源。

下面对如何绑定删除器为例,进行讲解。

在运行时绑定删除器

我们不知道shared_ptr通过一个指针或者一个封装了指针的类(如function)保存删除器,而不是通过成员对象保存,因为直到运行时才知道。在shared_ptr的运行期,我们可以随时修改其删除器类型:使用一种删除器构造shared_ptr后,随时调用reset修改为另一种删除器。

// 构建时传入一个删除器
shared_ptr<double> ps(new double, [](double *p){ delete p; });

// 构建后修改删除器
ps.reset(new double[3], []()(double *p){ delete[] p; };

在编译时绑定删除器

unique_ptr在编译时就传入其删除器,编译器知道删除器类型。运行时不可修改。
如果删除器是类似于DebugDelete之类的东西,调用可能会被编译为内联形式。

// 编译时绑定删除器
unique_ptr<double, DebugDelete> pu(new double, DebugDelete());

unique_ptr避免了调用删除器的运行时开销,效率更高;shared_ptr使用户可以在运行时绑定删除器,更方便。

16.2 模板实参推断

利用函数实参类型来确定模板实参的过程,被称为模板实参推断(template argument deduction)

16.2.1 类型转换与模板类型参数

调用函数模板时,编译器通常不对函数实参类型转换,而是生成一个新的模板实例。
顶层const无论在形参还是实参中,都会被忽略。

函数模板调用时,支持的2种隐式转换:
1)const转换:可以将非const对象的引用(或指针)传递给一个const对象的引用或指针;
2)数组或函数指针转换:如果函数形参不是引用类型,则可以对 类型为 “数组或函数类型” 的实参 实施正常的指针转换。一个数组类型的实参,可以转换为指向其首元素的指针(如int a[] => int *pa);一个函数实参可以转换为该函数类型的指针(如 int func(int) => typedef int (*FuncType)(int); FuncType p = func;, func可以转换为p )。

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&); 将s1转换为const是允许的

int a[10], b[10];
fobj(a, b); // 调用f(int*, int*). 数组类型 => 数组指针
fref(a, b); // 错误:数组类型不能匹配 引用类型

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

处理前面提到的2种类型的转换,其他情况下,传递给模板类型参数的实参必须具备相同类型。

// 函数模板要求使用相同实参类型
template <typename T>
int compare(const T& l, const T& r);

// 客户端调用
long lng;
compare(lng, 1024); // 错误:不能实例化compare(long, int)

// 函数模板支持不同类型实参,但必须兼容,否则v1和v2无法调用运算符<进行比较
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. 正确调用flexibleCompare(long, int)

正常类型转换应用于普通函数实参

函数模板也可以用普通类型定义的参数(非模板类型参数对应类型)。

#include <fstream>
#include <ostream>
using namespace std;

template <typename T>
ostream& print(ostream& os, const T& obj) // os是普通类型ostream&
{
    return os << obj;
}

// 客户端
print(cout, 42);
ofstream f("output.txt"); // 创建文件
print(f, 10); // 调用print,向文件写入内容

16.2.2 函数模板显式实参

某些情况下,编译器无法推断出模板实参的类型。
比如,

// 编译器无法推断出T1,因为T1位于函数返回值类型中,而没有出现在参数列表中
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);

解决办法:为T1提供一个显式模板实参(explicit template argument)

// T1显式指定,T2、T3由函数实参类型推断而来
auto val = sum<long long>(i, lng); // long long sum(int, long)

显式模板实参是按从左至右的顺序与对应的模板参数匹配。只有最右侧目标参数的类型能推断出来时,才可以省略。

// 糟糕的设计,用户必须指定3个模板参数
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T1, T2);

// OK. 用户必须按从左到右顺序显式指定模板实参:T1 -- longlong, T2 -- int, T3 -- long
auto val2 = alternative_sum<long long, int, long>(i, lng); // long alternative_sum(long long, int);
// 错误. 因为显式指定模板参数是按左到右顺序的,也就是T1--long long,而函数实参能推断出T1--int, T2 -- long,无法推断出T3
auto val3 = alternative_sum<long long>(i, lng);

正常类型转换应用于显式指定的参数

对于普通类型定义的函数参数,允许进行类型转换。

// compare函数模板要求2个实参类型相同
template <typename T> 
int compare(const T &v1, const T &v2) {
    if (v1 < v2) return -1;
    else if (v1 > v2) return 1;
    return 0;
}

long lng;
compare(lng, 1024); // 错误:2个模板参数不相同,lng是long,1024是int
compare<long>(lng, 1024); // OK. 正确实例化compare(long, long)
compare<int>(lng, 1024);  // OK. 正确实例化compare(int, int)

16.2.3 尾置返回类型与类型转换

当我们希望用户确定返回类型时,用显示模板实参的确很有效。然而,当我们并不知道函数准确的返回类型,只知道返回类型是由所处理的返回值决定的:

template <typename It>
??? &fcn(It beg, It end) // "???" 表示并不知道函数返回的准确类型
{
    return *beg; // 返回一个元素的引用
}

// 客户端是这样的
vector<int> vi = {1,2,3,4,5};
Blob<string> ca = {"hi", "bye"};
auto &i = fcn(vi.begin(), vi.end()); // fcn应该返回int&
auto &s = fcn(ca.begin(), ca.end()); // fcn应该返回string&

如何定义这样的函数模板?
使用尾置返回类型,用decltype(*beg)表示函数返回值类型

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

进行类型转换的标准库模板类

有时,通过迭代器解引用得到是元素的引用类型,而非元素的类型。那么,我们要如何获得元素的类型呢?
可以使用标准库的类型转换(type transformation)模板。定义在头文件<type_traits>中。
类型转换模板remove_reference,有一个模板类型参数和一个名为type的public static成员。如果实例化remove_reference<int&>,则type表示int;如果实例化remove_reference<string&>,那么type表示string。

例如,前一小节例子中,我们通过 decltype(beg)得到的元素的引用类型,而通过remove_reference<decltype(beg)>::type 可以得到元素的类型。

template <typename It>
auto fnc2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
    return *beg; // 返回一个元素的拷贝
}

其他类似的标准类型转换模板还有:add_const, add_lvalue_reference, add_value_reference, remove_pointer, make_signed, make_unsigned, remove_extent, remove_all_extents。

16.2.4 函数指针和实参推断

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

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

// 函数指针pf1类型为 int (*)(const int&, const int&);
int (*pf1)(const int&, const int&) = compare; // 用函数模板compare给函数指针pf1赋值,可以推断出模板参数T为int

当编译器无法推断模板参数时,就会报错。解决办法就是显示指出实例化哪个版本

// func的2个重载版本
// 参数是函数类型int(*)(const string&, const string&)
void func(int(*)(const string&, const string&));

// 参数是函数类型int(*)(const int&, const int&)
void func(int(*)(const int&, const int&));

// 错误:编译器无法推断出使用compare的哪个实例
func(compare);

// OK. 显式模板参数
func(compare<int>); // 传递int(*)(const int&, const int&)

16.2.5 模板实参推断和引用

【从函数调用推断模板参数】

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

当函数参数是模板类型参数的一个普通(左值)引用(形如T&)时,绑定规则告诉我们:只能传递给它一个左值(如,一个变量或一个返回引用类型的表达式)。

template <typename T>
void f1(T&); // 实参必须是一个左值

// 根据f1调用推断类型
f1(i); // i是一个int,模板参数类型T是int
f1(ci); // ci是一个const int,T是const int
f1(5); // 错误:传递给函数模板参数必须是&参数,5不能作为引用类型传入

如果一个函数参数类型是const T&,正常绑定规则告诉我们:可以传递给它任何类型的实参 ---- 一个对象(const或non-const)、一个临时对象、一个字面常量值。

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

// 根据f2调用推断类型
f2(i); // i是一个int,模板参数类型T是int
f2(ci); // ci是一个const int,T是const int
f2(5);  // OK:const &参数可以绑定到一个右值,T是int

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

当函数参数是一个右值引用(形如T&&)时,正常绑定规则告诉我们:可以传递给它一个右值。

template <typename T> void f3(T&& n);

f3(42); // 实参是一个int类型的右值;T是int

引用折叠和右值引用参数

C++在正常绑定规则之外,定义了2个例外规则,允许将一个int对象(左值)绑定到右值引用(如,int i,调用f3(i))。这2个例外规则是move这种标准库设施正确工作的基础。

2个例外规则:
1)右值引用的特殊类型推断规则 => 根据左值实参,推断模板参数T
第一个例外规则影响右值引用参数的推断如何进行。当我们将一个左值(如int i)传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型(int&)。因此,当我们调用f3(i)时,编译器推断T的类型为T&。T被推断为int&,那么f3参数n似乎是int&的右值引用(int& &&)。通常,我们不能直接定义一个引用的引用。如果定义了,参见例外规则2)。

2)引用折叠规则 => 根据折叠的引用,推断出参数n的类型
第二个例外绑定规则:如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。在新标准中,折叠规则扩展到右值引用。只在一种特殊情况下,引用会折叠成右值引用:右值引用的右值引用。i.e. 对于一个给定类型X:

  • X& &、X& &&和X&& &都折叠成类型X&;
  • 类型X&& &&折叠成X&&;

注意:引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数。

如果引用折叠规则和右值引用的特殊类型推断规则组合到一起,则意味着我们可以对一个左值调用f3(函数形参是右值类型)。当我们将一个左值传递给f3的(右值引用)函数参数时,编译器推断T为一个左值引用类型。

f3(i); // 实参是左值,T是int&,形参n是int&
f3(ci); // 实参是左值,T是const int&,形参n是const int&

根据例外规则1),f3(i)的推断结果:
void f3<int&>(int& && n);
根据例外规则2),f3(i)的折叠结果:
void f3<int&>(int &n);

从这2条规则,我们可以得到2个重要结果:

  • 如果一个函数形参是模板类型参数的右值引用(如T&& n),则它可以被绑定到一个左值;且
  • 如果实参是一个左值,则推断出的模板参数类型(如T)将是一个左值引用,而且函数形参(如n)将被实例化为一个普通左值引用参数(T& n)。

这2条规则暗示了,我们可以将任意类型的实参传递给T&&类型的函数参数。实参是左值时,函数形参将成为左值引用;实参是右值时,函数形参将成为右值引用。

编写接受右值引用参数的模板函数

模板参数可以推断为一个引用类型,见上一小节的例外规则1)。

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

分两种情况讨论:
1)当我们对一个右值调用f3时,如字面常量42,则T为int,形参n为int&&。此时,局部变量t为int,且通过拷贝参数n的值被初始化。当对t赋值时,形参n的值不变。

2)当我们对一个左值调用f3时,如int i对象,则T为int&,形参n为int&。此时,局部变量t为int&,其初始将绑定到n。当对t赋值时,形参n也随之改变。

原文链接: https://www.cnblogs.com/fortunely/p/14564383.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

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

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/402878

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月21日 上午11:15
下一篇 2023年4月21日 上午11:15

相关推荐