c++primer笔记十六、模板与泛型编程

模板就是一个创建类或函数的蓝图或者公式,是泛型编程的基础,如vector的泛型类型和find这样的泛型函数

16.1 定义模板

编写一个函数比较两个值

//如果相等返回0,小于返回-1,反则1
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 函数模板

模板定义以tempalte开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用< >包围
模板定义中,模板参数列表不能为空

template <typename T>
int compare(const T &v1, const T &v2)
{
    if  (v1 < v2) return -1;
    if  (v2 < v1) return 1;
    return 0;
}
实例化函数模板

编译器用函数实参来推断模板实参

cout << compare(1, 0) << endl;  //实参类型是int,编译器推断出模板实参为int,并绑定到模板参数T

这些编译器生成的版本通常被称为模板的实例

模板类型参数

一般可以将类型参数看作类型说明符,类型参数也可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换:

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

类型参数前必须使用关键字class或typename

template <typename T, class U> calc (const T&, const U&);   //无区别

typename更直观

非类型模板参数

还可以在模板定义非类型参数,表示一个值而非一个类型,通过特定的类型名来定义
当一个模板被实例化时,非类型参数被常量表达式所代替

//模板参数表示数组长度
template<unsigned N, unsighed M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
    return strcmp(p1, p2);
}
//调用时
compare("hi", "mom")

//使用字面常量的大小来代替N和M,从而实例化模板,即如下版本
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. 函数体中的条件判断仅仅使用<比较运算

模板程序应该尽量减少对实参类型的要求

模板编译

编译器遇到模板不生成代码,只有当实例化出模板的一个特定版本时,编译器才会生成代码。
为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义,因此与非模板不同,模板的头文件通常既包括声明也包括定义。

大多数编译错误在实例化期间报告

编译器通常在三个阶段报错:

  1. 编译模板本身时,一般检查语法错误
  2. 编译器遇到模板使用时,通常检查实参数目和类型是否匹配
  3. 模板实例化时,只有这个阶段才能发现类型相关的错误。依赖于编译器如何管理实例化,这类错误可能在链接时才报告
    保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任

16.1.2 类模板

类模板是用来生成类的蓝图的,编译器不能为类模板推断模板参数类型,必须用<>来提供额外信息

定义类模板

例:StrBlob的模板版本

template <typename T> class Blob{
public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_tpye;
    //构造函数
    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<int> ia;
Blob<int> ia2 = {0,1,2,3,4};

当编译器从Blob模板实例化出一个类时,它会重写Blob模板,将模板参数T的每个实例替换为给定的模板实参
对我们指定的每一种元素类型,编译器都生成一个不同的类:

Blob<string> names;
Blob<double> prices;

一个模板中的每个实例都形成一个独立的类

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

一个类模板中使用了另一个模板,通常将模板自己的参数当作被使用模板的实参。

std::shared_ptr<std::vector<T>> data;
//实例化一个特定类型如Blob<string>时,data变为
shared_prt<vector<string>>
类模板的成员函数

和其他类相同,可以在类模板内部或外部定义函数,且定义在内部的成员函数被隐式声明为内联函数
定义在类模板之外的成员函数必须以关键字template开始,后接类模板参数列表

//对于StrBlob的一个给定的成员函数
ret-type StrBlob::member-name(para-list)
//对应的Blob成员应该是
template <typename T>
ret-type Blob<T>::member-name(parm-list)
check和元素访问成员

首先定义check成员,检查一个给定的索引

template <typename T>
void Blob<T>::check(size_tpye i, const std::string &msg) const
{
    if (i >= data->size())
        throw std::out_of_range(msg);
}

下标运算符和back函数用模板参数指出返回类型

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> void Blob<T>::pop_back()
{
    check(0, "pop_back on empty Blob");
    data->pop_back();
}
Blob构造函数
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> squared = {0,1,2,3,4,5,6,7,8,9};
for (size_t i = 0; i != squares.size(); ++i)
    squares[i] = i*i;

如果一个成员函数没有被用到,则不会被实例化。因此即使某种类型不能完全符合模板操作的要求,但是也能使用该类型实例化类

在类代码内简化模板类名的使用

在类模板自己的作用域中,可以直接使用模板名而不提供实参

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;
};
在类模板外使用类模板名
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
    //此处无须检查,因为前置++时会检查
    BlobPtr ret = *this;
    ++*this;
    return ret;
}

