Skip to content

Latest commit

 

History

History
1410 lines (973 loc) · 52.6 KB

模板与泛型编程[上].md

File metadata and controls

1410 lines (973 loc) · 52.6 KB

模板与泛型编程[上]

定义模板

如果给每种类型都定义完全一样的函数体,是非常烦琐且容易出错的。

函数模板

定义一个通用的函数模板(function template),模板定义以关键字template开始,后跟模板参数列表(template parameter list),这是一个逗号分隔的模板参数(template parameter)的列表,不能是空。

template <typename T> int compare(const T &v1, const T &v2){
    if (v1 < v2) return -1;
    if (v2 < v1) return 1;
    return 0;
}

当使用模板时,隐式或显式地指定模板实参(template argument),将其绑定到模板实参上。

编译器用推断出的模板实参来为我们实例化(instantiate)一个特定版本的函数。

编译器实例化一个模板的时候,它使用实际的模板实参代替对应地模板参数来创建出模板地一个新实例。

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

上例compare函数有个模板类型参数(type parameter)。可以把类型参数看作类型说明符。类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或者类型转换:

// 正确
template <typename T> T foo(T* p)		// T是类型参数
{
    T tmp = *p;			// tmp的类型将是指针p指向的类型
    // ...snap install spotify
    return tmp;
}

每个类型参数前都必须要加class或者typename

typename是在模板已经广泛使用后才引入C++的,而有些人更习惯用class

还可以在模板里定义非类型参数(nonetype parameter)。一个非类型参数表示一个值而非类型。

当模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式。

template<unsigned N, unsigned M> int compare(const char (&p1)[N], const char(&p2)[M]){
    return strcmp(p1, p2);
}

compare("hi", "hello");

编译器实例化版本:

// 编译器用字面常量的大小来代替N和M 从而实例化模板
int compare(const char (&p1)[3], const char (&p2)[6])

函数模板可以声明成inlineconstexpr的。它们都放在模板参数列表后,返回类型前:

// 正确
template <typename T> inline T min(const T&, const T&);
// 错误
inline template <typename T> T min(const T&, const T&);

通过将函数参数设为const引用,保证了函数可以用于不能拷贝的类型。

compare函数说明编写泛型代码的两个重要原则:

  1. 模板中的函数参数是const的引用:保证函数可以用于不能拷贝的类型;也能使得处理大对象时速度更快
  2. 函数体中的条件判断只用<比较:降低了compare函数对于要处理的类型的要求

如果真的关心类型和可移植性,可能要用less

template <typename T> int compare(const T &val1, const T &val2)
{
    if (less<T>()(val1, val2)) return -1;
    if (less<T>()(val2, val1)) return 1;
    return 0;
}

原始版本的问题是如果比较指针,可能出现未定义的结果。

其实less<T>的默认实现也是用的<

模板程序应该尽量减少对于实参类型的依赖。

模板编译

编译器遇到模板定义不会生成代码。只有实例化出模板后才会生成代码。这影响我们如何组织代码以及错误何时被检测到。

通常会把类定义和函数声明放在头文件里,而普通函数和类的成员函数定义在源文件里。

但是为了生成模板的实例化版本,编译器要了解函数模板或类模板成员函数的定义。所以模板的头文件通常既有声明又有定义。

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

关键概念:模板和头文件

模板包含两种名字:

  1. 不依赖模板参数的名字
  2. 以来模板参数的名字

使用模板时,不依赖模板参数的名字必须可见,且模板被实例化时,模板定义包括类模板的成员的定义必须可见,由模板提供者保证。

用来实例化模板的所有函数、类型以及与类型关联的运算符的声明都必须可见,由模板用户保证。

模板设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。

模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。

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

编译器会在三个阶段报告错误::

  1. 编译模板本身。检查语法错误。
  2. 编译遇到模板。检查实参数目或是参数类型是否匹配。对于类模板,编译器可以检查用户是否提供了正确数目的模板实参。
  3. 模板实例化时,发现与类型相同的错误。依赖于编译器如果管理实例化,这类错误可能在链接时才报告。

编写模板时,代码不应该针对某种特定类型,但是通常会对所用的类型做些假设。比如假定类型已经有了<

保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能够正确工作,是调用者的责任。

类模板

类模板(class template)用于生成类的蓝图。

编译器不能为类模板推断模板参数类型。为了用类模板,必须在模板名后的尖括号提供额外信息——用于替换模板参数的模板实参列表。

定义类模板

#include <memory>
#include <initializer_list>
#include <vector>
#include <string>

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);

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

