leetcode刷了600多题啦,最近会放缓刷题速度(具体而言,就是只打周赛),开始看C++相关内容。
该内容近短时间内将持续更新,但本身没啥技术含量,因为只是我个人的笔记而已。
内容:C++ Primer / Effective C++
运算符重载
重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个。除了重载的函数调用运算符operator()之外,其它重载运算符不能含有默认实参。
如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式地this指针上。因此,成员运算符函数的显式参数数量比运算符的运算对象总数少一个。
我们可以重载大多数运算符。
我们只能重载已有的运算符。
有四个符号(+,-,*,&)即是一元运算符也是二元运算符,从参数的数量我们可以推断定义的是哪种运算符。
对于一个重载的运算符而言,其优先级和结合律和与对应的内置运算符保持一致。
直接调用一个重载的运算符函数
// 一个非成员运算符函数的等价调用 data1 + data2; // 普通的表达式 operator+(data1, data2); // 等价的函数调用
我们像调用其它成员函数一样显示地调用成员运算符函数:
data1 += data2; // 基于调用的表达式 data1.operator += (data2); // 对成员运算符函数的等价调用
某些运算符不应该被重载
操作符重载
拷贝赋值操作符,C风格转换赋值操作:
String& operator = (const String& ); String& operator = (const char* );
append操作:
String& operator+=(const String& ); String& operator+=(const char* );
作为类成员的操作符重载符的问题在于,不支持如下语句:
String flower; if(flower == "lily") // ok else if("tulip" == flower) // error
此处,我们希望存在一个操作符,满足左操作数是C风格字符串,右操作数是String类型字符串,但实际上不存在,编译器也不会做隐式转换。(需要查看所有类定义,找到能够把左操作数转换为该类型的构造函数,效率低下)
作为非成员函数的写法:
bool operator == (const String&, const String& ); bool operator == (const String&, const char* );
提供后一种重载的初衷是:考虑到C风格字符串到String类型转换的开销。
以下必须是类成员操作符:
=, [], (), ->
拷贝构造函数
class Foo { public: Foo(); // 默认构造函数 Foo(const Foo&); // 拷贝构造函数(通常不应该是explicit的);如果参数不是引用类型,则调用永远不会成功 };
拷贝初始化,直接初始化:
string dots(10, '.'); // 直接初始化 string s(dots); // 直接初始化 string s2 = dots; // 拷贝初始化 string null_book = "9-999-99999-9" // 拷贝初始化 string nines = string(100, '9'); // 拷贝初始化
如果一个类有一个移动构造函数,则拷贝构造有时会使用移动构造函数而非拷贝构造函数来完成。
● 将一个对象作为实参传递给一个非引用类型的形参
● 从一个返回类型为非引用类型的函数返回一个对象
● 用花括号列表初始化一个数组中的元素 / 聚合类中的成员
其它:
使用insert或push成员时,容器会对其元素进行拷贝初始化;使用emplace成员时,创建的元素将进行直接初始化。
编译器也可以选择跳过拷贝/移动构造函数,直接创建对象(但拷贝/移动构造函数必须是存在且可访问的)。
拷贝赋值运算符
Sales_data trans, accum; trans = accum; // 使用拷贝赋值运算符
重载赋值运算符
赋值运算符是一个名为operator=的函数。类似于其它函数,运算符函数也有一个返回类型和一个参数列表。
拷贝赋值运算符接受一个与其所有在类相同类型的参数:
class Foo { public: Foo& operator=(const Foo&); // 赋值运算符 };
合成拷贝赋值运算符
如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符。它会将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员。对于数组类型的成员,逐个赋值数组元素。
// 等价于合成拷贝赋值运算符 Sales_data& Sales_data::operator=(const Sales_data& rhs) { bookNo = rhs.bookNo; // 调用string::operator= units_sold = rhs.units_sold; // 使用内置的int赋值 revenue = rhs.revenue; // 使用内置的double赋值 return *this; // 返回一个此对象的引用 }
析构函数
析构函数释放对象使用的资源,并销毁对象的非static数据成员。
class Foo { public: ~Foo(); // 析构函数 };
在一个析构函数中,首先执行函数体,然后销毁成员。不存在类似构造函数中初始化列表控制成员如何销毁,析构部分是隐式的。成员销毁时发生什么完全依赖于成员的类型。
隐式销毁一个内置指针类型的成员不会delete它所指向的对象。
与普通指针不同,智能指针成员在析构阶段会被自动销毁。
调用析构函数时间:
● 变量离开作用域时
● 当一个对象被销毁时
● 容器被销毁时,元素被销毁
● 动态分配对象,delete时
● 临时对象,创建它的完整表达式被销毁
{ // 新作用域 // p和p2指向动态分配的对象 Sales_data* p = new Scales_data; // p是一个内置指针 auto p2 = make_shared<Sales_data>(); // p2是一个shared_ptr Sales_data item(*p); // 拷贝构造函数将*p拷贝到item中 vector<Sales_data> vec; // 局部对象 vec.push_back(*p2); // 拷贝p2指向的对象 delete p; // 对p指向的对象执行析构函数 }; // 退出局部作用域:对item,p2和vec调用析构函数 // 销毁p2会递减其引用计数,如果变为0则释放 // 销毁vec会销毁它的元素
当指向一个对象的引用/指针离开作用域,析构函数不会执行。
合成析构函数
当一个类未定义自己的析构函数时,编译器会生成一个合成析构函数。
以下代码等价于合成析构函数:
class Sales_data { public: // 成员会被自动销毁 ~Sales_data() { } };
成员是在析构函数体之后隐含的析构阶段被销毁的。
三五法则
有三个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符和析构函数。新标准下还可以定义移动构造函数、移动赋值运算符。
这些操作通常应该被看作一个整体。
如果一个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值函数。
如果只有析构函数:
class HasPtr { public: HasPtr(const std::string& s = std::string()) : ps(new std::string(s)), i(0) { } ~HasPtr() { delete ps; } // 错误,还需要拷贝构造,拷贝赋值运算符 };
多个HasPtr对象可能指向相同的内存:
HasPtr f(HasPtr hp) { HasPtr ret = hp; // 拷贝 return ret; // ret 和 hp 被销毁 }
Hasptr p("values"); f(p); // 当f结束时,p.ps指向无效内存 HasPtr q(p); // p,q都指向无效内存
如果一个类需要一个拷贝构造函数,那么也需要一个拷贝赋值运算符。如果一个类需要一个拷贝赋值运算符,那么也需要一个拷贝构造函数。但不意味着需要一个析构函数。
使用=default
使用=default要求编译器生成合成的版本:
class Sales_data { public: // 拷贝控制成员:使用default Sales_data() = default; Sales_data(const Sales_data&) = default; Sales_data& operator=(const Sales_data&); ~Sales_data() =default; }; Sales_data& Sales_data::operator=(const Sales_data&) = default;
当我们在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联的。如果不希望,可以在类外使用=default。
阻止拷贝
对于有些类而言,拷贝构造和拷贝赋值运算符没有合理的意义。例如,iostream阻止了拷贝,避免多个对象写入/读取相同IO缓冲。
我们可以定义删除的函数来阻止拷贝:
struct NoCopy { NoCopy() = default; // 使用合成的默认构造函数 NoCopy(const NoCopy&) = default; // 阻止拷贝 NoCopy& operator=(const NoCopy&) = default; // 阻止赋值 ~NoCopy() =default; // 使用合成的析构函数 }
=delete必须出现在函数第一次声明的时候;我们可以对任何函数指定=delete。
析构函数不能是删除的成员
如果析构函数被删除,就无法销毁此类型的对象了。
struct NoDtor { NoDtor() = default; // 使用合成默认构造函数 ~NoDtor() = delete; // 我们不能销毁NoDtor类型的对象 }; NoDtor nd; // 错误:NoDtor的析构函数是删除的 NoDtor* p = new NoDtor(); // 正确:但我们不能delete p delete p; // 错误,析构函数是删除的
对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针。
合成的拷贝控制成员可能是删除的
对于某些类来说,编译器将这些合成的成员定义为删除的函数:
● 如果类的某个成员的析构函数是删除的或者不可访问的(如是private)的,那么类的合成析构函数被定义为删除的。
● 如果类的某个成员的拷贝构造函数是删除的或者不可访问的,则类的合成拷贝构造函数也被定义为删除的;如果类的某个成员的析构函数是删除的或者不可访问的,则类合成的拷贝构造函数也被定义为删除的。
● 如果类的某个成员的拷贝赋值运算符是删除的或者不可访问的,或是类有一个const或引用的成员,则类的合成拷贝赋值运算符是删除的。
● 如果类的某个成员的析构函数是删除的或不可访问的,或是类有一个引用成员,没有类内初始化器,或是类有一个const成员,没有类内初始化器且类型未显式定义默认构造函数,则类的默认构造函数被定义为删除的。
如果一个类有数据成员不能默认构造、拷贝、赋值或销毁,则对应成员函数被定义为删除的。
一个成员有删除的不可访问的析构函数会导致合成的默认或拷贝构造函数定义为删除的,因为我们可能会创建出无法销毁的对象。
对于具有引用成员或无法默认构造的const成员的类,不会合成默认构造函数,没有合成拷贝赋值函数。因为将一个新值赋给一个const对象是不可能的。
虽然可以将一个新值赋给一个引用成员,但改变的是引用指向的对象的值,而非引用本身。对于合成拷贝赋值运算符,将指向与赋值前一样的对象。
private拷贝控制
新标准之前通过将拷贝构造函数和拷贝赋值运算符声明为private来阻止拷贝:
class PrivateCopy { // 无访问说明符:接下来的成员默认private PrivateCopy(const PrivateCopy&); PrivateCopy& operator=(const PrivateCopy&); public: PrivateCopy() =default; ~PrivateCopy(); };
声明但不定义一个成员函数是合法的(对此只有一个例外)。试图访问未定义成员会导致链接错误。
strVec的实现
● elements 指向分配的内存中的首元素
● first_free 指向最后一个实际元素之后的位置
● cap 指向分配的内存末尾之后的位置
工具函数:
● alloc_n_copy 分配内存,并拷贝一个给定范围中的元素
● free 销毁构造的元素,并释放内存
● chk_n_alloc保证strVec至少有容纳一个新元素的空间。如果没有空间添加新元素,chk_n_alloc会调用reallocate来分配更多内存
● reallocate 在内存用完后为strVec分配新内存
class StrVec { public: StrVec() : // allocator成员进行默认初始化 elements(nullptr), first_free(nullptr), cap(nullptr) { } StrVec(const StrVec&); // 拷贝构造函数 StrVec& operator=(const StrVec&); // 拷贝赋值运算符 ~StrVec(); // 析构函数 void push_back(const std::string&); // 拷贝元素 size_t size() const { return first_free - elements; } size_t capacity() const { return cap - elements; } std::string* begin() const { return elements; } std::string* end() const { return first_free; } // ... private: static std::allocator<std::string> alloc; // 分配元素,被添加元素的函数所使用 void chk_n_alloc() { // 工具函数,被拷贝构造函数、赋值运算符和析构函数所使用 if((size() == capacity()) reallocate(); } std::pair<std::string*, std::string*> alloc_n_copy (const std::string*, const std::string*); void free(); // 销毁元素并释放内存 void reallocate(); // 获得更多内存并拷贝已有元素 std::string* elements; // 指向数组首元素的指针 std::string* first_free; // 指向数组第一个空闲元素的指针 std::string* cap; // 指向数组尾后位置的指针 };
● 默认构造函数(隐式地)默认初始化alloc并(显示地)将指针初始化为nullptr, 表明没有元素
● size成员返回当前真正在使用的元素的数目,等于first_free-elements.
● capacity成员返回strVec可以保存的元素数量,等价于cap-elements.
● begin和end成员分别指向返首元素和最后一个构造元素之后位置的指针
void StrVec::push_back(const string& s) { chk_n_alloc(); // 确保有空间容纳新元素 alloc.construct(first_free++, s); }
当我们使用allocator分配内存时,必须记住内存是未构造的。为了使用此原始内存,我们必须调用construct,在此内存中构造一个对象。传递给construct的第一个参数必须是一个指针,指向调用allocate所分配的未构造的内存空间。剩余参数确定用哪个构造函数来构造对象。
pair<string*, string*> StrVec::alloc_n_copy(const string* b, const string* e) { // 分配空间保存给定范围中的元素 auto data = alloc.allocate(e - b); // 初始化并返回一个pair, 该pair由data和uninitialized_copy的返回值构成 return {data, uninitialized_copy(b, e, data) }; }
当我们拷贝或赋值strVec时,必须分配独立的内存,并从原strVec对象拷贝元素至新对象。此函数返回一个指针的pair,两个指针分别指向新空间的开始位置和拷贝后的位置。
alloc_n_copy用尾后指针减去首元素指针,来计算需要多少空间。在返回语句中完成拷贝工作,返回语句对返回值进行了列表初始化。
void StrVec::free() { // 不能传递给deallocate一个空指针,如果elements为0,函数什么也不做 if(elements) { // 逆序销毁旧元素 for(auto p = first_free; p != elements; ) alloc.destroy(--p); alloc.deallocate(elements, cap - elements); } }
destroy函数会运行string的析构函数。string的析构函数会释放string自己分配的内存空间。
StrVec& StrVec::operator=(const StrVec& rhs) { // 调用alloc_n_copy分配内存 auto data = alloc_n_copy(rhs.begin(), rhs.end()); free(); elements = data.first; first_free = cap = data.second; return *this; }
移动构造函数
构造时,必须调用move来表示希望使用移动构造函数,否则将会使用拷贝构造函数;当我们使用move时,直接调用std::move而不是move。
void StrVec::reallocate() { // 我们将分配当前大小的两倍内存空间 auto newcapacity = size() ? 2 * size() : 1' // 分配新内存 auto newdata = alloc.allocate(newcapacity); // 将数据从旧内存移动到新内存 auto dest = newdata; // 指向新数组中的下一空闲位置 auto elem = elements; // 指向旧数组中的下一个元素 for(size_t i = 0; i != size(); i++) alloc.construct(dest++, std::move(*elem++)); free(); // 一旦我们移动完,就释放旧的内存空间 // 更新我们的数据结构,执行新元素 elements = newdata; first_free = dest; cap = elements + newcapacity; }
由于我们使用了移动构造函数,这些string管理的内存将不会被拷贝。相反,我们构造的每个string都会从elem指向的string那里接受内存的所有权。在元素移动完毕后,我们调用free销毁旧元素,并释放StrVec原来使用的内存。string成员对象不再管理他们曾经指向的内存。
标准库容器、string和shared_ptr既支持移动也支持拷贝;IO类和unique_ptr类可以移动但不能拷贝。
右值引用
必须绑定到右值的引用,我们通过&&而不是&来获得右值引用。
右值引用只能绑定到一个将要销毁的对象中,因此,我们可以自由地将一个右值引用的资源移动到另一个对象中。
一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表达的是对象的值。
int i = 42; int& r = i; // 正确: int&& r = i; // 错误,不能将一个右值引用绑定到一个左值上 int& r2 = i * 42; // 错误,i * 42是一个右值 const int& r3 = i * 42; // 正确,我们可以将一个const的引用绑定到一个右值上 inr&& r2 = i * 42; // 正确,将rr2绑定到乘法结果上
左值:返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符。
右值:返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符。(要么是字面常量,要么是表达式求值过程中临时创建的对象)
变量是左值
变量表达式是左值,因此,我们不能将一个右值引用绑定到一个右值引用类型的变量上:
int&& rr1 = 42; // 正确:字面常量是右值 int&& rr2 = rr1; // 错误:表达式rr1是左值
标准库move函数
虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显式地将一个左值转换为对应右值引用类型。
int&& rr3 = std::move(rr1); // ok
move告诉编译器:我们有一个左值,但希望像一个右值一样处理。调用move意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。
我们使用std::move而不是move,这样可以避免潜在的名字冲突。
移动构造函数和移动赋值运算符
引用参数在移动构造函数中是一个右值引用,移动构造函数还必须确保移动后,源对象不再指向被移动的资源,这些资源的所有权已经归属新创建的对象。
StrVec::StrVec(StrVec&& s) noexpect // 移动操作不应抛出任何异常 // 成员初始化器接管s中的资源 : elements(s.elements), first_free(s.first_free), cap(s.cap) { // 令s进入这样的状态-对其运行析构函数是安全的 s.elements = s.first_free = s.cap = nullptr; }
移动构造函数不分配任何新内存;它接管给定的StrVec中的内存。在接管内存之后,它将给定对象中的指针都置为nullptr。这样就完成了从给定对象的移动操作,此对象将继续存在。最终,移动后源对象将会被销毁,意味着将在其上运行构造函数。StrVec的析构函数在first_free上调用deallocate。
移动操作通常不会抛出任何异常,当编写一个不抛出异常的移动操作时,我们应该将此事通知标准库。我们将看到,除非标准库知道我们的移动构造函数不会抛出异常,否则它将为了可能存在的异常做一些额外的工作。
我们必须在类头文件的声明和定义中都指定noexcept.
class StrVec { public: StrVec(StrVec&&) noexcept; // 移动构造函数 // 其它成员的定义 }; StrVec::StrVec(StrVec&& s) noexcept : { // .... }
如果希望在vector重新分配内存这类情况下对我们自定义类型的对象进行移动而不是拷贝,就必须显示地告诉标准库我们的移动构造函数可以安全使用,通过将其标记为noexcept来做到这一点。
移动赋值运算符
StrVec& StrVec::operator=(StrVec&& rhs) noexcept { // 直接检测自赋值 if( this != &rhs ) { free(); // 释放已有元素 elements = rhs.elements; // 从rhs接管资源 first_free = rhs.first_free; cap = rhs.cap; // 将rhs置于可析构状态 rhs.elements = rhs.first_free = rhs.cap = nullptr; } return *this; }
我们需要进行自赋值检测,此右值可能是move调用返回的结果,不能在使用右侧运算对象之前释放左侧对象资源。
当我们编写一个移动操作时,必须确保移动后,源对象进入一个可析构的状态;移动操作还必须保证对象仍然是有效的。但是用户不能对其值进行任何假设。
合成的移动操作
如果一个类定义了自己的拷贝构造函数、拷贝赋值函数或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替移动操作。
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。
// 编译器会为X和hasX合成移动操作 struct X { int i; // 内置类型可以移动 std::string s; // string定义了自己的移动操作 }; struct hasX { X mem; // X有合成的移动操作 }; X x; X x2 = std::move(x); // 使用合成的移动构造函数 hasX hx; hasX hx2 = std::move(hx); // 使用合成的移动构造函数
● 与拷贝构造函数不同,移动构造函数被定义为删除的函数的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。
● 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的。
● 如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的。
● 如果类成员是const的或是引用,则类的移动赋值运算符被定义为删除的。
// 假设Y是一个类,它定义了自己的拷贝构造函数 // 但未定义自己的移动构造函数 struct hasY { hasY() = default; hasY(hasY&&) = default; Y mem; // hasY将有一个删除的移动构造函数 }; hasY hy; hy2 = std::move(hy); // 错误:移动构造函数是删除的
移动右值,拷贝左值
如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。赋值操作的情况类似。
StrVec v1, v2; v1 = v2; // v2 是左值;使用拷贝赋值 StrVec getVec(istream&); // getVec返回一个右值 v2 = getVec(cin); // getVec(cin)是一个右值,使用移动赋值
如果一个类没有移动构造函数,函数匹配规则保证该类型的对象会被拷贝,即使我们试图通过调用move来移动它们时也是如此:
class Foo { public: Foo() = default; Foo(const Foo&); // 拷贝构造函数 // 其它成员定义,但Foo未定义移动构造函数 }; Foo x; Foo y(x); // 拷贝构造函数,x是一个左值 Foo z(std::move(x)); // 拷贝构造函数,因为未定义移动构造函数
用拷贝构造函数代替移动构造函数几乎肯定是安全的(赋值运算符的情况类似)。
拷贝并交换赋值运算符和移动操作
class HasPtr { public: // 添加移动构造函数 HasPtr(HasPtr&& p) noexcept : ps(p.ps), i(p.i) { p.ps = 0; } // 赋值运算符既是移动赋值运算符,也是拷贝赋值运算符 HasPtr& operator = (HasPtr rhs) { swap(*this, rhs); return *this; } };
依赖于实参的类型,拷贝初始化要么使用拷贝构造函数,要么使用移动构造函数(左值拷贝,右值移动)。
hp = hp2; // hp2是一个左值;hp2通过拷贝构造函数来拷贝 hp = std::move(hp2); // 移动构造函数移动hp2
一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个拷贝操作。如前所述,某些类必须定义拷贝构造函数、拷贝赋值运算符和析构函数才能正确工作。
例子
// 从本Message移动Folder指针 void Message::move_Folders(Message* m) { folders = std::move(m->folders); // 使用set的移动赋值运算符 for(auto f ; folders) { // 对每个Folder f->remMsg(m); // 从Folder中删除旧Message f->addMsg(this); // 将本Message添加到Folder中 } m->folders.clear(); // 确保销毁m是无害的 }
向set插入一个元素可能会抛出一个异常——向容器添加元素的操作要求分配内存,意味着可能会抛出一个bad_alloc异常。因此我们未将它标记为noexcept。
Message::Message(Message&& m) : contents(std::move(m.contents)) { move_Folders(&m); // 移动folders并更新Folder指针 } Message& Message::operator=(Message&& rhs) { if( this != &rhs ) { // 直接检查自赋值的情况 remove_from_Folders(); contents = std::move( rhs.contents ); // 移动赋值运算符 move_Folders(&rhs); // 重置Folders指向本Message } return *this; }
移动迭代器
一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。移动迭代器的解引用运算符生成一个右值引用。
调用标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器。此函数接受一个迭代器参数,返回一个移动迭代器。
void StrVec::reallocate() { // 分配大小两倍于当前规模的内存空间 auto newcapacity = size() ? 2 * size() : 1; auto first = alloc.allocate(newcapacity); // 移动元素 auto last = uninitialized_copy( make_move_iterator(begin()), make_move_iterator(end()), first); free(); // 释放旧空间 elements = first; // 更新指针 first_free = last; cap = elements + newcapacity; }
右值引用和成员函数
定义了push_back的标准库容器提供两个版本:一个版本有一个右值引用参数,另一个版本有一个const左值引用。
void push_back(const X&); // 拷贝:绑定到任意类型的X void push_back(X&&); // 移动:只能绑定到类型X的可修改的左值
例子:
class StrVec { public: void push_back(const std::string&); // 拷贝元素 void push_back(std::string&&); // 移动元素 }; void StrVec::push_back(const string& s) { chk_n_alloc(); // 确保有空间容纳新元素 // 在first_free指向的元素中构造s的一个副本 alloc.construct(first_free++, s); } void StrVec::push_back(string&& s) { chk_n_alloc(); //如果需要的话为StrVec重新分配内存 alloc.construct(first_free++, std::move(s)); }
传递给constrcut的实参类型是string&&。
当我们调用push_back时,实参类型决定了新元素是拷贝还是移动到容器中:
StrVec vec; // 空StrVec string s = "some string or another"; vec.push_back(s); // 调用push_back(const string&) vec.push_back("done"); // 调用push_back(const string&&);
右值和左值引用成员函数
对右值做一些操作:
string s1 = "a value"; string s2 = "another"; auto n = (s1 + s2).find('a'); s1 + s2 = "wow!";
新的标准库仍然允许向右值赋值。但是我们希望在自己的类中阻止这种用法。可在参数列表后放置一个引用限定符:
class Foo { public: Foo& operator=(const Foo&) &; // 只能向可修改的左值赋值 // Foo的其它参数 }; Foo& Foo::operator=(const Foo& rhs) & { // 执行将rhs赋予本对象所需的工作 return *this; }
引用限定符可以是&或&&,分别指出this可以指向一个左值或者右值。类似const限定符,引用限定符只能用于(非static)成员函数,且必须同时出现在函数的声明和定义中。
Foo& retFoo(); // 返回一个引用; retFoo调用是一个左值 Foo retVal(); // 返回一个值; retVal调用是一个右值 Foo i, j; // i 和 j 是左值 i = j; // 正确 retFoo() = j; // 正确:retFoo()返回一个左值 retVal() = j; // 错误:retVal() 返回一个右值 i = retVal(); // 正确:我们可以将一个右值作为赋值操作的右侧运算对象
一个函数可以同时用const和引用限定。在此情况下,引用限定符必须跟在const限定符之后:
class Foo { public: Foo someMem()& const; // 错误 Foo anotherMem() const &; // 正确:const限定符在前 };
重载和引用函数
class Foo { public: Foo sorted() &&; // 可用于可改变的右值 Foo sorted() const &; // 可用于任何类型的Foo private: vector<int> data; }; // 本对象为右值,因此可以进行原址排序 Foo Foo::sorted() && { sort(data.begin(), data.end()); } // 本对象是const或左值,我们不能对原址进行排序 Foo Foo::sorted() const& { Foo ret(*this); // 拷贝一个副本 sort(ret.data.begin(), ret.data.end()); return ret; // 返回副本 }
编译器会根据调用sorted的对象的左值/右值来决定使用哪个sorted版本:
retVal().sorted(); // retVal() 是一个右值,调用Foo::sorted() && retFoo().sorted(); // refFoo()是一个左值,调用Foo::sorted() const &
如果我们定义两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有的函数加上引用限定符,或者所有都不加:
class Foo { public: Foo sorted() &&; Foo sorted() const; // 错误:必须加上引用限定符 using Comp = bool(const int&, const int&); Foo sorted(Comp*); // 正确:不同的参数列表 Foo sorted(Comp*) const; // 正确:两个版本都没有引用限定符 };
生存期
全局对象:程序启动时分配,程序结束时销毁。
局部本地对象:进入定义的程序块时创建,离开块时销毁。
局部static对象:第一次使用前分配,程序结束时销毁。
动态分配对象:显示释放时销毁。(free store / heap)
shared_ptr类
shared_ptr<string> p1; shared_ptr<list<int>> p2;
默认初始化的智能指针中保存着一个空指针。
通用基本操作:
shared_ptr<T> sp; // 空智能指针 unique_ptr<T> up; p // 判断是否指向一个对象 *p // 解引用p,获得指向对象 p->mem // (*p).mem p.get() // 返回p中的指针 swap(p, q); p.swap(q);
独有基本操作:
make_shared<T>(args); // 用args初始化此对象 shared_ptr<T> p(q); // p是shared_ptr q的拷贝,此操作会递增q中的计数器 p = q; // 递减p中的引用计数,递增q的引用计数 p.unique(); // 如果use_count()为1,返回true p.use_count(); // 返回与p共享对象的智能指针数量
make_shared 函数
此函数在动态内存中分配一个对象并初始化,返回该对象的shared_ptr。(头文件memory)
shared_ptr<int> p3 = make_shared<int>(42); shared_ptr<string> p4 = make_shared<string>(10, '9'); shared_ptr<int> p5 = make_shared<int>)(); // 值为0
如果我们不传递任何参数,对象就会进行值初始化。
shared_ptr拷贝和赋值
当进行拷贝或赋值操作时,shared_ptr会记录有多少个其它shared_ptr指向相同的对象:
auto p = make_shared<int>(42); // p指向对象只有一个引用者 auto q(p); // y有两个引用者
我们可以认为每个shared_ptr有一个关联的计数器,称为引用计数。无论我们何时拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或者将它作为参数传递给一个函数,以及作为函数的返回值时,关联的计数器都会递增。
当我们给shared_ptr一个新值或者shared_ptr被销毁,计数器就会递减。
一旦一个shared_ptr计数器为0,它会释放自己管理的对象。
auto r = make_shared<int>(42); // r 指向的int只有一个引用者 r = q; // 给r赋值,令它指向另一个地址 // 递增q指向对象的引用计数 // 递减r指向对象的引用计数 // r原来指向对象已经没有引用者,会自动释放
shared_ptr自动销毁所管理的对象
函数模板
模板定义以关键字template开始,加一个模板参数列表,包含逗号分隔的一个或多个模板参数列表。
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 compare(const int& v1, const int&v2) { if( v1 < v2 ) return -1; if( v2 < v1 ) return 1; return 0; }
模板类型参数
可以加类型参数看做类型说明符:
template <typename T> T foo(T* p) { T tmp = *p; // tmp 的类型是指针p指向的类型 // ... return tmp; }
非类型模板参数
非类型模板参数表示一个值(非一个类型),这些值必须是常量表达式&具有静态生存期。
template<unsigned N, unsigned M> int compare(const char* (&p1)[N], const char* (&p2)[M]) { return strcmp(p1, p2); }
调用compare("hi", "mom") 时,会使用字面常量的大小来代替M和N,从而实例化模板,得到如下实例化版本:
int compare(const char (&p1)[3], const char (&p2)[4])
仅仅在实例化模板的特例时,编译器才会生成代码。为了生成一个实例化版本,编译器需要函数模板或类模板成员函数的定义。因此,模板的头文件通常包含声明+定义。
inline & constexptr 的函数模板
说明符需要跟在模板参数列表之后:
template<typename T> inline T min(const T&, const T&);
大多数编译错误在实例化期间报错。
类模板
一个类模板的每个实例都对应一个独立的类,该类与其它类都没有关联,也不会对其它类型的成员有特殊的访问权限。
在模板作用域中引用模板类型
std::shared_ptr<std::vector<T>> data;
通常不使用实际类型(或值)的名字用作其模板实参,而是使用模板自己的参数当作模板的实参。
类模板的成员函数
我们可以在类模板的内部或外部定义成员函数。内部的函数被隐式声明未内联函数。
template<typename T>
ret-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> void Blob<T>::pop_back() { 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<string> articles = {"a", "an", "the" };
类模板成员函数的实例化
一个类模板的成员函数只有当程序使用到它时,才进行实例化。
在类代码内简化模板类型名的使用
在类模板自己的作用域中,我们可以直接使用模板名,而不提供实参。
// 若试图访问一个不存在的元素, BlobPtr抛出异常 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<std::vector<T>> wptr; std::size_t curr; // 数组中当前位置 };
在类模板外使用类模板名
由于返回类型位于类的作用域之外,我们必须指出返回类型是一个实例化的BlobPtr,它所用类型与类实例化所用类型一致。
在函数体内,我们已经进入类的作用域,因此在定义ret时无需重复定义模板实参。
template<typename T> BlobPtr<T> BlobPtr<T>::operator++(int) { BlobPtr ret = *this; ++*this; return ret; }
ret的定义等价于:BlobPtr<T> ret = *this;
类模板和友元
如果一个类包含一个非模板友元,则友元被授权可以访问所有的模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
一对一友好关系
为了引用(类或函数)模板的一个特定实例,我们必须首先声明模板自身。一个模板声明包括模板参数列表:
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>; friend bool operator==<T>(const Blob<T>&, const Blob<T>&); };
每个Blob实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符。
友好关系被限定在用相同类型实例化的Blob与BlobPtr相等运算符之间。
通用和特定的模板友好关系
一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的特例为友元:
// 前置声明,在将模板的一个特定实例声明为友元时要用到 template<typename T> class Pal; class C { // C是一个普通的非模板类 friend class Pal<C>; // 用类C实例化的Pal是C的一个友元 // Pal2的所有实例都是C的友元;这种情况无需前置声明 template<typename T> friend class Pal2; }; template<typename T> class C2 { // C2本身是一个类模板 // C2的每个实例都将相同实例化的Pal声明为友元 friend class Pal<T>; // Pal的模板声明必须在作用域之内 // Pal2的所有实例都是C2的每个实例的友元,不需要前置声明 template<typename X> friend class Pal2; // Pal3是一个非模板类,它是C2所有实例的友元 friend class Pal3; // 不需要Pal3的前置声明 };
为了让所有实例都成为友元,友元声明中必须使用与类模板本身不同的模板参数。
令模板自己的类型参数成为友元
template <typename Type> class Bar { friend Type; // 将访问权限授予用来实例化Bar的类型 // ... };
对于某个类型名Foo,Foo将成为Bar<Foo>的友元,Sales_data将成为Bar<Sales_data>的友元,依次类推。
模板类型别名
我们可以定义一个typedef来引用实例化的类:
typedef Blob<string> StrBlob;
由于模板不是一个类型,我们不能定义一个typedef引用一个模板。即,无法定义一个typedef引用Blob<T>.
但是,新标准允许我们为类模板定义一个类型别名:
template<typename T> using twin = pair<T, T>; twin<string> authors; // authors是一个pair<string, string>
一个模板类型别名是一族类的别名:
twin<int> win_loss; // win_loss是一个pair<int, int> twin<double> area; // area是一个pair<double, double>
当我们定义一个模板类型别名时,可以固定一个或多个模板参数:
template<typename T> using pair<T, unsigned>; partNo<string> books; // books是一个pair<string, unsigned> partNo<Vehicle> cars; // cars是一个pair<Vehicle, unsigned> partNo<Student> kids; // kids是一个pair<Student, unsigned>
类模板的static成员
template<typename T> class Foo { public: static std::size_t count() { return ctr; } private: static std::size_t ctr; };
Foo是一个类模板,它有一个名为count的public static成员函数和一个名为ctr的private static的数据成员。每个Foo的实例都有其自己的static成员实例。即,对任意给定类型X,都有一个Foo<X>::ctr和一个Foo<X>::count成员。所有Foo<X>类型的对象共享相同的ctr对象和count函数:
// 实例化static成员Foo<string>::ctr和Foo<string>::count Foo<string> fs; // 三个对象共享相同的Foo<int>::ctr和Foo<int>::count成员 Foo<int> fi, fi2, fi3;
我们将static数据成员也定义为模板:
template<typename T> size_t Foo<T>::ctr = 0; // 定义并初始化ctr
为了通过类来直接访问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
模板参数与作用域
在模板内不能重用模板参数名:
typedef double A; template<typename A, typename B> void f( A a, B b ) { A tmp = a; // tmp类型为模板参数A的类型,而非double double B; // 错误:重声明模板参数B }
正常的名字隐藏规则决定了A的typedef被类型参数A隐藏。因此,tmp不是一个double。由于我们不能重用模板参数名,声明为名字为B的变量是错误的。
一个模板参数名在一个特定模板参数列表中只能出现一次:
// 错误:非法重用模板参数名V template<typename V, typename V> // ...
模板声明
模板声明必须包含模板参数:
// 声明但不定义compare和Blob template<typename T> int compare(const T&, const T&); template<typename T> class Blob;
与函数参数相同,声明中的模板参数的名字不必与定义中相同:
// 3个calc都指向相同的函数模板 template<typename T> T calc(const T&, const T&); // 声明 template<typename U> U calc(const U&, const U&); // 声明 // 模板的定义 template<typename Type> Type calc(const Type& a, const Type& b) { // ..... }
一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。
使用类的类型成员
当编译器遇到如下形式的语句时:
T::size_type* p;
它需要知道我们是正在定义一个名为p的变量还是将一个名为size_type的static数据成员与名为p的变量相乘。
如果我们希望使用一个模板类型参数的类型成员,就必须显示告诉编译器该名字是一个类型,通过关键字typename实现:
template<typename T> typename T::value_type top(const T& c) { if(!c.empty()) return c.back(); else return typename T::value_type(); }
默认模板实参
在新标准里,我们可以为函数和类模板提供默认实参。而更早的C++标准只允许为类模板提供默认实参。
// compare有一个默认模板实参less<T> 和一个默认函数实参F() template<typename T, typename F = less<T>> int c ompare(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函数对象类。默认函数实参指出 f 将是类型F的一个默认初始化的对象。
bool i = compare(0, 42); // 使用less, i 是 -1 // 结果依赖于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; // 空<>表示我们希望使用默认类型
成员模板
一个类可以包含本身是模板的成员函数。这种成员被称为成员模板。成员模板不能是虚函数。
普通(非模板)类的成员模板
// 函数对象类,对给定的指针执行delete class DebugDelete { public: DebugDelete(std::ostream& s = std::cerr): os(s) { } // 与任何函数模板相同,T的类型由编译器推断 template<typename T> void operator()(T* p) const { os<< "deleting unique_ptr" <<endl; delete p; } private: std::ostream& os; }
每个DebugDelete对象都有一个ostream成员,用于写入数据;还包含一个自身是模板的成员函数。我们用这个类代替delete:
double* p = new double; DebugDelete d; // 可像delete表达式一样使用的对象 d(p); // 调用DebugDelete::operator()(double*),释放p int* ip = new int; //在一个临时DebugDelete对象上调用operator()(int*) DebugDelete()(ip);
由于调用一个DebugDelete对象会delete其给定的指针,我们也可以将DebugDelete用作unique_ptr的删除器。为了重载unique_ptr的删除器,我们在尖括号内给出删除器类型,并提供一个这种类型的对象给unique_ptr的构造函数。
// 销毁p指向的喜爱那个 // 实例化Debug::operator()<int>(int* ) unique_ptr<int, DebugDelete> p(new int(), DebugDelete()); // 销毁sp指向的对象 // 实例化DebugDelete::operator()<string>(string*) unique_ptr<string, DebugDelete> sp(new string(), DebugDelete());
上述定义会这样实例化:
// DebugDelete的成员模板实例化样例 void DebugDelete::operator()(int* p) const { delete p; } void DebugDelete::operator()(string* p) const { delete p; }
类模板的成员模板
对于类模板,我们也可以为其定义成员模板。在此情况下,类和成员有着割自己的独立的模板参数。
由于希望支持不同类型序列的迭代器,因此将构造函数定义为模板:
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>类的接受两个vector<long>::iterator的构造函数 Blob<int> a2(vi.beign(), vi.end()); // 实例化Blob<string>以及接受两个list<const char*>::iterator参数的构造函数 Blob<string> a3(w.begin(), w.end());
当我们定义a1时,显示地指出编译器应该实例化一个int版本的Blob。构造函数自己的类型参数则通过begin(ia) 和end(ia)的类型来推断。
控制实例化
当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就有这个模板的一个实例。在多个文件中实例化相同模板的额外开销非常严重。
我们可以通过显示实例化来避免这一开销。
extern template declaration // 实例化声明 template declaration; // 实例化定义
declaration是一个类或函数声明,其中所有模板已被替换为模板实参:
// 实例化声明与定义 extern template class Blob<string>; // 声明 template int compare(const int&, const int&); // 定义
当编译器遇到extern时,不会在本文件生成实例化代码。
extern声明必须出现任何实例化版本之前:
// 这些模板类型必须在其它位置实例化 extern template class Blob<string>; extern template int compare(const int&, const int&); Blob<string> sa1, s2; // 实例化会出现在其它位置 //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]);//实例化出现在别的位置
对于每个实例化声明,在程序某个位置必须有显式的实例化定义。
一个类模板的实例化定义会实例化该模板所有成员,包括内联的成员函数。
在运行时绑定删除器
可以推断出,shared_ptr必须能直接访问其删除器。删除器必须保存为一个指针或一个封装了的指针。在shared_ptr运行时,我们可以随时改变删除器的类型。可以使用一种类型的删除器构造一个shared_ptr,随后使用reset赋予shared_ptr另一种类型的删除器。
在编译时绑定删除器
unique_ptr有两个模板参数,一个表示它管理的指针,另一个是删除器的类型,这是在编译时可知道的。避免了间接调用删除器的运行时开销。
模板类型推断
对于函数模板,编译器根据实参推断类型,被称为模板实参推断。
类型转换与模板类型参数
能在调用中应用于函数模板的包括:
● 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&) // 将s1转换为const是允许的 int a[10], b[42]; fobj(a, b); // 调用f(int*, int*) fref(a, b); // 错误,数组类型不匹配
使用相同模板参数类型的模板形参
如果推断出的类型不匹配的,那么调用就是错误的,如compare接受两个const T& :
long lng; compare(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; }
正常类型转换应用于普通函数实参
函数模板可以使用普通类型定义的参数,即,不涉及模板类型参数的类型。这种函数实参不进行特殊处理:它们正常地转换为对应形参的类型:
template<typename T> ostream& print(ostream& os, const T& obj) { return os << obj; }
第一个函数参数是已知类型ostream&, 第二个参数obj是模板参数类型:
print(cout, 42); // 实例化print(ostream&, int) ofstream f("output"); print(f, 10); // 使用print(ostream&, int); 将f 转换为ostream&
由于普通参数不依赖于模板参数,因此编译器会将 f 隐式转换为ostream&。
函数模板显示实参
指定显示模板实参
我们可以定义表示返回类型的三个模板参数,从而允许用户控制返回类型:
// 编译器无法推断T1,它未出现在函数参数列表中 template<typename T1, typename T2, typename T3> T1 sum(T2, T3);
每次调用sum时,调用者必须为T1提供一个显式模板实参:
// T1是显式指定的,T2和T3是从函数实参类型推断而来的 auto val3 = sum<long long>(i, lng); // long long sum(int, long)
如果我们的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, int, long>(i, lng);
正常类型转换应用于显式指定的实参
对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换:
long lng; compare(lng, 1024); // 错误:模板参数不匹配 compare<long>(lng, 1024); // 正确:实例化compare(long, long) compare<int>(lng, 1024); // 正确:实例化compare(int, int)
尾置返回类型与类型转换
我们可能希望编写一个函数,接受表示序列的一对迭代器和返回序列中的一个元素的引用:
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&
此例中,我们知道函数应该返回*beg,而且知道我们可以用decltype(*beg)来获取此表达式的类型。但是,在编译器遇到函数的参数列表之前,beg都是不存在的。为了定义此函数,我们必须使用尾置返回类型。
// 尾置返回允许我们在参数列表之后声明返回类型 template<typename It> auto fcn( It beg, It end ) -> decltype(*beg) { // 处理序列 return *beg; // 返回序列中一个元素的引用 }
解引用运算符将返回一个左值,因此通过decltype推断的类型为beg表示的元素的类型的引用。因此,如果对一个string序列调用fcn,返回类型将是string&; 如果是int序列,返回类型将是int&。
进行类型转换的标准库模板类
有时我们无法直接获得所需要的类型。例如,我们可能希望编写一个类似fcn的函数,但返回一个元素的值而非引用。
我们可以使用remove_reference来获得元素类型。remove_reference模板有一个模板类型参数有一个名为type的public类型成员。如果我们用一个引用类型实例化remove_reference<int&>,则type成员将是int。类似的,如果我们实例化remove_reference<string&>,那么type成员将是string的。
remove_reference<decltype(*beg)>::type
我们可以在函数中返回元素的拷贝:
// 为了使用模板参数的成员,必须使用typename template<typename It> auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type { // 处理序列 return *beg;// 返回序列中一个元素的拷贝 }
函数指针和实参推断
当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值,编译器使用指针的类型来推断模板实参。
如果我们有一个函数指针,它指向的函数返回int,接受两个参数,每个参数都是指向const int的引用。我们可以使用该指针指向compare的一个实例:
template<typename T> // pf1指向实例:int compare(const T&, const T&); 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的参数类型无法确定模板实参的唯一类型。对func的调用既可以实例化接受int的compare版本;也可以实例化接受string的版本。由于不能确定func的实参的唯一实例化版本,将编译失败。
我们可以通过使用显示模板实参来消除func调用的歧义:
// 正确:显式指出实例化哪个compare版本 func(compare<int>); // 传递compare(const int&, const int&)
此表达式调用的func版本接受一个函数指针,该指针指向的函数接受两个const int&参数。当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。
模板实参推断和引用
编译器会应用正常的引用绑定规则:const是底层的,不是顶层的。
从左值引用函数参数推断类型
当一个函数参数是模板类型参数的一个普通(左值)引用时(形如T&),绑定规则告诉我们,只能传递给它一个左值(如,一个变量或一个返回引用类型的表达式)。实参可以是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); // 错误:传递给一个&参数的实参必须是一个左值
从右值引用函数参数推断类型
当一个函数参数是一个右值引用时,正常绑定规则告诉我们可以传递给它一个右值:
template<typename T> void f3(T&&); f3(42); // 实参是一个int类型的右值,模板参数T是int
引用折叠和右值引用参数
C++在正常绑定规则之外定义了两个例外规则,允许这种绑定。
第一个例外规则影响右值引用参数的推断如何进行。当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断模板类型参数为实参的左值引用类型。因此,当我们调用f3(i)时,编译器推断T的类型为int&,而非int。我们不能(直接)定义一个引用的引用,但是,通过类型别名或通过模板类型参数间接定义是可以的。
第二个例外规则:如果我们间接创建一个引用的引用,则这些引用形成折叠。在所有情况下,引用会折叠成一个普通的左值引用类型。引用会折叠成一个普通的左值引用类型。只有一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。
X& &, X& &&和X&& &都会折叠成X&
X&& &&折叠成X&&
当我们将一个左值传递给f3的(右值引用)函数参数时,编译器推断T为一个左值引用类型:
f3( i ); // 实参是一个左值,模板参数T是int& f3( ci ); // 实参是一个左值,模板参数T是一个const int&
当一个模板参数T被推断为引用类型时,折叠规则告诉我们函数参数T&&折叠为一个左值引用类型。
f3的函数参数是T&&, T是int&,因此T&&是int& &&,会折叠成int&。因此,即使f3的函数参数形式是一个右值引用,此调用也会用一个左值引用类型实例化。
● 如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),则它可以被绑定到一个左值;
● 如果实参是一个左值,则推断处的模板实参类型将是一个左值引用,且函数参数将被实例化为一个(普通)左值引用参数(T&)。
我们可以将任意类型的实参传递给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右值
与非模板函数一样,第一个版本将绑定到可修改的右值,而第二个版本将绑定到左值或const右值。
理解std::move
可以用move获得一个绑定到左值上的右值引用。标准库是如下定义move的:
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(string("bye!"))中:
● 推断出的T的类型为string
● 因此,remove_reference用string实例化
● 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&
● remove_reference用string&进行实例化
● remove_reference<string&>的type成员是string
● move的返回类型仍是string&&
● move的函数参数t实例化为string& &&, 会折叠为string&
因此,这个调用实例化move<string&>,即:
string&& move(string& t)
这正是我们所期望的:我们希望将一个右值引用绑定到一个左值。这个实例的函数体返回static_cast<string&&>(t)。在此情况下,t的类型为string&, cast将其转换为string&。
从一个左值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) // v2是一个引用 { cout << v1 << " " << ++v2 <<endl; }
在这段代码中,f改变了绑定到v2的实参的值。但是,如果我们通过flip1调用f,f所做的改变就不会影响实参:
f(42, i); // f改变了实参i flip1(f, j, 42); // 通过flip1调用不会改变j
这个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); }
如果我们调用flip2(f, j, 24),将传递给t1一个左值j。但是在flip2中,推断出T1的类型为int&,意味着t1的类型会折叠为int&。由于是引用类型,t1被绑定到j上。当flip2调用 f 时,f 中的引用参数 v2 会绑定到t1, 也就是绑定到 j。当 f 递增v2时,它也同时改变了 j 的值。
如果一个函数参数是指向模板类型参数的右值引用(如T&&),它对应的实参的const属性和左值 / 右值属性将得到保持。
但是,它不能用于接受右值引用参数的函数,如:
void g(int&& i, int& j) { cout << i << " " << j << endl; }
如果我们试图通过flip2调用g,则参数t2 将被传递给 g 的右值引用参数。即使我们传递给一个右值给flip2:
flip2(g, i, 42); // 错误:不能从一个左值实例化int&&
在调用中使用std::forward保持类型信息
我们可以使用一个名为forward的新标准库设施来传递flip2的参数,它能保持原始实参的类型。类似move, forward定义在头文件utility中。与move不同,forward必须通过显式模板实参来调用。forward返回该显式实参类型的右值引用。即,forward<T>的返回类型是T&&。
通常情况下,我们使用forward传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性:
template<typename Type> intermediary(Type&& arg) { finalFcn(std::forward<Type>(arg)); // ... }
由于arg是一个模板类型参数的右值引用,Type将标识传递给arg的实参的所有类型信息。如果实参是一个右值,则Type是一个普通类型,forward<Type>将返回Type&&,如果实参是一个左值,则通过引用折叠,Type本身是一个左值引用类型,在此情况下,返回类型是一个指向左值引用的右值引用。再次对forward返回值类型进行引用折叠,返回一个左值引用类型。
当用于一个指向模板参数类型的右值引用函数参数(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)); }
如果我们调用flip(g, i, 42), i 将以Int&类型传递给g,42将以int&&类型传递给g。
重载与模板
函数模板可以被另一个模板或一个普通非模板函数重载,如果涉及函数模板,则函数匹配规则会在以下几方面受到影响:
● 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
● 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板。
● 可行函数(模板与非模板)按类型转换来排序。
● 如果恰好有一个函数提供比任何其它函数更好的匹配,则选择此函数。如果有多个函数提供同样好的匹配:优先选择非模板函数;优先选择更特例化的模板,否则有歧义。
编写重载模板
我们首先编写函数的最通用版本,将它定义为一个模板,接受一个const对象的引用:
// 打印任何我们不能处理的类型 template<typename T> string debug_rep(const T& t) { ostringstream ret; ret << t; // 使用T的输出运算符打印t的一个表示形式 return ret.str(); // 返回ret绑定的string的一个副本 }
接下来,将定义打印指针的debug_rep版本:
// 打印指针的值,后跟指针指向的对象 // 注意:此函数不能用于char* template<typename T> string debug_rep(T* p) { ostringstream ret; ret << "pointer: " << p; // 打印指针本身的值 if(p) ret << " " << debug_rep(*p); // 打印p指向的值 else ret << " null pointer"; // 或指出p为空 return ret.str(); // 返回ret绑定的string的一个副本 }
此版本生成一个string,包括指针本身的值和调用debug_rep获得的指针指向的值。
string s("hi"); cout << debug_rep(s) <<endl;
对于这个调用,只有第一个版本的debug_rep是可行的,第二个debug_rep版本要求一个指针参数,但在此调用中我们传递的是一个非指针对象。因此编译器无法从一个非指针实参实例化一个期望指针类型参数的函数模板,因此实参推断失败。由于只有一个可行函数,所以此函数被调用。
如果我们使用一个指针调用debug_rep:
cout << debug_rep(&s) << endl;
两个函数都生成可行的实例:
● debug_rep(const string*&) ,由第一个版本的debug_rep实例化而来,T被绑定到string*。
● debug_rep(string*),由第二个坂本的debug_rep实例化而来,T被绑定到string。
第二个版本的实例是精确匹配,第一个版本需要普通指针到const指针的转换,因此选择第二个版本。
多个可行模板
考虑如下调用:
const string* sp = &s; cout << debug_rep(sp) << endl;
此例中两个模板都是可行的,而且两个都是精确匹配:
● debug_rep(const string*&) 由第一个版本的debug_rep实例化而来,T被绑定到string*。
● debug_rep(const string*) 由第二个版本的debug_rep实例化而来,T被绑定到const string。
根据重载函数模板的特殊规则,此调用被解析为debug_rep(T*),即更特例化的版本。
模板debug_rep(const T&)本质上可以用于任何类型,包括指针类型。此模板比debug_rep(T*)更通用,后者只能用于指针类型。没有这条规则,传递const指针的调用永远是有歧义的。
☆ 当有多个重载模板对一个调用提供同样好的匹配,应选择最特例化的版本。
非模板和模板重载
我们定义一个普通非模板版本的debug_rep来打印双引号包围的string:
// 打印双引号包围的string string debug_rep(const string& s) { return '"' + s + '"'; }
现在,当我们对一个string调用debug_rep时:
string s("hi"); cout << debug_rep(s) << endl;
有两个同样好的可行函数:
● debug_rep<string>(const string&),第一个模板,T被绑定到string*
● debug_rep(const string&),普通非模板函数。
在本例中,两个函数具有相同的参数列表,提供同样好的匹配。编译器会选择非模板版本。
重载模板和类型转换
接下来讨论C风格字符串指针和字符串字面常量。考虑如下调用:
cout << debug_rep("hi word!") << endl; // 调用debug_rep(T*)
● debug_rep(const T&),T被绑定到char[10]
● debug_rep(T*),T被绑定到const char
● 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&)的声明必须在作用域中,否则,就可能调用错误的debug_rep版本:
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)); }
☆ 在定义任何函数之前,记得声明所有重载的函数版本,这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本。
可变参数模板
一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包。存在两种参数包:模板参数包——表示零个或多个模板;函数参数包——表示零个或多个函数参数。
用一个省略号来指出一个模板参数或函数参数表示一个包。在一个模板参数列表中,class..或typename..指出接下来的参数表示零个或多个类型的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包:
// Args是一个模板参数包,rest是一个函数参数包 // Args表示零个或多个模板类型参数 // rest表示零个或多个函数参数 template<typename T, typename... Args> void foo(const T& t, const Args& ..rest);
声明了foo是一个可变参数函数模板,它有一个名为T的类型参数,和一个名为Args的模板参数包。这个包表示零个或多个额外的类型参数。foo的函数参数列表包含一个const&类型的参数,指向T的类型,还包含一个名为rest的函数参数包,此包表示零个或多个函数参数。
与往常一样,编译器从函数的实参推断模板参数类型。对一个可变参数模板,编译器还会推断包中参数的数目:
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]&);
sizeof...运算符
当我们需要知道包中有多少元素时,可以使用sizeof...运算符。类似sizeof,sizeof...也返回一个常量表达式,而且不会对其实参求值:
template<typename... Args> void g(Args... args) { cout << sizeof...(Args) << endl; // 类型参数的数目 cout << sizeof...(args) << endl; // 函数参数的数目 }
编写可变参数函数模板
我们可以使用一个initialzier_list来定义一个可接受可变数目实参的函数。但是,所有实参必须具有相同类型(或它们的类型可以转换为同一个公共类型)。
可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
// 用来终止递归并打印最后一个元素的函数 // 此函数必须在可变参数版本print之前定义 template<typename T> ostream& print(ostream& os, const T& t) { return os << t; // 包中最后一个元素不打印分隔符 } template<typename T> ostream& print(ostream& os, const T& t, const Args&... rest) { os << t << ", "; // 打印第一个实参 return print(os, rest..); // 递归调用,打印其它实参 }
当定义可变参数版本的print时,非可变参数版本的声明必须在作用于中。否则可能会产生无限递归。
包扩展
对于一个参数包,我们除了可以获取其大小外,能做的唯一的事情是扩展。当扩展一个包时,我们还要提供用于每个扩展元素的模式。扩展一个包就是将它分解为构成的元素,对于每个元素应用模式,获得扩展后的列表。
我们通过在模式右边加一个省略号(...)来触发扩展操作。
例如:我们的print函数包含两个扩展:
template<typename T, typename... Args> ostream& print(ostream& os, const T& t, const Args&... rest) // 扩展Args { os << t << ","; return print(os, rest...); // 扩展res }
第一个扩展操作扩展模板参数包,为print生成函数参数列表。第二个扩展操作出现在对print的调用中。此模式为print调用生成实参列表。
对Args的扩展中,编译器将模式const Args&应用到模板参数包Args中的每个元素。因此,此模式的扩展结果是一个逗号分隔的零个或多个类型的列表,每个类型都形如const type&,例如:
print(cout, i, s, 42); // 包中有两个参数
最后两个实参的类型和模式一起确定了尾置参数的类型,此调用被实例化为:
ostream& print(ostream&, const int&, const string&, const int&);
第二个扩展发生在对print的递归调用中。在此情况下,模式是函数参数包的名字(即rest)。此模式扩展出一个由包中元素组成的、逗号分隔的列表。因此,这个调用等价于:
print(os, s, 42);
理解包扩展
我们可以编写第二个可变参数函数,对其每个实参调用debug_rep,然后用print打印结果string:
// 在print调用中对每个实参调用debug_rep template<typename... Args> ostream& errorMsg(ostream& os, const Args&... rest) { return print(os, debug_rep(rest)...); }
这个print调用了模式debug_rep(res),表示我们希望对函数参数包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("otherData"), debug_rep(item));
下面的模式会编译失败:
// 将包传递给debug_rep:print(os, debug_rep(a1, a2, ... ,an)) print(os, debug_rep(rest...)); // 错误:此调用无匹配函数
这段代码的问题是我们在debug_rep调用中扩展了rest,它等价于:
print(cerr, debug_rep(fcnName, code.num(), otherData, "otherData", item));
转发参数包
在新标准下,我们可以组合使用可变参数模板与forward机制来编写函数,实现将其实参不变地传递给其它函数。标准库容器的emplace_back成员是一个可变成员模板,它用其实参在容器管理的内存空间直接构造一个元素。
首先,为了保持实参中的类型信息,必须将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(); // 如果需要的话,重新分配strVec内存空间 alloc.construct(first_free++, std::forward<Args>(args)...); }
emplace_back的函数调用了chk_n_alloc来确保有足够的空间容纳一个新元素,然后调用了construct在first_free指向的位置中创建了一个元素。
construct调用中的扩展为:
std::forward<Args>(args)...
它既扩展了模板参数包Args,也扩展了函数参数包args。此模式生成如下形式的元素:
std::forward<Ti>(ti);
如果我们调用:
std::forward<int>(10), std::forward<char>(c)
通过在此调用中使用forward,我们保证如果一个右值调用emplace_back,则construct也会得到一个右值。
svec.emplace_back(s1 + s2); // 使用移动构造函数
传递给emplace_back的实参是一个右值,它以如下形式传递给construct:
std::forward<string>(string("the end"))
forward<string>的结果类型是string&&,因此construct将得到一个右值引用实参。construct会继续将此实参传递给string的移动构造函数来创建新元素。
转发和可变参数模板
可变参数函数通常将它们的参数转发给其它函数。这种函数通常具有如下形式:
// fun有零个或多个参数,每个参数都是一个模板参数类型的右值引用 template<typename... Args> void fun(Args&&... args) // 将Args扩展为一个右值引用的列表 { // work的实参既扩展Args又扩展args work(std::forward<Args>(args)...); }
模板特例化
我们可以重载compare函数来处理字符串字面常量:
// 第一个版本,可以比较任意两个类型 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 = "hi", *p2 = "mom"; compare(p1, p2); // 调用第一个模板 compare("hi", "mom"); // 调用有两个非类型参数的版本
我们无法将一个指针转换为一个数组的引用,因此当参数是p1和p2时,第二个版本的compare不可行。
为了处理字符指针,可以为第一个版本compare定义一个模板特例化版本。
定义函数模板特例化
当我们特例化一个函数模板时,必须为原模板中每个模板参数都提供实参。为了指出我们正在特例化一个模板,应使用template加一个<>:
// compare的特殊版本,处理字符数组的指针 template<> int compare(const char* const& p1, const char* const& p2) { return strcmp(p1, p2); }
当我们定义一个特例化版本时,函数参数类型必须与一个先前声明的模板中对应的类型匹配。本例中,我们特例化:
template<typename T> int compare(const T&, const T&);
我们希望定义此函数的一个特例化版本,其中T为const char*。我们的函数要求一个指向此类型const版本的引用。一个指针类型的const版本是一个常量指针,而不是指向const类型的指针。
函数重载与模板特例化
一个特例化本质上是一个实例,而非函数名的一个重载版本。
因此,特例化不影响函数匹配。
我们将一个特殊的函数定义为一个特例化版本还是一个独立的非模板函数,会影响到函数匹配。例如,我们已经定义了两个版本的compare函数模板,一个接受数组引用参数,另一个接受const &,我们还定义了一个特例化版本来处理字符指针,这对函数匹配没有影响。当我们对字符串字面常量调用compare时:
compare("hi","mom");
对此,两个函数模板都是可行的,且提供同样好的匹配。但是,接受字符数组参数的版本更特例化,因此编译器会选择它。
如果我们将其定义为一个普通的非模板函数,将会选择非模板版本。
☆ 模板及其特例化版本应该声明在同一个头文件中。所有同名模板声明应该放在前面,然后是这些模板的特例化版本。
类模板特例化
我们可以为标准库hash模板定义一个特例化版本,可以用它来将Sales_data对象保存在无序容器中。一个特例化hash类必须定义:
● 一个重载的调用运算符,它接受一个容器关键字类型的对象,返回一个size_t
● 两个类型成员,result_type和argument_type,分别调用运算符的返回类型和参数类型
● 默认构造函数和拷贝赋值运算符
// 打开std命名空间,以便特例化std::hash namespace std { template<> // 我们正在定义一个特例化版本,模板参数为Sales_data struct hash<Scales_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); } } // 关闭std命名空间,注意:右花括号之后没有分号
我们的hash<Sales_data>定义以template<>开始,指出我们正在定义一个全特例化的模板。
假设我们的特例化版本在作用域中,当将Sales_data作为容器的关键字类型时,编译器就会自动使用此特例化版本:
unordered_multiset<Sales_data> SDset;
由于hash<Sales_data>使用Sales_data的私有成员,我们必须将它声明为Sales_data的友元
template<class T> class std::hash; // 友元声明所需要的 class Sales_data { friend class std::hash<Sales_data>; };
为了让Sales_data的用户能使用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; };
第一个模板定义了最通用的版本,它可以用于任意类型的实例化:它将模板实参作为type成员的类型,接下来的两个类是原始模板的部分特例化版本。
由于一个部分特例化版本本质是一个模板,与往常一样,我们首先定义模板参数。类似任何其它特例化版本,部分特例化版本的名字与原模板的名字相同。对每个未完全确定类型的模板参数,在特例化版本的模板参数列表中都有一项与之对应。
int i; // decltype(42) 为int, 使用原始模板 remove_reference<decltype(42)>::type a; // decltype(i) 为int,使用第一个(T&)部分特例化版本 remove_reference<decltype(i)>::type b; // decltype(std::move(i))为int&&,使用第二个特例化版本 remove_reference<decltype(std::move(i))>::type c;
特例化成员而不是类
我们可以只特例化特定成员而不是特例化整个模板。例如,如果Foo是一个模板类,包含一个成员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<int>类中的一个成员,其它成员将由Foo模板提供:
Foo<string> fs; // 实例化Foo<string>::Foo() fs.Bar(); // 实例化Foo<string>::Bar() Foo<int> fi; // 实例化Foo<int>::Foo() fi.Bar(); // 使用我们特例化版本的Foo<int>::Bar()
tuple类型
不同tuple类型的成员类型也不相同,但一个tuple可以有任意数量的成员数目,每个确定的tuple类型的成员数目是固定的,但一个tuple可以和另一个tuple类型不同。
tuple支持的操作:
tuple<T1, T2, ..., Tn> t; // t是一个tuple,成员数为n,第i个成员的类型为Ti,所有成员都进行值初始化。
tuple<T1, T2, ..., Tn> t (v1, v2, ..., vn); // t 是一个tuple,成员类型为T1...T。每个成员用对应的初始值vi进行初始化,此构造函数是explicit的。
make_tuple(v1, v2, ..., vn) // 返回一个用给定值初始化的tuple,类型由初始值类型推断。
t1 == t2, t1 != t2 // 使用成员的==运算符比较
t1 relop t2 // 关系运算使用字典序
get<i>(t) // 返回t的第i个数据成员的引用:如果t是一个左值,为左值引用;如果t是一个右值,为右值引用,所有成员都是public的
tuple_size<tupleType>::value // 一个类模板,可以通过一个tuple类型来初始化,它有一个名为value的public constexpr static数据成员,类型为size_t,表示给定tuple类型中成员的数量
tuple_element<i, tupleType>::type 一个类模板,可以通过一个整型常量和一个tuple类型来初始化。
定义和初始化tuple
当我们定义一个tuple时,需要指出每个成员的类型:
tuple<size_t, size_t, size_t>threeD; // 三个成员都设置为0 tuple<string, vector<double>, int, list<int>> someVal("constants",{3.14,2.718},42,{0,1,2,3,4,5})
由于构造函数是explicit的,因此我们必须使用直接初始化语法:
tuple<size_t, size_t, size_t> threeD = {1, 2, 3}; // 错误 tuple<size_t, size_t, size_t> threeD{ 1, 2, 3 }; // 正确
标准库定义了make_tuple函数,可以用它来生成tuple对象:
// 表示书店交易记录的tuple auto item = make_tuple("0-999-78345-X", 3, 20.00);
make_tuple使用初始值的类型来推断tuple类型。item类型为:tuple<const char*, int, double>
访问tuple的成员
auto book = get<0>(item); // 返回item的第一个成员 auto cnt = get<1>(item); // 返回item的第二个成员 auto price = get<2>(item) / cnt; // 返回item的最后一个成员 get<2>(item) *= 0.8; //打折20%
和往常一样,我们从0开始计数,意味着get<0>是第一个成员。
如果不知道一个tuple准确的信息,可以用两个辅助类模板来查询tuple成员的数量和类型:
typedef decltype(item) trans; // trans是item的类型 // 返回trans类型对象中成员的数量 size_t sz = tuple_size<trans>::value; // 返回3 // cnt的类型与item第二个成员相同 tuple_element<1, trans>::type cnt = get<1>(item); // cnt是一个int
关系和相等运算符
只有两个tuple具有相同数量的成员时,我们才可以比较它们。对每对成员使用==运算符必须都是合法的。
tuple<string, string> duo("1", "2"); tuple<size_t, size_t> twoD(1, 2); bool b = (duo == twoD); // 错误:不能比较size_t和string tuple<size_t, size_t, size_t> threeD(1, 2, 3); b = (twoD < threeD); // 错误:成员数量不同 tuple<size_t, size_t> orgin(0, 0); b = (orgin < twoD); // 正确:b为true
使用tuple返回多个值
// matches有三个成员:一家书店的索引和两个指向书店vector中元素的迭代器 typedef tuple<vector<Sales_data>::size_type, vector<Sales_data>::cosnt_iterator, vector<Sales_data>::const_iterator> matches; // files保存每家书店的销售记录 // findBook返回一个vector,每家销售了给定书籍的书店在其中都有一项 vector<matches> findBook(const vector<vector<Sales_data>>& files, const string& book) { vector<matches> ret; // 初始化为空vector // 对每家书店,查找与给定书籍匹配的记录范围(如果存在的话) for(auto it = files.cbegin(); it != files.cend(); it++) { // 查找具有相同ISBN的Sales_data范围 auto found = equal_range(it->cbegin(), it->cend(), book, compareIsbn); // 记住此书店的索引以及匹配的范围 if(found.first != found.second) ret.push_back(make_tuple(it - files.cbegin(), found.first,found.second)); } return ret; }
RTTI
运行时类型识别:指向基类的指针/引用获取到实际上的类型。
(1) dynamic_cast操作符,安全类型转换。
(2) typeid操作符,可以得到实际派生类型。
应该更多的使用编译时刻类型检查。
dynamic_cast运算符
dynamic_cast<type*>(e); dynamic_cast<type&>(e); dynamic_cast<type&&>(e);
type必须是一个类类型,且通常情况下该类型应该含有虚函数;在第一种形式中,e必须是一个有效的指针;在第二种形式中,e必须是一个左值;在第三种形式中,e不能是左值。
e的类型必须满足三个条件的一个:e的类型是目标type的公有派生类;e的类型是目标type的公有基类或者e的类型就是目标type的类型。
如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0;如果转换目标是引用类型并且失败了,则dynamic_cast运算符抛出一个bad_cast异常。
指针类型的dynamic_cast
假定Base类至少含有一个虚函数,Derived是Base公有派生类。如果有一个指向Base指针的bp,则我们可以在运行时将它转换成指向Derived的指针:
if(Derived* dp = dynamic_cast<Derived*>(bp)) { // 使用dp指向的Derived对象 } else // bp 指向一个Base对象 { // 使用bp指向的Base对象 }
如果bp指向Derived对象,则上述的类型转换初始化dp并令其指向bp所指的Derived对象。此时,if 语句内部使用Derived操作的代码是安全的。否则,类型转换的结果为0,dp为0意味 if 语句的条件失败,此时else子句执行相应的Base操作。
☆ 我们可以对一个空指针执行dynamic_cast,结果是所需类型的空指针。
☆ 在条件部分执行dynamic_cast操作可以确保类型转换和结果检查在同一条表达式中完成。
引用类型的dynamic_cast
因为不存在所谓的空引用,所以对于引用类型来说无法使用与指针类型完全相同的错误报告策略。当对引用的类型转换失败时,程序抛出一个名为std::bad_cast的异常。该异常定义在typeinfo标准库的头文件中。
我们可以按照如下形式改写之前的程序,令其使用引用类型:
void f( const Base& b) { try { const Derived& d = dynamic_cast<const Derived&>(b); // 使用b引用的Derived对象 } catch (bad_cast) { // 处理类型转换失败的情况 } }
typeid运算符
typeid表达式的形式是typeid( e ),其中e可以是任意表达式或类型的名字。typeid操作的结果是一个常量对象的引用,该对象的类型是标准库类型type_info或者type_info的公有派生类型。type_info类定义在typeinfo头文件中。
typeid运算符可以作用于任意类型的表达式。和往常一样,顶层const会被忽略,如果表达式是一个引用,则typeid返回该引用所引对象的类型。当typeid作用于数组或函数时,并不会执行向指针的标准类型转换。也就是说,如果我们对数组a执行typeid(a),则所得的结果是数组类型而非指针类型。
当运算对象不属于类类型,或者是一个不包含任何虚函数的类时,typeid运算符指示的是运算对象的静态类型。而当运算对象是定义了至少一个虚函数的类的左值时,typeid的结果直到运行时才会求得。
使用typeid运算符
通常情况下,我们使用typeid比较两条表达式的类型是否相同,或者比较一条表达式的类型是否与指定类型相同:
Derived* dp = new Derived; Base* bp = dp; // 两个指针都指向Derived对象 // 在运行时比较两个对象的类型 if( typeid(*bp) == typeif(*dp)) { // bp 和 dp 指向同一类型的对象 } // 检查运行时类型是否是某种指定的类型 if( typeid(*bp) == typeid(Derived)) { // bp实际指向Derived对象 }
注意,typeid应该作用于对象,因此我们使用*bp,而不是bp:
☆ 当typeid作用于指针而非指针所指的对象时,返回的结果是该指针的静态编译时类型。
typeid是否需要运行时检查决定了表达式是否会被求值。只有当类型含有虚函数时,编译器才会对表达式求值。反之,则返回表达式的静态类型。
new/delete
调用new时,实际执行了三步操作:
(1) 调用operator new( 或者operator new[] ) 的标准库函数,分配足够大、原始、未命名内存空间。
(2) 运行构造函数以构造对象,传入初始值。
(3) 分配空间构造完成,返回对象指针。
调用delete时,实际执行了两步操作:
(1) 用指针所指的对象或数组中的元素执行对应的析构函数。
(2) 调用operator delete( 或者operator delete[] )的标准库函数释放内存空间。
::new/::delete只在全局作用于中查找匹配的operator new/delete函数
面向对象设计
class Quote { public: Quote() = default; Quote(const std::string& book, double sales_price) : bookNo(book), price(sales_price) { } string isbn() const { return bookNo;}virtual double net_price(std::size_t n) const { return n * price; } virtual ~Quote() = default private: std::string bookNo; protected: double price = 0.0; };
class Bluk_quote : public Quote { public: Bulk_quote() = default; Bulk_quote(const std::string&, double, std::size_t, double disc) : Quote(book, p), min_qty(qty), discount(disc) { }
// 首先初始化基类的部分,然后按照声明的顺序依次初始化派生类成员 double net_price(std::size_t) const override; private: std::size_t min_aty = 0; double discount =0.0; };
基类通常都应该定义一个虚析构函数,即使它不执行任何实际操作。
任何构造函数之外的非静态函数都可以是虚函数。
如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
大多数都只继承自一个类,这种形式被称为单继承。
派生类对象以及派生类向基类的类型转换
一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与该派生类继承的基类对应的子对象。如果有多个基类,这样的对象也有多个。
在一个对象中,继承自基类的部分和派生类自定义的部分不一定是连续存储的。
我们总能把派生类的对象当作基类来使用,也能将基类的指针或引用绑定到派生类的基类部分上(派生类对象中含有与基类对应的组成部分):
Quote item; // 基类对象 Bluk_quote bulk; // 派生类对象 Quote* p = &item; // p 指向Quote对象 p = &bulk; // p指向bulk的Quote部分 Quote& r = bulk; // r绑定到bulk的Quote部分
隐式地执行派生类到基类的类型转换。
派生类也必须使用基类的构造函数来初始化它的基类部分。
泛型算法
只读算法
int sum = accumulate(vec.cbegin(), vec.cend(), 0);
另一个例子,由于string定义了+运算符,可以通过调用accumulate将string元素连接起来:
string sum = accumulate(v.cbegin(), v.cend(), string(""));
注意,将空串当做一个字符串字面值传递给第三个参数是不可行的,会导致一个编译错误:
// 错误:const char* 上没有定义+运算符 string sum = accumulate(v.cbegin(), v.cend(), "");
由于const char*并没有+运算符,此调用将产生编译错误。
操作两个序列的算法
只读算法equal,用于确定两个序列是否保存相同的值。将第一个序列中的每个元素与第二个序列中的对应元素进行比较。
// roster2中的元素数目应该至少与roster1一样多 equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());
元素类型不必一样,只要我们能用==来比较两个元素类型即可。例如,在此例中,roster1可以是vector<string>,roster2可以是list<const char*>。但是我们必须能使用==来比较来自两个序列中的元素。
☆ 只接受一个单一迭代器表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。
写容器元素的算法
必须注意确保序列原大小至少不小于我们要求算法写入的元素数目。(算法不会执行容器操作,因此它们自身不可能改变容器的大小)。
fill(vec.begin(), vec.end(), 0); // 将每个元素置为0 // 将容器的一个子序列设置为10 fill(vec.begin(), vec.end() + vec.size() / 2, 10);
算法不检查写操作
一些算法接受一个迭代器来指出一个单独的目的位置。这些算法将新值赋予一个序列中的元素,从目的位置迭代器指向的元素开始:
vector<int> vec; // 空vector // 使用vec,赋予它不同值 fill_n(vec.begin(), vec.size(), 0); // 将所有元素重置为0
函数fill_n假定写入指定个元素是安全的(对应位置开始的序列至少包含n个元素)
back_inserter
保证算法有足够元素空间来容纳输出数据的方法是使用插入迭代器(insert_iterator)
赋值运算符会调用push_back将一个具有给定值的元素添加到容器:
vector<int> vec; //空向量 auto it = back_inserter(vec); // 通过它赋值会将元素添加到vec中 *it = 42; // vec中现在有一个元素,值为42
我们常常使用back_inserter来创建一个迭代器:
vector<int> vec; // 空向量 // 正确: back_insert创建一个插入迭代器 fill_n(back_insert(vec), 10, 0); // 添加十个元素到vec
拷贝算法
使用copy实现内置数组的拷贝:
int a1[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; int a2[sizeof(a1) / sizeof(*a1)]; // a2大小与a1一样 // ret指向拷贝到a2的尾元素之后的位置 auto ret = copy(begin(a1), end(a1), a2); // 把a1的内容拷贝给a2
此例中我们定义了一个名为a2的数组,并使用sizeof确保a2与数组a1包含同样多的元素。接下来调用copy完成从a1到a2的拷贝。在调用copy后,两个数组的元素具有相同的值。copy返回的是其目的位置迭代器(递增后)的值。即,ret恰好指向拷贝到a2尾元素之后的位置。
replace算法读入一个序列,并将其中所有等于给定值的元素都改为另一个值。
// 将所有值为0的元素改为42 replace(list.begin(), list.end(), 0, 42);
如果希望保留原序列不变:
replace_copy(list.begin(), list.end(), back_inserter(vec), 0, 42);
消除重复单词:
void elimDups(vector<string>& words) { // 按照字典序排序words,以便查找重复单词 sort(words.begin(), words.end()); // unique重新输入范围,使得每个单词只出现一次 // 排列在范围的前部,返回指向不重复区域之后最后一个位置的迭代器 auto end_unique = unique(words.begin(), words.end()); // 使用向量操作erase删除重复的单词 words.erase(end_unique, words.end()); }
谓词
标准库算法所使用的谓词分为两类:一元谓词和二元谓词。
// 比较函数,用来按长度排序单词 bool isShorter(const string& s1, const string& s2) { return s1.size() < s2.size(); }
sort(words.begin(), words.end(), isShorter); // 按长度由短至长排序words
lambda表达式
一个lambda表达式具有如下形式:
[capture list] (parameter list)->return type { function body }
我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体:
auto f = [] { return 42;};
在lambda中忽略括号和忽略参数列表等价于指定一个空参数列表。如果忽略返回类型,lambda根据函数体内的代码推断出返回类型。
[](const string& a, const string& b) { return a.size() < b.size(); }
stable_sort(words.begin(), words.end(), [](const string& a, const string& b) { return a.size() < b.size(); });
捕获列表
[sz](const string& a) { return a.size() >= sz; };
一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
求大于等于一个给定长度的单词有多少:
void biggies(vector<string>& words, vector<string>::size_type sz) { elimDup(words); // 将words按字典序排序,删除重复单词 // 按长度排序,长度相同的单词维持字典序 stable_sort(words.begin(), words.end(), isShorter); // 获取一个迭代器,指向第一个满足size() >= sz的元素 auto wc= find_if(words.begin(), words.end(), [sz](const string& a) { return a.size() >= sz; }); // 计算满足size >= sz的元素的数目 auto count = words.end() - wc; // 打印长度大于等于给定值的单词,每个单词后面接一个空格 for_each(wc, words.end(), [](const string& s) {cout << s << " ";}); cout << endl; }
捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和它所在函数之外的声明的名字。
lambda捕获和返回
当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。类似的,当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。
值捕获
类似参数传递,变量的捕获方式也可以是值或引用。被捕获的值是在lambda创建时拷贝,而非调用时拷贝:
void fcn1() { size_t v1 = 42; auto f = [v1] { return v1; } v1 = 0; auto j = f(); // j 为42 }
引用捕获
我们可以使用引用方式捕获变量:
void fcn2() { size_t v1 = 42; auto f2 = [&v1]{ return v1; } v1 = 0; auto j = f2(); // j为0 }
如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。
引用捕获有时是必要的,例如,我们可能希望biggies函数接受一个ostream的引用:
void biggies(vector<string>& words, vector<string>::size_type sz, ostream& os = cout, char c = ' ' ) { // 与前面例子一样的重排words代码 for_each(words.begin(), words.end(), [&os, c](const string& s) { os << s << c; }); }
我们不能拷贝ostream对象,因此捕获os的唯一方法就是捕获其引用。
如果函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此lambda也不能包含引用捕获。
一般来说,我们应该尽可能减少捕获的数据量,如果可能的话,应该避免捕获指针或引用。
隐式捕获
可以让编译器根据lambda体中的代码来推断我们要使用哪些变量。&告诉编译器采用捕获引用的方式,=表示采用值捕获的方式:
wc = find_if(words.begin(), words.end(), [=](const string& s) { return s.size() >= sz; } );
如果我们希望对一部分变量采用值捕获,对其它变量采用引用捕获,可以混合使用隐式捕获和显示捕获:
void biggies(vector<string>& words, vector<string>::size_type sz, ostream& os = cout, char c = ' ') { for_each(words.begin(), words.end9),[&,c](const string& s) {os << s << c;}); for_each(words.begin(), words.end9),[=,&os](const string& s) {os << s << c;}); }
可变lambda
如果我们希望可以改变一个被捕获变量的值,就必须在参数列表首加上关键字mutable。因此,可变lambda能省略参数列表:
void fcn3() { size_t v1 = 42; // 局部变量 // f 可以改变它所捕获的变量的值 auto f = [v1] () mutable { return ++v1; }; v1 = 0; auto j = f(); // j为43 }
void fcn4() { size_t v1 = 42; // 局部变量 // v1是一个非const 变量的引用 // 可以通过f2中的引用来改变它 auto f2 = [&v1[ { return ++v1; }; v1 = 0; auto j = f2(); // j为1 }
指定lambda返回类型
默认lambda返回void,与其它返回void的函数类似,被推断返回void的lambda不能返回值。
transform(vi.begin(), vi.end(), vi.begin(), [](int i) { return i < 0 ? -i : i; });
我们无需指定返回类型,因为可以根据条件运算符的类型推断。
但是,如果我们将程序改写为看起来等价的if语句,就会产生编译错误:
// 错误:不能推断lambda的返回类型 transform(vi.begin(), vi.end(), vi.begin(), [](int i) { if(i < 0) return -i; esle return i;});
编译器推断这个版本的lambda返回类型为void,但它返回了一个int值。
当我们需要为一个lambda定义返回类型时,必须使用尾置返回类型:
transform(vi.begin(), vi.end(), [](int i) -> int { if(i < 0) return -i; else return i; });
参数绑定
如果我们需要在很多地方使用相同的操作,通常应该定义一个函数;如果lambda的捕获列表未空,通常可以用函数来代替它。但对于捕获局部变量的lambda,用函数替换并不容易。
标准库bind函数
我们可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。
auto newCallable = bind(callable, arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定callable的参数。当我们调用newCallable的时候,newCallable会调用callable,并传递给它arg_list中的参数。
arg_list中的参数可能包含形如_n的名字,其中n是一个整数。这些参数是占位符:_1为第一个参数,_2为第二个参数,以此类推。
绑定check_size的sz参数
我们使用bind生成一个调用check_size的对象,用一个定值作为大小参数调用check_size:
// check6是一个可调用对象,接受一个string类型的参数 // 并使用此string和值6来调用check_size auto check6 = bind(check_size, _1, 6);
此bind调用只有一个占位符,表示check6只接受单一参数。占位符出现在arg_list的第一个位置,表示check6的此参数对应check_size的第一个参数,此参数是一个const string&。因此,调用check6必须传递给它一个string类型的参数,check6会将此参数传递给check_size。
string s = "hello"; bool b1 = check6(s); // check6(s)会调用check_size(s,6);
使用bind,我们可以将原来基于lambda的find_if调用:
异常处理
异常使得我们能够将问题的检测与解决过程分离开来。程序的一部分负责检测问题的出现,然后将解决该问题的任务传给程序的另一部分。
抛出异常
我们通过抛出(throwing)一条表达式来引发(raised)一个异常。被抛出的表达式的类型以及当前的调用链共同决定了哪段处理代码被用来处理该异常。当执行一个throw时,跟在throw后面的语句将不再被执行。相反,程序的控制权从throw转移到catch。
● 沿着调用链的函数可能会提前退出
● 一旦程序开始执行异常处理代码,则沿着调用链创建的对象会被销毁
栈展开
当throw出现在一个try语句块时,检查与该try块关联的catch字句,如果找到了匹配的catch,就使用该catch处理异常,如果没找到且该try嵌套在其它try里,则继续检查与外层的try匹配的catch字句。否则退出,在调用函数的外层函数中继续寻找。
以上过程被称为栈展开过程。栈展开沿着嵌套函数的调用链不断查找,直到找到了与异常匹配的catch子句位置;或者可能一直没找到匹配的catch,则退出主函数后查找过程终止。
假设找到了一个匹配的catch子句,则程序进入该子句并执行其中的代码。当执行完这个catch子句后,找到与try块关联的最后一个catch子句后的点,从这里继续执行。如果没有找到,程序将退出。因为当找不到匹配的catch时,程序将调用标准库函数terminate,负责终止程序的执行过程。
栈展开过程中对象被自动销毁
在栈展开的过程中,位于调用链上的语句块可能会提前退出,会销毁局部对象。
如果异常发生在构造函数中,当前对象可能只构造了一部分,有的成员已经初始化了,另外一些成员在异常发生前也许还没有初始化。此时我们也要确保已经构造的成员能被正确的销毁。
类似的,异常也可能发生在数组或标准库容器的初始化过程中,我们应该确保元素被正确销毁。
析构函数与异常
析构函数总是会执行的,但是函数中负责释放资源的代码可能会被跳过。如果在释放资源前代码发生了异常,则释放资源的代码将不会被执行。出于栈展开可能使用析构函数考虑,析构函数不应该抛出不能被它自身处理的异常。
异常对象
编译器使用异常抛出表达式来对异常对象进行拷贝初始化。因此,throw语句中的表达式必须有完全类型。而且如果该表达式是类类型的话,相应的类必须含有一个可访问的析构函数和可访问的拷贝或移动函数。如果是数组、函数类型,会被转换成与之对应的指针类型。
当异常出来完毕后,异常对象会被销毁。
抛出一个指向局部对象的指针是一种错误的行为,如果指针所指的对象位于某个块中,该块在catch语句之前就已经退出了,意味着catch语句之前对象已经被销毁。
当我们抛出一条表达式时,该表达式的静态编译时类型决定了异常对象的类型。如果一条throw表达式解引用一个基类指针,而该指针实际指向的是派生类对象,则跑出的对象将被切掉一部分,只有基类部分被抛出。
☆ 抛出指针要求在任何对应处理代码存在的地方,指针所指对象都必须存在。
捕获异常
声明的类型决定了处理代码所能捕获的异常类型,这个类型必须是完全类型,它可以是左值引用,但不能是右值引用。
当进入一个catch语句后,如果catch的参数类型是非引用类型,则该参数是异常对象的一个副本,在catch语句内改变该参数实际上改变的是局部副本,而非异常对象本身;如果参数是引用类型,该参数是异常对象的一个别名,此时改变参数也就是改变异常对象。
如果catch参数是基类类型,则我们可以使用其派生类类型进行初始化;如果是非引用类型,则异常对象将被切掉一部分。
查找匹配的处理代码
我们最终找到的catch未必是异常的最佳匹配,挑选出来的应该是第一个与异常匹配的catch语句,越是专门的catch就越应该置于整个catch列表的前端。
派生类的异常处理代码应该出现在基类异常处理代码之前。
除了一些极细小的差别外,要求异常的类型和catch声明的类型是精确匹配的:
● 允许从非常量到常量的转换。
● 允许从派生类到基类的转换。
● 数组被转换成指向数组类型的指针,函数被转换为指向该函数类型的指针。