返回类型位于类的作用域外,必须指出一个实例化的BlobPrt;函数体内已经进入类的作用域,ret无须重复模板实参

类模板和友元

当一个类包含一个友元声明,类与友元各自是否时模板时相互无关的。

一对一友好关系

一种常见形式是建立对应实例及其友元间的友好关系
例:Blob类应该将BlobPtr类和一个模板版本的Blob相等运算符定义为友元

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

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

Blob<char> ca;  //BlobPtr<char>和operator==<char>都是本对象友元
Blob<int> ia;   //BlobPtr<int>和operator==<int>都是本对象友元
通用和特定的模板友好关系

一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元:

//前置声明,在将模板的一个特定实例声明为友元时要用到
template <typename T> class Pal;
class C {   //C时一个普通的非模板类
    friedn class Pal<C>;    //用类C实例化的Pal是C的一个友元
    //Pal2的所有实例都是C的友元,这种情况不需要前置声明
    template <typename T> firend class Pal2;
};
template <typename T> class C2{ //C2本身是一个类模板
    //C2的每个实例将相同实例化的Pal声明为友元
    firedn class Pat<T>;    //Pal的模板声明必须在作用域之内
    //Pal2的所有实例都是C2的每个实例的友元,不需要前置声明
    template <typename X> friend class Pal2;
    //Pal3是一个非模板类,它是C2所有实例的友元
    friend class Pal3;  //不需要Pal3的前置声明
}

为了让所有实例称为友元,友元声明中必须使用与类模板本身不同的模板参数

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

新标准中可以将模板类型参数声明为友元

template <typename Type> class Bar{
friend Type;    //将访问权限授予用来实例化Bar的类型
}
模板类型别名

可以用typedef来引用实例化的类

typedef Blob<string> StrBlob;

不能定义一个typedef引用一个模板,但新标准允许为类模板定义一个类型别名
使用using

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

可以固定一个或多个模板参数:

template <typename T> using partNo = pari<T, unsigned>;
partNo<string> books;   //pair<string, unsigned>
partNo<Student> kids;   //pair<Student, unsigned>
类模板的static成员

类模板可以声明static成员

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

每个Foo的实例都有自己的static成员实例,所有Foo类型的对象共享相同的ctr对象和count函数

模板类的每个static数据成员必须有且仅有一个定义,但是类模板的每个实例都有一个独有的static对象。因此与定义模板的成员函数类似,我们将static数据成员也定义为模板

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

可以通过类类型对象来访问一个类模板的static成员,也可以使用作用域运算符直接访问成员

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

16.1.3 模板参数

类似函数参数,可以使用任何名字,通常命名为T

template <typename Foo> Foo calc(const Foo& a, const Foo&b)
{
    Foo tmp = a;
    //...
    return tmp;
}
模板参数与作用域

模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。模板参数会隐藏外层作用域中声明的相同的名字,但是模板内不能重用模板参数名

tpyedef double A;
template <typename A, tpyename B> void f(A a, B b)
{
    A tmp = a;
    double B;       //错误,重声明模板B
}
//一个参数名在一个特定模板参数列表只能出现一次
//错误
template <typename V, typename V>
模板声明

必须包含模板参数

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

与函数参数相同,声明中的模板参数名字不必与定义一致,但必须有相同的数量和种类的参数

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

使用类的类型成员

用::来访问static成员和类型成员,如string::size_type,编译器有string定义,从而知道size_tpye是一个类型