有个名为T的模板类型参数,用来表示Blob保存的元素类型。

实例化类模板

使用类模板时提供额外信息,这些额外是显式模板实参(explicit template argument)列表,它们被绑定到模板参数。

编译器用这些模板实参来实例化出特定的类。

Blob<int> ia;			// 显式模板实参 <int>

编译器实例化出与下面代码等价的类:

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<string>与任何其他Blob类型都没有关联,也不会对其他Blob类型的成员有特殊访问权限。

模板作用域里引用模板类型

类模板用来实例化类型,而一个实例化的类型总是包含模板参数。

如果一个类模板里用了另一个模板,一般都是用模板自己的参数当作被用模板的实参:

std::shared_ptr<std::vector<T>> data;		// 创建vector时用了T

类模板的成员函数

类模板的每个实例都有自己版本,所以类模板的成员函数具有和模板相同的模板参数。所以定义在类模板类外的成员函数要以template开头,后接类模板参数列表。

格式:

template <typename T> 
return-type Blob<T>::member-name(parm-list)

check和元素访问成员

// 定义check 用于检查给定索引
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);
}

// 定义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)
{
    // 若i太大 则check抛出 阻止访问一个不存在的元素
    check(i, "subscript out of range");
    return (*data)[i];
}

Blob构造函数

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

这段代码在作用域Blob<T>里定义了名为Blob的成员函数。该构造函数分配一个空vector,并将指向vector的指针保存在data里。

类似,接受一个initializer_list参数的构造函数将其类型参数T作为initializer_list参数的元素类型:

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

类似默认构造函数,该构造函数分配一个initializer_list,其中的元素必须与Blob的元素类兼容:

Blob<string> articles = {"a"};		// 构造函数的参数类型时initializer_list<string> 列表中的每个字符串字面常量隐式转换成string

类模板成员函数的实例化

默认一个类模板的成员函数只有在程序用到它的时候才会实例化。这个特性使得即使某种类型不能完全符合模板操作的要求,还是能用这个类型实例化类。

比如马(类模板)和驴(类型)能生出骡(实例化类),但是螺子能不能用来做事只有让它做事的时候才能知道。

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

使用类模板类型时候必须提供模板实参,但有个例外。如果在类模板自己的作用域内,可以直接用模板名而不提供类模板类型实参:

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)为本对象指向的vector
    }

    BlobPtr& operator++();      // 前置递增运算符
    BlobPtr& operator--();      // 前置递减运算符
private:
    // 如果检查成功 check返回一个指向vector的shared_ptr
    std::shared_ptr<std::vector<T>> check(std::size_t, const std::string&) const;

    // 保存一个weak_ptr 表示底层vector可能被销毁
    std::weak_ptr<std::vector<T>> wptr;
    std::size_t curr;           // 数组中的当前位置
};

如上代码所示:

BlobPtr& operator++();      // 前置递增运算符
BlobPtr& operator--();      // 前置递减运算符

没有指定模板类型。

类模板外使用类模板名

类模板外定义成员时,就要指出类模板类型了:

template <typename T>
BlobPtr<T> &BlobPtr<T>::operator++()
{
    // 无须检查 调用前置递增会进行检查
    BlobPtr ret = *this;    // 保存当前值
    ++*this;                // 推进 前置递增会自己检查是否合法
    return ret;             // 返回保存的状态
}

因为返回类型在类模板作用域外,所以需要指出返回类型,而BlobPtr ret = *this;处在作用域内就不需要。

类模板和友元

  • 非模板友元:则友元被授权可以访问所有类模板实例。
  • 模板友元:
    • 类模板可以授权给所有友元模板实例
    • 类模板可以只授权给特定实例

一对一友好关系

类模板与另一个模板间友好关系的最常见形式是建立对应实例以及其友元间的友好关系。

例如将BlobPtr类和一个模板版本的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的模板形参作为它们自己的模板实参。所以友好关系被限定在用相同类型实例化的BlobBlobptr相等运算符之间。

通用和特定的模板友好关系

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

template <typename T> class Pal;

// 普通非模板类
class C
{
    friend class Pal<C>;        // 用类C实例化的Pal是C的友元
    // Pal2的所有实例都是C的友元 这种情况无须前置声明
    template <typename> friend class Pal;
};

// 模板类
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;                  // 不需要Pal3的前置声明
};

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

C++11后,可以把模板类型参数声明为友元:

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

模板类型别名

可以定义一个typefef来引用一个实例化的类:

typedef Blob<string> StrBlob;

但不能用一个typefef引用Blob<T>。但是C++11后允许我们为类模板定义一个类型别名:

using namespace std;

template <typename T> using twin = pair<T, T>;	// 模板声明无法出现在块作用域里,所以要在主函数外定义

int main()
{
	twin<string> authors;			// authors是pair<string, string>
 
    twin<int> win_loss;				// win_loss是pair<int, int>
    
    system("pause");
    return 0;
}

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

template <typename T> using partNo = pair<T, int>;
	
	partNo<string> books;		// books是pair<string, int>

类模板的static成员

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

private:
    static std::size_t ctr;
};

每个Foo的实例都有其自己的static成员实例。例如:

// 实例化static成员Foo<string>::ctr和Foo<string>::count
Foo<string> fs;

// 下面的三个对象共享相同的Foo<int>::ctr和Foo<int>::count
Foo<int> f1, f2, f3;

模板类的每个static数据成员必须有且只有一个定义。但是类模板的每个实例都有一个独有的static对象。所以也把static成员定义为模板:

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

访问示例:

Foo<int> f1;		// 实例化Foo<int>类和static数据成员ctr
auto ct = Foo<int>::count();		// 实例化Foo<int>::count
ct = f1.count();		// 使用Foo<int>::count
ct = Foo::count();		// 错误 不知道在用哪个版本的count

static成员也是只有在使用时才会实例化。

模板参数

模板参数的名字没什么内在含义。想叫啥都行。

模板参数与作用域

模板参数的可用范围是声明之后到模板声明结束或定义结束之前。

模板参数会隐藏外层作用域中声明的相同名字。但是与其他上下文不同之处在于模板内不能重用模板参数名:

typedef double A;
template <typename A, typename B> void f(A a, B b)
{
    A tmp = a;			// tmp的类型是A
    double B;			// 错误 重命名参数B
}

因为参数名不能重用,所以一个模板参数名在一个特定模板参数列表只能出现一次:

// 错误 非法重用模板参数名
template <typename V, typename V> ...

模板声明

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

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

一个特定文件所用到的模板的声明一般都放在文件最开始的地方。也就是在使用这些模板前就一起声明好。

使用类的类型成员

因为模板代码只有在实例化之后才能得知通过作用域运算符来访问的是static成员还是一个类型成员。

假如Type是个模板类型参数,当编译器处理Type::mem这样的代码的时候,它不知道mem到底是个类型成员还是static数据成员。

Type::mem * p;		// mem是什么?

需要知道*可以用于表示指针或者进行乘法运算。如果mem是个可运算对象,并且p也是个可运算对象,那么就是mem * p将得到一个右值(假设)。

默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。所以如果想用一个模板类型参数的类型成员,就必须要显示的告知编译器这个名字是个类型。可以用通过typename实现:

template <typename Type>
typename Type::mem top(const Type& c)	// 通过typename告知编译器Type::mem是个类型 这是个返回Type::mem类型的函数
{
    if (!c.empty())
        return c.back();
    else
        return typename Type::mem();
}

默认模板实参

如同默认实参,也可以提供默认模板实参(default template argument)。

C++11后,可以给函数和类模板提供默认实参,而更早的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;
}

调用:

bool i = compare(0, 42);		// 使用less i为-1; T是int(0), int(42); F是less<int(T)>

// 结果依赖于item1和item2的isbn
Sales_data item1(cin), item2(cin);
bool j = compare(item1, item2, compareIsbn);

与函数默认实参相同,对于一个模板参数,只有在它右侧的所有参数都有默认实参时,它才可以有默认实参。

模板默认实参与类模板

template <class T = int> class Numbers      // T 默认是int
{
public:
    Numbers(T v = 0): val(v) { }
    // 对数值的各种操作
private:
    T val;
};

Numbers<long double> lots_of_precision;
Numbers<> average_precision;        // 空<>表示我们希望使用默认类型 其实也就是Numbers<int>

成员模板

类可以包含本身是模板的成员函数。这种成员称为成员模板(member template)。成员模板不能是虚函数。

成员(非模板)类的成员函数

定义一个类似unique_ptr所使用的默认删除器类型:

  • 包含一个重载的函数调用运算符
  • 接受一个指针并对该指针执行delete
  • 该类将在删除器被执行之前打印一条信息
#include <iostream>

// 函数对象类 对给定指针执行delete
class DebugDelete {
public:
	DebugDelete(std::ostream &s = std::cerr): os(s) { }
	