对于模板就很困难,如:

T::size_type * p;

是定义一个p的变量还是将一个size_type的static数据成员与p相乘
默认情况下,c++通过作用域运算符访问的名字不是类型。因此如果希望使用一个模板类型参数成员,必须显式地用typename告诉编译器

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

默认模板实参
//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;
}

模板添加了第二个类型参数F,表示可调用对象;并定义了一个新的函数参数f,绑定到一个可调用对象上
默认实参指出compare将使用标注库地less函数对象类。默认函数实参指出f僵尸类型F的一个默认初始化对象
当用户调用compare时,可以提供自己的比较操作

bool i = compare(0, 42);    //使用less
Sales_data item1(cin), item2(cin);
bool j = compare(item1, item2, 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;    //<>表示使用默认类型

16.1.4 成员模板

一个类(普通或模板)可以包含本身时模板的函数成员,这种成员称为成员模板,不能为虚函数

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

可以用这个类代替delete,顺便再打印

double *p = new double;
DebugDelete d;
d(p);   //释放p
int* ip = new int;
DebugDelete()(ip);  //在一个临时DebugDelete对象上调用operator()(int*)

可以将DebugDelete用作unique_ptr的删除器

//销毁p指向的对象
//实例化
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
//销毁sp指向的对象
//实例化DebugDelete::operator()<string>(string*)
unique_ptr<string, DebugDelete> sp(new string, DebugDelete());

这样其析构函数会调用DebugDelete

类模板的成员模板
template <typename T> class Blob {
    template <typename It> Bolb(It b, It e);
};

此构造函数有自己的模板类型参数It
在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供参数列表,类模板的参数列表在前,后跟成员自己的模板参数列表

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,2,3,4,5,7,8,9};
vector<long> vi = {0,1,23,4,5,6,7,8};
list<const char*>w = {"now", "is", "the", "time"};
//实例化Blob<int>类及其接受两个int*参数的构造函数
Blob<int> al(begin(ia), end(ia));
//实例化Blob<int>以及接受两个vector<long>::iterator的构造函数
Blob<int> a2(vi.begin(), vi.end());
//实例化Blob<int>以及接受两个list<const char*>::iterator的构造函数
Blob<string> a3(w.begin(), w.end());
控制实例化

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

额外开销可能会很大,通过显式实例化来避免

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

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

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

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

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

//Application.cc
//这些模板类型必须在程序其他位置进行实例化
extern template class Bolb<string>;
extern template int compare(const int&, const int&);
Blob<string> sa1, sa2;  //实例化会出现在其他位置
//Blob<int>及其接受initializer_list的构造函数在本文件中实例化
Blob<int> a1= {0,1,23,4,5,6};
Bolb<int> a2(a1);       //拷贝构造函数在本文件中实例化
int i = compare(a1[0], a2[0]);  //实例化出现在其他位置

Blob和compare的定义必须出现在程序的其他文件中:

//templateBuild.cc
//实例化文件必须为每个在其他文件中声明为extern的类型和函数提供一个(非extern)的定义
template int compare(const int&, const int&);   
template class Blob<string>;             //实例化类模板的所有成员

当编译器遇到一个实例化定义时,它为其生成代码。因此templateBuild.o会包含compare的int实例化版本的定义和Blob类的定义。当我们编译此程序时,必须将templateBuild.o和Application.o链接到一起

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

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

一个类模板的实例化定义会实例化所有成员,包括内联的成员函数。用来显式实例化一个类的模板类型,必须能用于模板的所有成员。

16.1.6 效率与灵活性

shared_ptr共享指针所有权,容易重载其删除器,在创建或reset指针时传递给它一个可调用对象即可
相反,删除器的类型时unique_ptr对象的类型的一部分,必须在定义unique_ptr时以显式模板实参的形式提供删除器类型,使用就比较负责

如何处理删除器的差异实际上就是这两个类功能的差异,这差异对性能有重要影响