    // 与任何函数模板相同 T类型由编译器推断
	// 因为希望删除器适用于任何类型 所以将调用运算符定义为一个模板
    template <typename T> void operator()(T *p) const
	{
		os << "deleteing unique_ptr" << std::endl;
		delete p;
	}
private:
	std::ostream &os;
};

可以用这个类来代替delete

using namespace std;

int main(int argc, char const *argv[])
{
	double* p = new double;
	DebugDelete d;				// 可以像delete表达式一样用的对象
	d(p);						// 调用DebugDelete::operator()(double*) 释放p
	int* ip = new int;
	// 在一个临时DebugDelete对象上调用operator()(int*)
	DebugDelete()(ip);
	system("pause");
	return 0;
}

因为调用一个DebugDelete对象会delete其给定的指针,我们也可以把DebugDelete用作unique_ptr的删除器。

为了重载unique_ptr的删除器,在尖括号内给出删除器类型,并且提供一个这种类型的对象给unique_ptr的构造函数:

int main(int argc, char const *argv[])
{
	// 销毁p指向的对象
	// 实例化DebugDelete::operator()<int>(int *)
	unique_ptr<int, DebugDelete> p(new int, DebugDelete());
	
	// 销毁sp指向的对象
	// 实例化DebugDelete::opertor()<string>(string*)
	unique_ptr<string, DebugDelete> sp(new string, DebugDelete());

	system("pause");
	return 0;
}

本例中,声明p的删除器的类型是DebugDelete,并且在p的构造函数中提供了该类型的一个未命名对象。

unique_ptr的析构函数会调用DebugDelete的调用运算符。所以只要unique_ptr的析构函数实例化时,DebugDelete的调用运算符就会跟着实例化:实例化过程:

// DebugDelete的成员模板实例化样例
void DebugDelele::operator()(int *p) const { delete p; }
void DebugDelele::operator()(string *p) const { delete p; }

类模板的成员模板

例如给Blob类定义一个构造函数,接受两个迭代器,表示要拷贝的元素范围。因为希望支持不同类序列的迭代器,所以把构造函数定义为模板:

template <typename ClassType> class Blob
{
	template <typename InitType> Blob(InitType b, InitType e);
};

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

// 定义类模板成员
template <typename ClassType>		// 类的类型参数
template <typename InitType>		// 构造函数的类型参数
	Blob<ClassType>::Blob(InitType b, InitType 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>类的接受两个vector<long>::iterator的构造函数
Blob<int> a2(vi.begin(), vi.end());

// 实例化Blob<string>及其接受两个list<const char*>::iterator参数的构造函数
Blob<string> a3(w.begin(), w.end());

定义a1时,构造函数的类型参数通过begin(ia)end(ia)类型推断,结果为int*,因此相当于实例化了如下版本:

Blob<int>::Blob<int*, int*>;

定义a2时使用了已经实例化的Blob<int>类,并用vector<short>::iterator替换InitType来实例化构造函数。

定义a3则显式地实例化了string版本Blob,并隐式地实例化了该类的成员模板构造函数。模板参数被绑定到list<const char*>

控制实例化

模板在被使用的时候才会进行实例化,但是一个实例可能同时出现在多个对象文件里面。如果两个或者多个独立编译的源文件使用了相同的模板,并且提供了相同的模板参数时,每个文件里都会有这个模板的一个相同实例。

大型系统里,多个文件实例化相同模板造成的额外开销可能非常严重。C++11后,可以通过显式实例化(explicit instantiation)来避免这种开销。

显式实例化形式:

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

declaration是个类/函数声明,其中所有模板参数已被替换成为模板实参,例如:

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

编译器遇到extern模板声明的时候,不会在本文件里生成实例化代码,而是向编译器承诺在程序的其它地方会有该实例化的一个定义。对于一个给定的实例化版本,可能有多个extern声明,但是只能有一个定义。

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

// Application.cc
// 这些模板必须在程序其它地方进行实例化
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]);			// 实例化出现在其他地方

文件Application.o将包含Blob<int>的实例及其接受initializer_list参数的构造函数和拷贝构造函数的实例。

compare<int>函数和Blob<string>类将不在本文件里进行实例化。这些模板的定义必须出现在程序的其他文件中:

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

当我们编译该应用程序的时候,必须将templateBuild.oApplication.o链接到一起。

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

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

一个类模板的实例化该模板的所有成员,包括内联的成员函数。因为编译器不了解程序会用该类模板的哪些成员函数,所以编译器会实例化该类的所有成员。

所以用来显式实例化一个类模板的类型,必须要能够用于模板的所有成员。

效率与灵活性

对模板设计者所面对的设计选择,标准库智能指针是个很好的展示:

  • shared_ptr:共享指针所有权;允许用户重载默认删除器,在创建或reset指针时传递一个可调用对象。
  • unique_ptr:独占指针;允许用户重载默认删除器,定义unique_ptr时以显式模板实参的形式提供删除器的类型。

怎么处理删除器的差异实际上就是这两个类功能的差异。这一实现策略上的差异可能对性能有重要影响。

运行时绑定删除器(shared_ptr)

虽然不知道标准库类型如何实现,但是可以推断shared_ptr必须要能够直接访问它的删除器。也就是说,删除器必须保存为一个指针或者一个封装了指针的类(例如function)。

可以确定shared_ptr不是将删除器直接保存为一个成员,因为删除器的类型直到运行的时候才会知道。

实际上,在shared_ptr生存期中,可以随时改变它删除器的类型:可以随便用一种类型的删除器构造一个shared_ptr,然后再用reset赋予这个shared_ptr另一种类型的删除器。通常,类成员的类型在运行时是不能改变的,所以不能直接保存删除器。

假定shared_ptr将其管理的指针保存于成员p中,且删除器通过名为del的成员访问。则shared_ptr的析构函数包含类似如下的语句:

// del的值只有在运行时才知道 通过一个指针来调用它
// 判断del是否含有一个可调用对象 如果有:执行del(p) 否则:执行delete p
del ? del(p) : delete p;		// del(p)需要运行时跳转到del的地址

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

编译时绑定删除器(unique_ptr)

该类中,删除器的类型是类类型的一部分。

unique_ptr<管理的指针, 删除器类>

因为删除器的类型是unique_ptr的一部分,所以删除器成员的类型在编译的时候就知道了,从而使得删除器可以直接保存在unique_ptr对象里。

unique_ptr的析构函数与shared_ptr的析构函数类似,也是对其保存的指针调用用户提供的删除器或执行delete

// del在编译时绑定 直接调用实例化的删除器
del(p);			// 无运行时额外开销

del的类型是默认删除器类型,或者是用户提供的类型。

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

模板实参推断

已经知道,对于函数模板,编译器利用调用中的函数实参来确定其模板参数。从函数实参来确定模板实参的过程被称之为:模板实参推断(template argument deduction)。模板实参推断过程中,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数版本与给定的函数调用最为匹配。

类型转换与模板类型参数

如果用模板类型参数指定了函数形参的类型,那么这个函数会采用特殊的初始化规则。

只有很有限的几种类型转换会自动应用在实参上面。编译器通常不会对实参进行类型转换,而是生成一个新的模板实例。

顶层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);			// 调用fobj(string, string);		const被忽略
fref(s1, s2);			// 调用fref(const string&, const string&);
						// s1转成const是允许的
int a[10], b[42];
fobj(a, b);				// 调用 f(int*, int*);
fref(a, b);				// 错误 数组类型不匹配 因为传进去int* 但是接收要求是const int& 主要是这里是指针 但里面期待是引用

将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换。

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

一个模板类型参数可以用作多个函数形参的类型。但传递给这些形参的实参的类型必须相同。

如果希望允许对函数实参进行正常的类型转换,可以在函数模板定义两个(或多个)类型参数。

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

函数模板可以有用普通类型定义的参数,也就是不涉及模板类型参数的类型:

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

第一个参数是已知类型ostream&,第二个参数obj是模板参数类型。

如果函数参数类型不是模板参数,则对实参进行正常的类型转换。

函数模板显示实参

如果函数返回类型和参数列表中任何类型都不同时,这两种情况最常出现:

  1. 有时候编译器无法推断出模板实参的类型
  2. 其它一些情况下,我们希望允许用户控制模板实例化

指定显示模板实参

示例定义名为sum的函数模板,接受两个不同类型的参数。允许用户指定结果的类型:

// 定义表示返回类型的第三个模板参数,从而允许用户控制返回类型
// 编译器无法推断t1 它未出现在函数参数列表中
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3)

本例中,没有函数实参可以用来推断T1的类型。每次调用sum时调用者都必须给T1提供一个显式模板实参(explicit template argument)。

提供显式模板实参的方式与定义类模板实例化的方式相同:

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

该调用显式指定T1的类型为long longT2T3则由编译器从ilng推断出来。

显式模板实参从左到右与对应模板参数匹配。若sum函数按如下形式编写:

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, ing, long>(i, lng);

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