在运行时绑定删除器

shared_ptr必须能直接访问其删除器,即删除器必须保存为一个指针或一个封装了指针的类。在一个shared_ptr生存期,可以随时用reset改变其删除器类型。通常类成员的类型在运行时是不能改变的,因此不能直接保存删除器。

假定shared_ptr将管理的指针保存在p,删除器通过del的成员来访问,则析构函数必须包含类似的语句:

//del的值只有运行时才知道;通过一个指针调用它
del ? del(p) : delete p; //del(p)需要运行时跳转到del的地址

由于删除器时间接保存的,调用del(p)需要一次运行时的跳转操作,转到del中保存的地址来执行对于的代码

在编译时绑定删除器

在unique_ptr中,删除器的类型时类类型的一部分,即unique_ptr有两个模板参数,一个表示指针,一个表示删除器的类型。因此在编译时知道删除器的类型,可以直接保存在unique_ptr对象中

//del在编译时绑定,直接调用实例化的删除器
del(p);

通过在编译时绑定删除器,避免了间接调用删除器运行时的开销;在运行时调用删除器,使用户重载删除器更为方便

16.2 模板实参推断

从函数实参来确定模板实参的过程称为模板实参推断,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数版本与给定的函数调用最匹配

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

如果一个函数形参类型使用了模板类型参数,则采用特使的初始化规则。只有有限的几种类型转换会自动地应用于这些实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例

顶层const无论在形参或实参都会忽略,在其他类型转换中,应用于函数模板的有:

  1. const转换:将一个非const对象的引用传递给一个const的引用形参
  2. 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针

例:

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);       //const被忽略
fref(s1, s2);       //将s1转换为const是允许的
int a[10], b[42];
fobj(a, b);     //调用f(int*, int*);
fref(a, b);     //错误,数组类型不匹配
使用相同模板参数类型的函数形参
long lng;
compare(lng, 1024)  //错误

第一个函数实参推断为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)

前提是定义了能比较这些类型的<

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

不涉及模板类型参数的类型可以正常转换为对应形参的类型:

template <typename T> ostream &print(ostream &os, const T &obj)
{
    return os << obj;
}

//os的类型是固定的,可以正常转换
print(cout, 42);
ofstream f("output");
print(f, 10);

16.2.2 函数模板显式实参

指定显式模板实参
// 编译器无法推断T1,它未出现再函数参数列表中
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);

每次调用sum时必须为T1提供一个显式模板实参

auto val3 = sum<long long>(i, lng); //long long sum(int, long)

显式模板实参按左到右的顺序与对应的模板匹配,只有尾部的可以忽略

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

则我们总是必须为所有的三个形参指定实参

//错误,不能推断前几个模板参数
auto val3 = alternative_sum<long long>(i, lng); //long long sum(int, long)
//正确
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)

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&

使用尾置返回类型:

template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
    //处理序列
    return *beg;    //返回序列中一个元素的引用
}
进行类型转换的标准库模板类

有时需要返回元素的值,但对于传递的参数的类型一无所知。唯一可以使用的操作是迭代器操作,但迭代器只生成引用。
可以使用标准库的类型转换模板,定义在type_traits中,见表16.1
可以使用remove_reference来获得元素类型,模板右一个模板类型参数和一个名为type的类型成员,如remove_reference<int&>则type成员为int

remove_reference<decltype(*beg)>:;type

将获得beg引用的元素的类型

通过组合使用remove_reference、尾置返回和decltype就可以返回元素值的拷贝

//为了使用模板参数的成员,必须用typename
template <typename It>
auto fcn2(It beg, It end) ->
    typename remove_reference<decltype(*beg)>::type
{
    return *beg;
}

16.2.4 函数指针和实参推断

用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参

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

如果不能从函数指针类型确定模板实参则产生错误

//func的重载版本,每个版本接受一个不同的函数指针类型
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare);      //错误,使用compare的哪个实例?