模板类型参数已经显式指定了的函数实参,也允许进行正常的类型转换:

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

尾置返回类型与类型转换

当我们希望用户确定返回类型时,用显式模板实参表示模板函数的返回类型是很有效的。但在其它情况下,要求显式指定模板实参会给用户增添额外负担。

例如有个函数接受表示序列的一堆迭代器和返回序列中一个元素的引用:

template <typename It>
??? &fnc(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&

此例中,我们知道函数应该返回*beg,而且知道我们可以用decltype(*beg)来获取此表达式的类型。但是在编译器遇到函数的参数列表前,beg都是不存在的。为了定义这个函数,必须使用尾置返回类型:

// 尾置返回出现在参数列表之后 可以用函数的参数
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
    // 处理序列
    return *beg;
}

此例中通知编译器fcn函数的返回类型与解引用beg参数的结果类型相同。解引用运算符返回一个右值,所以通过decltype推断的类型为beg表示的元素的类型的引用。

所以如果对一个string序列调用fcn,返回类型是string&,如果是int,那么就是int&

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

有时无法直接获得所需要的类型。比如可能希望写个类似fcn的函数,但是返回一个元素的值而不是引用。

在写这个函数的时候,面临一个问题:对于传递的参数的类型,几乎一无所知。此例函数中,唯一可用的操作是迭代器操作,而所有迭代器操作都不会生成元素,只能生成元素的引用。

为获得元素类型,可以用标准库的类型转换(type transformation)模板。这些模板定义在头文件type_traits中。该头文件的类通常用于所谓的模板元程序设计。但类型转换模板在普通变成里也很有用。

标准类型转换模板表:

对Mod,其中Mod是 如T为 则Mod::type为
remove_reference X&X&&
否则
X
T
add_cosnt X&const X或函数
否则
T
const T
add_lvalue_reference X&
X&&
否则
T
X&
T&
add_rvalue_reference X&X&&
否则
T
T&&
remove_pointer X*
否则
X
T
add_pointer X&X&&
否则
X*
T*
make_signed unsigned X
否则
X
T
make_unsigned 带符号类型
否则
unsigned X
T
remove_extent X[n]
否则
X
T
remove_all_extents X[n1][2]...
否则
X
T

本例中,可以用remove_reference来获得元素类型。remove_reference模板有个模板类型参数和一个名为type的(公有)类型成员。如果用个引用类型实例化remove_reference,则type将表示被引用的类型。例如,如果实例化remove_reference<int&>,那么type成员就是int&

更一般的,给定一个迭代器beg

remove_reference<decltype(*beg)>::type

将获得beg引用的元素的类型:decltype(*beg)返回元素类型的引用类型。remove_reference::type脱去引用,剩下元素类型本身。

组合remove_reference、尾置返回以及decltype,就可以在函数中返回元素值的拷贝:

// 为了用模板参数的成员 必须用typename 因为type是个类成员 而该类依赖于一个模板参数 所以必须在返回类型的声明里用typename来告知编译器:type表示一个类型
template <typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
    // 处理序列
    return *beg;			// 返回序列中一个元素的拷贝
}

标准类型转换模板表中描述的所有类型转换模板的工作方式都与remove_reference类似。每个模板都有type公有成员表示类型。

如果不可能或者不必要转换模板参数,那么type成员就是模板参数类型本身。

函数指针和实参推断

当一个函数模板初始化一个函数指针或者给一个函数指针赋值的时候,编译器会用指针的类型来推断模板实参。

示例:

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&));		// 接受一个int类型的指针 显式给该指针传递两个string
void func(int(*)(const int&, const int&));

func(compare);		// 错误 使用compare的哪个实例?

通过func的参数类型无法确定模板实参的唯一类型。此调用将会编译失败。

可以通过使用显式模板实参来消除func调用的歧义:

// 正确 显式指出实例化哪个compare版本
func(compare<int>);			// 传递compare(const int&, const int&) // 也就是func(int(*)(const int&, const int&))

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

模板实参推断和引用

考虑例子:

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

其中函数参数p是个模板类型参数T的引用,需要记住:

  1. 编译器会应用正常的引用绑定规则
  2. const是底层的,不是顶层的

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

当一个函数参数是模板类型参数的一个普通左值引用时,绑定规则告诉我们,只能传递给他一个左值。

实参可以是const类型,也可以不是。如果实参是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&,正常的绑定规则告诉我们可以传递给它任何类型的实参——一个对象(const或非const)、一个临时对象或一个字面常量值。

如果函数参数本身是const时,T的类型推断的结果不会是一个const类型:const已经是函数参数类型的一部分。

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

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