我们可以通过显式地指出来消除歧义

func(compare<int>);

当参数时一个函数模板实例地地址时,程序上下文必须满足:对每个模板参数,能确定唯一确定其类型或值。

16.2.5 模板实参推断和引用

template <typename T> void F(T &p);

编译器会应用正常地引用绑定规则:const是底层地,不是顶层地

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

如果模板类型参数是普通引用T& 实参可以是const

template <typename T> void f1(T&);  //实参必须是一个左值
//对f1的调用使用实参所引用的类型作为模板参数类型
f1(i);      //i是一个int,T是int
f1(ci);     //ci是一个const int,模板参数T是const int
f1(5);      //错误,传递给&参数的实参必须是左值

如果是const T&,则可以传递任意类型实参

template <typename T> void f2(const T&);  //可以接受右值
//f2中参数是const &,实参中的const无关
//每个调用都推断为const int&,模板参数T为int
f1(i);      
f1(ci);     
f1(5);      
从右值引用函数参数推断类型

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

template <typename T> void f3(T&&);
f3(42); //实参是int类型的右值,模板参数T是int
引用折叠和右值引用参数

假定i是一个int对象,f3(i)是不合法的,不能将右值引用绑定到左值。但有两个例外规则:

  1. 当我们将一个左值(i)传递给函数的右值引用参数,且此右值引用参数指向模板类型参数(T&&)是,编译器推断模板类型参数为实参的左值引用类型。因此调用f3(i)时,T的类型为int&而非int.通常我们不能直接定义一个引用的引用,但通过类型别名或模板类型参数间接定义是可以的
  2. 如果我们间接创建一个引用的引用,则这些引用形成了折叠。所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。新标准中折叠规则扩展到右值引用。只有一个特殊情况下引用会折叠成右值引用:右值引用的右值引用。即对于一个给定的类型X:
  • X& &, X& &&和X&& &都折叠成X&.
  • 类型X&& &&折叠成X&&

如果将引用折叠规则和右值引用的特使类型推断规则组合,可以对一个左值调用f3

f3(i);      //实参是一个左值;T为int&
f3(ci);     //T是一个const int&

f3(i)的实例化结果可能如下

//无效代码仅演示
void f3<int&>(int& &&);
//即使f3的函数参数形式是一个右值引用,也会用一个左值引用类型实例化f3
void f3<int&>(int&);

这两个规则导致两个重要结果:

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

即我们可以将任意类型的实参传递给T&&类型的函数参数

编写接受右值引用参数的模板函数
template <typename T> void f3(T&& val)
{
    T t = val;      //拷贝还是引用?
    t = fcn(t);     //赋值改变t还是val
    if (val == t){ /* ... */}   //若t为引用则一直为true
}

当对右值调用f3时,如42,则t的类型为int,对t赋值val保持不变
对i调用f3,T为int&,则t初始化绑定到val

实际中右值引用用于两种情况:模板转发其实参或模板被重载
使用右值引用的函数模板通常使用如下方式重载:

template <typename T> void f(T&&);      //绑定到非const右值
template <typename T> void f(const T&); //左值和const右值

16.2.6 理解std::move

标准库move函数是使用右值引用的好例子

std::move是如何定义的
//在返回类型和类型转换中也要用到typename
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    return static_cast<typename remove_reference<T>::tpye&&>(t);
}

move参数T&&通过引用折叠可以与任意类型实参匹配

string s1("hi!"), s2;
s2 = std::move(string("bye!")); //从一个右值移动数据
s2 = std::move(s1);             //正确,但在赋值之后,s1的值不确定
std::move是如何工作的

在std::move(string("bye!"))中,实参是右值

  1. 推断出T的类型为string
  2. remove_reference用string进行实例化
  3. remove_reference的type成员是string
  4. move的返回类型是string&&
  5. move函数参数t的类型为string&&

因此即调用

string&& move(string &&t);

函数体返回static_cast<string&&>(t)。t的类型已经是string&&,因此类型转换什么都不做

在std::move(s1)中,实参是左值

  1. 推断出T的类型为string&
  2. remove_reference用string&进行实例化
  3. remove_reference<string&>的type成员是string
  4. move的返回类型是string&&
  5. move函数参数t的类型为string& &&,折叠成string&

因此这个调用实例化move<string&>即

string&& move(string &t);

cast将string&转换为string&&

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

虽然不能隐式地将一个左值转换为右值引用,但可是用static_cast显式转换
对于操作右值引用的代码来说,将一个右值引用绑定到左值的特性允许他们截断左值。
统一使用std::move使得我们在程序中查找潜在的截断左值代码变得很容易

16.2.7 转发

某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参地所有性质,包括是否是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调用不会改变,因为传递的是普通非引用的int

void flip1(void(*fcn)(int, int&), int t1, int t2);
定义能保持类型信息的函数参数
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
    f(t2, t1);
}

如果一个函数参数是指向模板类型参数的右值引用(T&&),它对应的实参的const属性和左值/右值属性将得到保持
此版本flip2解决了一般问题,对于接受一个左值引用的函数工作得很好,但不能接受右值引用参数得函数

void g(int &&i, int &j)
{
    cout << i << " " << j << endl;
}
flip2(g, i, 42);        //错误,不能从一个左值实例化int&&
在调用中使用std::forward保持类型信息

可以使用一个 forward的新标准库设施来传递flip2参数,能够保持原始实参的类型。forward必须通过显式模板实参来调用,forward返回显式实参类型的右值引用。即forward返回类型是T&&
通常情况下,使用forward传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性:

template <typename Type> intermediary(Type &&arg)
{
    finalFcn(std::forward<Type>(arg));
}

当用于一个指向模板参数类型的右值引用函数参数(T&&)时,forward会保持实参类型的所有细节

使用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));
}

16.3 重载与模板

函数模板可以被另一个模板或一个普通非模板函数重载

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

  1. 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例
  2. 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板
  3. 与往常一样,可行函数按类型转换来排序(非常有限的)
  4. 如果恰有一个函数提供比任何其他函数都有更好的匹配,则选择此函数。但如果有多个函数提供同样好的匹配,则:
  • 如果同样好的函数中只有一个时非模板函数,则选择此函数
  • 如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板
  • 否则,此调用有歧义
编写重载模板

例:debug_rep

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

此函数可以用来生成一个对象对于的string表示,该对象可以是任意具备输出运算符的类型
定义打印指针的debug_rep版本

//不能用于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();
}

char*值定义了一个<<版本,此<<版本假定指针表示一个空字符结尾的字符数组,并打印数组的内容而非地址值

//使用函数
string s("h1");
cout << debug_rep(s) << endl;

对于此调用,只有第一个版本可行,如果用指针调用:

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

两个函数都可生成可行的实例,且第二个版本是精确匹配。第一个版本需要普通指针到const指针的转换

  1. T绑定到string, debug_rep(const string&)
  2. T绑定到string, debug_rep(string*)
多个可行模板

考虑

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

两个模板都可行,根据重载函数模板的特殊规则,此调用被解析为debug_rep(T)
原因:没有它,无法对一个const的指针调用指针版本的debug_rep。模板debug_rep(const T&)本质上可以用于任何类型,包括指针,比debug_rep(T
)更通用,如果没有此规则,传递给const指针的调用永远都是有歧义的。

当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本

非模板和模板重载

定义一个普通非模板版本的debug_rep来打印string:

string debug_rep(const string &s)
{
    return '"' + s + '"';
}

对string调用时

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

编译器选择非模板版本。
对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本

重载模板和类型转换

还有一种情况:C风格字符串指针和字符串字母常量。

cout << debug_rep("hi world!") << endl;