如果函数参数是个右值引用,那么类型推断过程类似普通左值引用函数参数的推断过程。

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

引用折叠和右值引用参数

假定i是个int对象,可能会觉得像f3(i)这样的调用是不合法的。毕竟i是个左值,而通常不能将右值引用绑定到左值上。

但C++在正常绑定规则外还定义了两个例外规则以允许这种绑定,这两个例外规则是move这种标准库设施正确工作的基础:

  1. 影响右值引用参数的推断如何进行。当我们将一个左值传递给函数的右值引用参数,并且这个右值引用指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型。所以调用f3(i)时,编译器推断T的类型时int&,而不是int
    • T被推断为int&看起来就像f3的函数参数应该是个int&的右值引用。通常不能直接定义一个引用的引用,但是通过类型别名或者模板类型参数间接定义是可以的。
  2. 这种情况下,可以用第二个例外绑定规则:如果间接创建一个引用的引用,那么这些引用就形成了"折叠"。所有情况下(除第一个),引用会折叠成一个普通的左值引用类型。

C++11后,折叠规则扩展到右值引用。只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。

即,对于一个给定类型X

  • X& &X& &&X&& &都折叠成类型X&
  • 类型X&& &&折叠成X&&

引用折叠只能应用在间接创建的引用的引用,比如类型别名或模板类型。

如果将引用折叠规则和右值引用的特殊类型推断规则组合在一起,就说明我们可以对一个左值调用f3。如果我们把一个左值传给f3的右值引用函数参数时,编译器推断T是个左值引用类型:

f3(i);			// 实参是个左值 模板参数T是int&
f3(ci);			// 实参是个左值 模板参数T是const int&

当一个模板参数T被推断为引用类型时候,折叠规则告诉我们函数参数T&&折叠为一个左值引用类型。比如f3(i)的实例化结果可能如下:

// 伪码
void f3<int&>(int& &&);		// 当T是int&时 函数参数折叠为int&

f3的函数参数是T&&并且Tint&,所以T&&int& &&,最终折叠成int&。搜易f3的函数参数形式是个右值引用,这个调用也会用个左值引用类型来实例化f3

void f3<int&>(int&);		// 当T是int&时候 函数参数折叠为int&

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

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

另外需要注意的是,这两个规则暗示我们可以把任意类型的实参传递给T&&类型的函数参数。对于这种类型的参数,(显然)也能传给它右值。

如果一个函数参数是指向模板参数类型的右值引用(如T&&),那么可以传给他任意类型的实参。如果把左值传给这样的参数,那么函数参数被实例化为一个普通的左值引用(T&)。

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

模板参数可以推断为一个引用类型,该特性对模板内的代码可能有令人惊讶的影响:

template <typename T> void f3(T&& val)
{
	T t = val;			// 是拷贝还是绑定一个引用?
	t = fcn(t);			// 赋值只改变t还是既改变t有改变val?
	if (val == t) ;		// 若T是引用类型 则一直是true
}
  • 右值:右值调用f3时,例如字面常量42Tint。这种情况下,局部变量t的类型为int,且通过拷贝参数val的值被初始化。当我们对t赋值的时候,参数val保持不变。
  • 左值:左值调用f3时,例如i,则Tint&。当我们定义并初始化局部变量t时,赋予它类型int&。所以对t的初始化将其绑定到val。当我们对t赋值时,也同时改变了val的值。if判断永远是true

在代码中涉及的类型可能是普通类型或是引用类型时,编写正确的代码就很难。虽然remove_reference这样的类型转换类可能会有帮助。

实际中,右值引用通常用于两种情况:

  1. 模板转发它的实参
  2. 模板被重载

目前该注意到的,使用右值引用的函数模板通常使用我们如下方式进行重载:

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

与非模板函数一样,第一个版本将绑定到可修改的右值,第二个版本将绑定到左值或者const右值。

理解std::move

虽然不能直接把右值引用绑定到左值上,但是可以用move获得一个绑定到左值上的右值引用。因为move本质上可以接受任何类型的实参,因此我们不会惊讶它是个函数模板。

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&&是个指向模板类型参数的右值引用。
  • 通过引用折叠,该参数可以与任何类型的实参匹配。

可以传给move左值或右值:

string s1("hi!"), s2;

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

std::move是如何工作

第一个复制里,传给move的实参是string的构造函数的右值结果——string("bye!")

如已经见到过的,当向一个右值引用函数参数传递一个右值时,由实参推断出的类型为被引用的类型。