三个版本都可行:

  1. debug_rep(const T&), T绑定到char[10]
  2. debug_rep(T*), T绑定到const char
  3. debug_rep(const string&),要求从const char* 到string的类型转换

模板和函数都被认为是精确匹配。但T*版本更特例化,编译器选择它

如果我们希望将字符串按string处理,可以另外定义两个非模板重载版本

//将字符指针转换为string,并调用string版本的debug_reg
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);
//为了使debug_rep(char*)的定义正确工作,下面的声明必须在作用域中
string debug_rep(const string &);
string debug_rep(char *p)
{
    //如果接受一个const string&的版本的声明不在作用域中
    //返回语句将调用debug_rep(const T&)的T实例化为string的版本
    return debug_rep(string(p));
}

在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本

16.4 可变参数模板

可变参数模板就是一个接受可变数目参数的模板函数或模板类。可变数目的参数成为参数包。存在两种参数包:模板参数包,表示零个或多个模板参数;函数参数包,表示零个或多个函数参数

用省略号来指出一个模板参数或函数参数表示一个包。在模板参数列表中,class...或typename...指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。

//Args是一个模板参数包;rest是一个函数参数包
//Args表示零个或多个模板类型参数
//rest表示零个或多个函数参数
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("hi");              //空包

编程器会实例化出三个不同版本

sizeof...运算符

当我们需要知道包中有多少元素时,可以用sizeof...运算符
sizeof...也返回一个常量表达式,而且不会对其实参求值

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

16.4.1 编写可变参数函数模板

可以使用initializer_list来定义一个可接受可变参数数目实参的函数。但所有实参必须具有相同的类型。当我们既不知道实参数目也不知道类型时,用可变参数函数很有用。
可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。

// 用来终止递归并打印最后一个元素的函数
// 此函数必须在可变参数的版本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)
{
    os << t << ", ";    //打印第一个实参
    return print(os, rest...);
}

对于最后一个调用,两个函数提供同样好的匹配,但是非可变参数模板比可变参数模板更特例化,因此编译器选择非可变参数版本

当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中。否则可变参数版本会无限递归

16.4.2 包扩展

对于一个参数包,除了获取其大小外,能对其做的唯一的事情是扩展。当扩展一个包时,还要提供用于每个扩展元素的模式。扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号(...)来触发扩展操作

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

调用:

print(cout, i, s, 42);
//第二次调用就等价于
print(os, s, 42);
理解包扩展
//在print调用中对每个实参调用debug_rep
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
    return print(os, debug_rep(rest)...);
}

调用了模式debug_reg(rest).希望我们对函数参数包中的每个元素调用debug_rep。

下面的模式会编译失败

print(os, debug_rep(rest...));  //此调用无函数匹配

扩展中的模式会独立地应用于包中的每个元素

16.4.3 转发参数包

组合使用可变参数模板和forward机制来编写函数,实现其实参不变地传递给其他函数

//设计一个 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)...);
}

emplace_back的函数体调用了chk_n_alloc来确保空间容纳一个新元素,然后调用了construct在first_free指向的位置中创建了一个元素。construct调用中的扩展为

std::forward<Args>(args)...

既扩展了模板参数包Args,又扩展了函数参数包args
例如,假定svec是一个StrVec:

//如果我们调用
svec.emplace_back(10, 'c');
//construct调用中的模式会扩展出
std::forward<int>(10), std::forwrad<char>(c)
建议:转发和可变参数模板

可变参数函数通常将它们的参数转发给其他函数。这种函数通常具有与我们emplace_back函数一样的形式

//fun有零个或多个参数,每个参数都是一个模板参数类型的右值引用
template<typename... Args>
void fun(Args&&... args)    //将Args扩展为一个右值引用的列表
{
    // work实参既扩展Args又扩展args
    work(std::forward<Args>(args)...);
}

这里我们希望将fun的所有实参转发给另一个名为work的函数,假定由它完成函数的实际工作。类似emplace_back中对construct的调用,work调用中的扩展既扩展了模板参数包也扩展了函数参数包。
由于fun的参数是右值引用,因此我们可以传递给它任意类型的实参;由于我们使用std::forward传递这些实参,因此它们的所有类型信息在调用work时都会得到保持。

16.5 模板特例化

模型情况下,通用模板的定义对特定类型是不适合的:通用定义可能编译失败或不正确。当我们不能(或不希望)使用模板版本时,可以定义类或函数模板的特例化版本

//第一个版本;可以比较任意两个类型
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一个字符串字面常量或一个数组时,才调用接受两个非类型模板参数的版本,如果传递字符指针则调用第一个版本

const char *p1 = "h1", *p2 = "mom";
compare(p1, p2);        //调用第一个模板
compare("hi", "mom");   //调用第二个

为类处理字符指针,可以为第一个版本的compare定义一个模板特例化版本。就是模板的一个独立的定义,在其中一个或多个模板参数被指定为特定的类型

定义函数模板特例化

必须为原模板中的每个模板参数都提供实参,template后跟空尖括号

//compare的特殊版本,处理字符数组的指针
template <>
int compare(const char* const & p1, const char* const &p2)
{
    return strcmp(p1, p2);
}
函数重载和模板特例化

当定义函数模板的特例化版本时,本质上接管了编译器的工作。一个特例化版本本质上是一个实例,而非函数名的一个重载版本。因此特例化不影响函数匹配

关键概念:普通作用域规则应用于特例化

为了特例化一个模板,原模板声明必须在作用域中。而且在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中
模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本

类模板特例化

例:特例化一个hash版本,用它来将Sales_data对象保存在无序容器中。一个特例化hash类必须定义:

  1. 一个重载的调用运算符,接受一个容器关键字类型的对象,返回一个size_t
  2. 两个类型成员,result_type和argument_type,分别调用运算符的返回类型和参数类型
  3. 默认构造函数和拷贝赋值运算符

在定义此特例化版本的hash时,唯一复杂的地方是:必须在原模板定义所在的命名空间中特例化它。

//打开std命名空间,以便特例化std::hash
namespace std {
    
}//关闭std命名空间;没有分号

花括号对之间的任意定义都将称为std的一部分
下面代码定义了一个能处理Sales_data的特例化hash版本

namespace std {
template <> //我们正在定义一个特例化版本,模板参数为Sales_data
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);
}
}   //关闭命名空间

使用hash<string>对象来生成bookNo的哈希,其他同理。将这些结果进行异或运算,形成给定Sales_data对象的完整的哈希值

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

template <class T> class std::hash;
class Sales_data {
friedn class std::hash<Sales_data>;
}
类模板部分特例化

一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参

我们只能部分特例化类模板,而不能部分特例化函数模板

标准库remove_reference类型是通过一系列的特例化版本来完成其功能的:

//原始通用版本
template <class T> struct remove_reference {
    typedef T type;
};
//部分特例化版本,将用于左值引用和右值引用
template <class T> struct remove_reference<T&>  //左值
{ typedef T type; };
template <class T> struct remove_reference<T&&> //右值
{ typedef T type; };

部分特例化版本的模板参数列表是原始模板的参数列表的一个自己或者一个特例化版本

特例化成员而不是类

我们可以只特例化特定成员函数而不特例化整个类。

//特例化bar
template <typename T> struct Foo {
    Foo(const T &t = T()): mem(t) { }
    void Bar() { /* ... */ }
    T mem;
    // Foo其他成员
};
template<>  //我们正在特例化一个模板
void Foo<int>::Bar() //我们正在特例化Foo<int>的成员Bar
{
    //进行应用于int的特例化处理
}

本例中只特例化Foo类的一个成员,其他成员由Foo模板提供

Foo<string> fs;
fs.Bar();
Foo<int> fi;
<!--fi.Bar();       //特例化版本
原文地址:https://www.cnblogs.com/aqq2828/p/14464712.html