所以在std::move(string("bye!"))中:

  • 推断出T的类型是string
  • 因此,remove_referencestring进行实例化
  • remove_reference<string>type成员是string
  • move的返回类型是string&&
  • move的函数参数t的类型是string&&

所以该调用实例化move<string>,也就是函数:

string &&move(string &&t)

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

第二个赋值:该调用中,传给move的实参是个左值:

  • 推断出的T的类型是string&(string的引用)
  • 所以remove_referencestring&进行实例化
  • remove_reference<string&>type成员是string
  • move的返回类型是string&&
  • move的函数参数t实例化为string& &&,会折叠为string&

所以也就是:

strign&& move(string &t)

也就是将一个右值引用绑定到一个左值。该实例的函数体返回static_cast<string&&>(t)。该情况下,t的类型是string&cast将其转换为string&&

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

通常static_cast只能用于其他合法的类型转换。但!也有一条针对右值引用的特许规则:虽然不能隐式地将左值转换为右值引用,但可以用static_cast显式地将左值转为右值引用。

对于操作右值引用地代码来说,将一个右值引用绑定到一个左值地特性允许它们截断左值。

通过强制使用static_cast,C++试图阻止我们意外地进行这种转换。

虽然可以直接写这种类型转换代码,但使用标准库move函数是更加容易的方式。并且统一使用std::move使得我们在程序里查找潜在的截断左值的代码变得更加容易。

转发

某些函数需要把实参连同它们地类型不变地转发给其它函数。这就需要保持被转发实参的所有性质,包括实参类型是否是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调用ff所做的改变就不会影响实参:

f(42, i);				// f改变了实参i
flip1(f, j, 42);		// 通过flip1调用f不会改变j

问题是j被传递给flip1的参数t1。但是这个参数是普通int

定义能保存类型信息的函数参数

为了解决这个问题,需要重写函数,使其参数能够保持给定实参的"左值性"。更进一步,可以想到我们也希望保持参数的const属性。

通过将一个函数参数定义为一个指向模板类型参数的右值引用,可以保持其对应实参的所有类型信息。而使用引用参数使得我们可以保持const属性,因为在引用类型里const是底层的。

如果将函数参数定义为T1&&T2&&,通过引用折叠就可以保持翻转实参的左值/右值属性:

template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
	f(t2, t1);
}

与较早版本一样,若调用filp2(f, j, 42),将传递给参数t1一个左值j。但是会推断出T1的类型是int&,这意味着t1的类型会折叠为int&

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

该版本filp2解决了大半问题,但是不能用于接受右值引用参数的函数:

void g(int &&i, int& j)
{
	cout << i << " " << j << endl;
}

若尝试通过flip2调用g,则参数t2被传给g的右值引用参数i

flip2(g, 变量, 42);			// 错误 不能从一个左值实例化int&&

// 实际调用是: g(42, 左值引用)

/*
为便于理解
flip2(函数, &&右值引用1, &&右值引用2)	// 注意这里有&&符号
{	
	// 调用 因为(左值引用)特性被保留 所以即使传进来了 在下面的调用中也会被拒绝: 右值引用不能接受一个左值引用
	函数(右值引用2, 右值引用1)		// 错误原因 由于右值引用可以被修改
}

函数(右值引用, 左值)
{
	输出 << 左值 << " " << 右值引用 << endl;
}

*/

也就是flip2g的调用中,g的右值引用参数实际上接收到的是个左值。

调用中使用std::forward保持类型信息

可以用一个叫forward的新标准库设施来传递flip2的参数,它能保持原始实参的类型。

类似moveforward定义在头文件utility中。

move不同,forward必须通过显式模板实参来调用。

forward返回该显式实参类型的右值引用。也就是forward<T>的返回类型是T&&

通常用forward传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性:

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

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

本例Type作为forward的显式模板实参类型(下面用类型(其实没太大区别)代指):

因为arg是类型的右值引用,所以Type也就表示传给arg的实参的所有类型信息。

  • 如果arg是个右值,那么Type是个普通类型,forward<Type>会返回Type&&
  • 如果arg是个左值(Type& &&arg),那么就会通过引用折叠将Type &&arg折叠成左值引用类型,在此情况下,forward<Type>返回指向左值引用类型的右值引用。再次对forward<Type>的返回类型进行引用折叠,将返回一个左值引用类型。

如果调用flip(g, i, 42)i将以int&类型传递给g42将以int&&类型传递给g

最好也不要对std::forward使用using

使用forward再次重写翻转函数:

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