导图社区 C++第18章:探讨C新标准
《C Primer Plus》第18章同步思维导图。介绍了C 11的新特性,以及回顾了之前章节的C 内容,适合初学者回顾与温习。重点介绍了右值引用、移动语义、lambda函数和可变参数模板,也是深入迈向C 的第一步。
编辑于2021-01-21 15:29:24第18章 探讨C++新标准
18.1 复习前面介绍过的C++11功能
18.1.1 新类型
(1)long long和unsigned long long
(2)char16_t和char32_t
(3)原始字符串
18.1.2 统一的初始化
(1)举例
· 内置类型
int x = {5}; double y {2.75}; short quar[5] { 4, 5, 2, 76, 1 };
· new
int*ar = new int[4] { 2, 4, 6, 7 };
· 创建对象
class Stump { int roots; double weight; public: Stump(int r, double w) : roots(r), weight(w) {} }; Stump s1(3, 15.6); // old style Stump s2{5, 43.4}; // C++11 Stump s3 = {4, 32.1}; // C++11
(2)缩窄问题
· 禁止将值存储到比它“窄”的变量中: char c1 {1.57e27}; // double-to-char, compile-time error char c2 = {459585821}; // int-to-char, compile-time error · 值在较窄类型的取值范围内,将其转换为较窄的类型也是允许的: char c3 {66}; // int-to-char, in range, allowed
· 允许转换为更宽的类型: double c4 = {66}; // int-to-double, allowed
18.1.3 声明
1. auto
(1)自动类型判断
auto maton = 112; // maton is type int auto pt = &maton; // pt is type int* double fm(double, int); auto pf = fm; // pf is type double(*)(double, int)
(2)简化模板声明
如果il是一个std::initializer_list<double>对象,则可将下述代码: for (std::initializer_list<double>::iterator p = il.begin(); p != il.end(); p++) 替换为如下代码: for (auto p = il.begin(); p != il.end(); p++)
2. decltype
(1)回顾
关键字decltype将变量的类型声明为表达式指定的类型。下面的语句的含义是,让y的类型与x相同,其中x是一个表达式: decltype(x) y;
(2)示例
double x; int n; decltype(x * n) q; // q same type as x*n decltype(&x) pd; // pd same type as &x
(3)用途
这在定义模板时特别有用,因为只有等到模板被实例化时才能确定类型: template<typename T, typename U> void ef(T t, U u) { decltype(T * U) tu; ... }
3. 返回后置类型
(1)回顾
C++11新增了一种函数声明语法:在函数名和参数列表后面(而不是前面)指定返回类型: double f1(double, int); // traditional syntax auto f2(double, int) -> double; // new syntax, return type is double
(2)用途
能够使用decltype来指定模板函数的返回类型: template<typename T, typename U> auto eff(T t, U u) -> decltype(T * U) { ... } 这里解决的问题是,在编译器遇到eff的参数列表前,T和U还不在作用域内,因此必须在参数列表后使用decltype。这种新语法使得能够这样做
4. 模板别名:using =
(1)回顾
C++11提供了另一种创建别名的语法,这在第14章讨论过: using itType = std::vector<std::string>::iterator;
(2)用途
可用于模板部分具体化,但typedef不能: template<typename T> using arr12 = std::array<T, 12>; // template for multiple aliases arr12<double> a1; // std::array<double, 12> a1; arr12<std::string> a2; // std::array<std::string, 12> a2;
5. nullptr
空指针是不会指向有效数据的指针。以前,C++在源代码中使用0表示这种指针,但内部表示可能不同。这带来了一些问题,因为这使得0即可表示指针常量,又可表示整型常量。正如第12章讨论的,C++11 新增了关键字nullptr,用于表示空指针;它是指针类型,不能转换为整型类型。为向后兼容,C++11仍允许使用0来表示空指针,因此表达式nullptr == 0为true,但使用nullptr而不是0提供了更高的类型安全。例如,可将0传递给接受int参数的函数,但如果您试图将nullptr传递给这样的函数,编译器将此视为错误。因此,出于清晰和安全考虑,请使用 nullptr—如果您的编译器支持它
18.1.4 智能指针
如果在程序中使用new从堆(自由存储区)分配内存,等到不再需要时,应使用delete将其释放。C++引入了智能指针auto_ptr,以帮助自动完成这个过程。随后的编程体验(尤其是使用STL时)表明,需要有更精致的机制。基于程序员的编程体验和BOOST库提供的解决方案, C++11摒弃了auto_ptr,并新增了三种智能指针:unique_ptr、shared_ptr和weak_ptr,第16章讨论了前两种
18.1.5 异常规范方面的修改
(1)摒弃了之前的语法: void f501(int) throw(bad_dog); // can throw type bad_dog exception void f733(long long) throw(); // doesn't throw an exception
(2)添加了关键字noexcept: void f875(short, short) noexcept; // doesn't throw an exception
18.1.6 作用域内枚举
(1)传统枚举的缺陷
· 类型检查相当低级
· 如果在同一个作用域内定义两个枚举,它们的枚举成员不能同名(枚举名的作用域为枚举定义所属的作用域)
· 枚举可能不是可完全移植的,因为不同的实现可能选择不同的底层类型
(2)新枚举class和struct
enum Old1 { yes, no, maybe }; // traditional form enum class New1 { never, sometimes, often, always }; // new form enum struct New2 { never, lever, sever }; // new form
(3)新枚举语法的改变
· 新枚举要求进行显式限定,以免发生名称冲突。因此,引用特定枚举时,需要使用New1::never和New2::never等
· 在与常规类型进行比较时,新枚举变量不能进行隐式的变换。这意味着需要进行显式的变换: int aha = 10; bool big = aha > New1::never;
· 用以下语法规定新枚举的底层数据类型为short: enum class : short New3 { a, b, c };
18.1.7 对类的修改
1. 显式转换运算符explicit
C++引入了关键字explicit,以禁止单参数构造函数导致的自动转换: class Plebe { Plebe(int); // automatic int-to-Plebe conversion explicit Plebe(double); // requires explicit use ... } ... Plebe a, b; a = 5; // implicit conversion, call Plebe(5) b = 0.5; // not allowed b = Plebe(0.5); // explicit conversion
2. 类内成员初始化
可使用等号或大括号版本的初始化,但不能使用圆括号版本的初始化。其结果与给前两个构造函数提供成员初始化列表,并指定mem1和mem2的值相同: class Session { int mem1 = 10; // in-class initialization double mem2 {1966.54}; // in-class initialization short mem3; public: Session(); // #1 Session(short s) : mem3(s) {} // #2 Session(int n, double d, short s) : mem1(n), mem2(d), mem3(s) {} // #3 ... }; 如果构造函数在成员初始化列表中提供了相应的值,这些默认值将被覆盖,因此第三个构造函数覆盖了类内成员初始化
18.1.8 模板和STL方面的修改
1. 基于范围的for循环
double prices[5] = { 4.99, 10.99, 6.87, 7.99, 8.49 }; for (double x : prices) std::cout << x << std::endl;
2. 新的STL容器
· forward_list
· unordered_map
· unordered_multimap
· unordered_set
· unordered_multiset
3. 新的STL方法
C++11新增了STL方法cbegin()和cend()。与begin()和end()一样,这些新方法也返回一个迭代器,指向容器的第一个元素和最后一个元素的后面,因此可用于指定包含全部元素的区间。另外,这些新方法将元素视为const。与此类似,crbegin()和crend()是rbegin()和rend()的const版本
4. valarray升级(*)
5. 摒弃export
C++98新增了关键字export,旨在提供一种途径,让程序员能够将模板定义放在接口文件和实现文件中,其中前者包含原型和模板声明,而后者包含模板函数和方法的定义。实践证明这不现实,因此C++11终止了这种用法,但仍保留了关键字export,供以后使用
6. 尖括号
为避免与运算符>>混淆,C++要求在声明嵌套模板时使用空格将尖括号分开: std::vector<std::list<int> > v1; // >> not ok C++11不再这样要求: std::vector<std::list<int> > v1; // >> ok in C++11
18.1.9 右值引用
C++11新增了右值引用(这在第8章讨论过),这是使用&&表示的。右值引用可关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值。右值包括字面常量(C-风格字符串除外,它表示地址)、诸如x + y等表达式以及返回值的函数(条件是该函数返回的不是引用): int x = 10; int y = 23; int&& r1 = 13; int&& r2 = x + y; double&& r3 = std::sqrt(2.0); 注意,r2关联到的是当时计算x + y得到的结果。也就是说,r2关联到的是23,即使以后修改了x或y,也不会影响到r2
将右值关联到右值引用导致该右值被存储到特定的位置,且可以获取该位置的地址。也就是说,虽然不能将运算符&用于13,但可将其用于r1。通过将数据与特定的地址关联,使得可以通过右值引用来访问该数据
18.2 移动语义和右值引用
18.2.1 为何需要移动语义
1. 移动语义的介绍
在按值返回对象时,原有的对象并不被释放,也不创建新的临时对象,而是将该对象的记录做了修改,其所有权转让给其他变量
2. 目的
优化内存控制,减少数据的转移次数
3. 原因介绍
当我们定义的类使用动态内存分配时,若使用函数返回这个类的对象,由于是按值传递,需要经历三个步骤: · 创建这个对象的临时副本 · 返回临时副本(原来的对象已经被释放) · 删除临时副本 因此我们实际上对原对象进行了3次操作。假如该对象很大,则会大大降低程序的效率
4. 实现过程
(1)右值引用的使用
(2)定义移动构造函数
18.2.2 一个移动示例(部分)
1. 类定义
class Useless { private: int n; // number of elements char * pc; // pointer to data static int ct; // number of objects void ShowObject() const; public: ... Useless(const Useless & f); // regular copy constructor Useless(Useless && f); // move constructor ... }; int Useless::ct = 0;
2. 说明
引用f将指向左值对象one,因此下面的语句将使用的构造函数: Useless two = one; // calls copy constructor
程序不能对同一个地址调用delete[]两次。为避免这种问题,移动构造函数随后将原来的指针设置为空指针,因为对空指针执行delete[]没有问题。这种夺取所有权的方式常被称为窃取(pilfering): Useless::Useless(Useless && f): n(f.n) { ++ct; pc = f.pc; // steal address f.pc = nullptr; // give old object nothing in return f.n = 0; ShowObject(); }
不能在移动构造函数的参数声明中使用const,因为需要修改其对象
18.2.3 移动构造函数解析
1. 移动语义发生的两个步骤
(1)右值引用让编译器知道何时可使用移动语义: Useless two = one; // matches Useless::Useless (const Useless&) Useless four(one + three); // matches Useless::Useless (Useless&&)
(2)编写移动构造函数,使其提供所需的行为
18.2.4 赋值
1. 复制赋值运算符
Useless& Useless::operator=(const Useless& f) // copy assignment { if (this == &f) return *this; delete[] pc; n = f.n; pc = new char[n]; for (int i = 0; i < n; i++) pc[i] = f.pc[i]; return *this; }
2. 移动赋值运算符
Useless& Useless::operator=(Useless&& f) // move assignment { if (this == &f) return *this; delete[] pc; n = f.n; pc = f.pc; f.n = 0; f.pc = nullptr; return *this; }
18.2.5 强制移动
移动构造函数和移动赋值运算符使用右值。如果要让它们使用左值,该如何办呢?为此,可使用运算符static_cast<>将对象的类型强制转换为Useless &&,但C++11提供了一种更简单的方式————使用头文件utility中声明的函数std::move(): four = std::move(one); // forced move assignment, one become empty
18.3 新的类功能
18.3.1 特殊的成员函数
1. 特殊的成员函数
(1)默认构造函数
(2)复制构造函数
(3)复制赋值运算符
(4)析构函数
(5)移动构造函数
(6)移动赋值运算符
C++11新增
2. 编译器提供的函数
(1)您提供了2、3或4,编译器将不会自动提供5和6
(2)您提供了5或6,编译器将不会自动提供2和3
(3)编译器提供的默认移动构造函数和移动赋值运算符的工作方式与复制版本类似:执行逐成员初始化并复制内置类型。所以并不能起到你想要的的效果 ->_->
18.3.2 默认的方法和禁用的方法
1. 默认
您可使用关键字default显式地声明类方法的默认版本(仅限上述6个): class Someclass { public: Someclass(Someclass&&); Someclass() = default; // use compiler-generated default constructor Someclass(const Someclass&) = default; Someclass& operator=(const Someclass&) = default; ... };
2. 禁用
另一方面,关键字delete可用于禁止编译器使用特定方法(任何类成员函数)。例如,要禁止复制对象,可禁用复制构造函数和复制赋值运算符: class Someclass { public: Someclass() = default; Someclass(const Someclass&) = delete; Someclass& operator=(const Someclass&) = delete; Someclass(Someclass&&) = default; Someclass& operator=(Someclass&&) = default; Someclass& operator+(const Someclass&) const; ... }; 移动操作使用的右值引用只能关联到右值表达式,这意味着: Someclass one; Someclass two; Someclass three(one); // not allowed, one an lvalue Someclass four(onr + two); // allowed, expression is an rvalue
18.3.3 委托构造函数
C++11允许您在一个构造函数的定义中使用另一个构造函数。这被称为委托,因为构造函数暂时将创建对象的工作委托给另一个构造函数。委托使用成员初始化列表语法的变种: class Notes{ int k; double x; std::string st; public: Notes(int kk, double xx, std::string stt) : k(kk), x(xx), st(stt) {} Notes() : Notes(0, 0.01, "Oh") {}
18.3.4 继承构造函数
1. using声明
class C1 { ... public: int fn(int j) { ... } double fn(double w) { ... } void fn(const char* s) { ... } }; class C2 : public C1 { ... public: using C1::fn; double fn(double) { ... } }; C2中的using声明让C2对象可使用C1的三个fn()方法,但将选择C2而不是C1定义的方法fn(double)
2. 继承构造函数
C++11将上述方法用于构造函数。这让派生类继承基类的所有构造函数(默认构造函数、复制构造函数和移动构造函数除外),但不会使用与派生类构造函数的特征标匹配的构造函数: class BS { int q; double w; public: BS() : q(0), w(0) {} BS(int k) : q(k), w(100) {} BS(double x) : q(-1), w(x) {} BS(int k, double x) : q(k), w(x) {} void Show() const { std::cout << q << w << '\n'; } }; class DR : public BS { short j; public: using BS::BS; DR() : j(-100) {} // DR needs its own default constructor DR(double x) : BS(2 * x), j(int(x)) {} DR(int i) : j(-2), BS(i, 0.5 * i) {} void Show() const { std::cout << j ; BS::Show(); } }; int main() { DR o1, o2(18.81), o3(10, 1.8); ... } 由于没有构造函数DR(int, double),因此创建DR对象o3时,将使用继承而来的BS(int, double)。请注意,继承的基类构造函数只初始化基类成员
18.3.5 管理虚方法
1. 问题的提出
假设基类声明了一个虚方法,而您决定在派生类中提供不同的版本,这将覆盖旧版本。但正如第13章讨论的,如果特征标不匹配,将隐藏而不是覆盖旧版本: class Action { int a; public: Action(int i = 0) : a(i) {} int val() const { return a; } virtual void f(char ch) const { std::cout << val() << ch; } }; class Bingo : public Action { public: Bingo(int i = 0) : Action(i) {} virtual void f(char* ch) const { std::cout << val() << ch << "!"; } }; 由于类Bingo定义的是f(char* ch)而不是f(char ch),将对Bingo对象隐藏f(char ch),这导致程序不能使用类似于下面的代码: Bingo b(10); b.f('s'); // works for Action object, fails for Bingo object
2. 两个管理方法
(1)override
在C++11中,可使用虚说明符override指出您要覆盖一个虚函数:将其放在参数列表后面。如果声明与基类方法不匹配,编译器将视为错误。因此,下面的Bingo::f()版本将生成一条编译错误消息: virtual void f(char* ch) const override { std::cout << val() << ch << "!"; }
(2)final
说明符final解决了另一个问题。您可能想禁止派生类覆盖特定的虚方法,为此可在参数列表后面加上final。例如,下面的代码禁止Action的派生类重新定义函数f(): virtual void f(char ch) const final { std::cout << val() << ch; }
18.8 语言变化
18.8.1 Boost项目
Boost库是C++编程的重要部分,给C++11带来了深远影响。Boost项目发起于1998年,当时的C++库工作小组主席Beman Dawes召集其他几位小组成员制定了一项计划,准备在标准委员会的框架外创建新库。该计划的基本理念是,创建一个充当开放论坛的网站,让人发布免费的C++库。这个项目提供有关许可和编程实践的指南,并要求对提议的库进行同行审阅。其最终的成果是,一系列得到高度赞扬和广泛使用的库。这个项目提供了一个环境,让编程社区能够检验和评估编程理念以及提供反馈
18.8.2 TRI
TR1(Technical Report 1)是C++标准委员会的部分成员发起的一个项目,它是一个库扩展选集,这些扩展与C++98标准兼容,但不是必不可少的。这些扩展是下一个C++标准的候选内容。TR1库让C++社区能够检验其组成部分的价值。当标准委员会将TR1的大部分内容融入C++11时,面对的是众所皆知且经过实践检验的库
18.8.3 使用Boost(*)
18.7 C++11新增的其他功能
18.7.1 并行编程
1. 介绍
当前,为提高计算机性能,增加处理器数量比提高处理器速度更容易。因此,装备了双核、四核处理器甚至多个多核处理器的计算机很常见,这让计算机能够同时执行多个线程,其中一个处理器可能处理视频下载,而另一个处理器处理电子表格
2. 用处
考虑单向链表的搜索:程序必须从链表开头开始,沿链接依次向下搜索,直到到达链表末尾;在这种情况下,多线程的帮助不大
考虑到数组的随机存取特征,可让一个线程从数组开头开始搜索,并让另一个线程 从数组中间开始搜索,这将让搜索时间减半
3. thread_local关键字
关键字thread_local将变量声明为静态存储,其持续性与特定线程相关;即定义这种变量的线程过期时,变量也将过期
18.7.2 新增的库
1. random
头文件random支持的可扩展随机数库提 供了大量比rand()复杂的随机数工具。例如,您可以选择随机数生成器和分布状态,分布状态包括均匀分布(类似于rand())、二项式分布和 正态分布等
2. chrono
头文件chrono提供了处理时间间隔的途径
3. tuple
头文件tuple支持模板tuple。tuple对象是广义的pair对象。pair对象可存储两个类型不同的值,而tuple对象可存储任意多个类型不同的值
4. ratio
头文件ratio支持的编译阶段有理数算术库让您能够准确地表示任何有理数,其分子和分母可用最宽的整型表示。它还支持对这些有理数进行算术运算
5. regex
在新增的库中,最有趣的一个是头文件regex支持的正则表达式库。正则表达式指定了一种模式,可用于与文本字符串的内容匹配。例如,方括号表达式与方括号中的任何单个字符匹配,因此[cCkK]与c、C、k和K都匹配,而[cCkK]at与单词cat、Cat、kat和Kat都匹配。其他模式包括与一位数字匹配的\d、与一个单词匹配的\w、与制表符匹配的\t等。在C++中,斜杠具有特殊含义,因此对于模式\d\t\w\d(即依次为一位数字、制表符、单词和一位数字),必须写成字符字面量“\d\t\w\d”,即使用\表示\。这是引入原始字符串的原因之一(参见第4章),它让您能够将该模式写成R“\d\t\w\d”
18.7.3 低级编程
1. 介绍
低级编程中的“低级”指的是抽象程度,而不是编程质量。低级意味着接近于计算机硬件和机器语言使用的比特和字节。对嵌入式编程和改善操作的效率而言,低级编程很重要。C++11给低级编程人员提供了一些帮助
2. 变化
(1)放松了POD(Plain Old Data)的要求
(2)允许共用体的成员有构造函数和析构函数,这让共用体更灵活;但保留了其他一些限制,如成员不能有虚函数。在需要最大程度地减少占用的内存时
(3)constexpr机制让编译器能够在编译阶段计算结果为常量的表达式,让const变量可存储在只读内存中,这对嵌入式编程来说很有用
18.7.4 杂项
1. C99引入了依赖于实现的扩展整型,C++11继承了这种传统。在使用128位整数的系统中,可使用这样的类型。在C语言中,扩展类型由头文件stdint.h支持,而在C++中,为头文件cstdint
2. C++11提供了一种创建用户自定义字面量的机制:字面量运算符(literal operator)。使用这种机制可定义二进制字面量,如1001001b,相应的字面量运算符将把它转换为整数值
3. C++提供了调试工具assert。这是一个宏,它在运行阶段对断言进行检查,如果为true,则显示一条消息,否则调用abort()。断言通常是程序员认为在程序的某个阶段应为true的东西
4. C++11新增了关键字static_assert,可用于在编译阶段对断言进行测试。这样做的主要目的在于,对于在编译阶段(而不是运行阶段)实例化的模板,调试起来将更简单
5. C++11加强了对元编程(metaprogramming)的支持。元编程指的是编写这样的程序,它创建或修改其他程序,甚至修改自身。在C++中,可使用模板在编译阶段完成这种工作
18.6 可变参数模板
18.6.1 模板和函数参数包
C++11提供了一个用省略号表示的元运算符(meta-operator),让您能够声明表示模板参数包的标识符,模板参数包基本上是一个类型列表。同样,它还让您能够声明表示函数参数包的标识符,而函数参数包基本上是一个值列表。其语法如下: template<typename... Args> // Args is a template parameter pack void show_list1(Args... args) // args is a function parameter pack { ... } 其中,Args是一个模板参数包,而args是一个函数参数包。与其他参数名一样,可将这些参数包的名称指定为任何符合C++标识符规则的名称。Args和T的差别在于,T与一种类型匹配,而Args与任意数量(包括零)的类型匹配。请看下面的函数调用: show_list1('S', 80, "sweet", 4.5); 在这种情况下,参数包Args包含与函数调用中的参数匹配的类型:char、int、const char *和double。更准确地说,这意味着函数参数包args包含的值列表与模板参数包Args包含的类型列表匹配—无论是类型还是数量。在上面的示例中,args包含值‘S’、80、“sweet”和4.5。
18.6.2 展开参数包
但函数如何访问这些包的内容呢?索引功能在这里不适用,即您不能使用Args[2]来访问包中的第三个类型。相反,可将省略号放在函数参数包名的右边,将参数包展开。假设有如下函数调用: show_list1(5, 'L', 0.5); 在该函数内部,下面的调用: show_list1(args...); 将展开成如下所示: show_list1(5, 'L', 0.5);
18.6.3 在可变参数模板函数中使用递归
1. 改变
核心理念是,将函数参数包展开,对列表中的第一项进行处理,再将余下的内容传递给递归调用,以此类推,直到列表为空。与常规递归一样,确保递归将终止很重要。这里的技巧是将模板头改为如下所示: template<typename T, typename... Args> void show_list3(T value, Args... args) { std::cout << value << ", "; show_list3(args...); }
2. 改进
添加一个处理一项的模板,并让其行为与通用模板稍有不同: template<typename T> void show_list3(T value) { std::cout << value << '\n'; } 这样,当args包缩短到只有一项时,将调用这个版本,而它打印换行符而不是逗号。另外,由于没有递归调用show_list3(),它也将终止递归
另一个可改进的地方是,当前的版本按值传递一切。对于这里使用的简单类型来说,这没问题,但对于cout可打印的大型类来说,这样做的效率很低。在可变参数模板中,可指定展开模式(pattern)。为此,可将下述代码: void show_list3(T value, Args... args) 替换为如下代码: void show_list3(const T& value, const Args&... args)
18.5 包装器
18.5.1 包装器function及模板的低效性
1. 包装器
C++提供了多个包装器(wrapper,也叫适配器[adapter])。这些对象用于给其他编程接口提供更一致或更合适的接口
2. 代码
template <typename T, typename F> T use_f(T v, F f) { ... return f(v); } class Fp { double z_; public: Fp(double z = 1.0) : z_(z) {} double operator()(double p) { return z_*p; } }; class Fq { double z_; public: Fq(double z = 1.0) : z_(z) {} double operator()(double q) { return z_+ q; } };
int main() { double y = 1.21; cout << "Function pointer dub:\n"; cout << " " << use_f(y, dub) << endl; cout << "Function pointer sqrt:\n"; cout << " " << use_f(y, square) << endl; cout << "Function object Fp:\n"; cout << " " << use_f(y, Fp(5.0)) << endl; cout << "Function object Fq:\n"; cout << " " << use_f(y, Fq(5.0)) << endl; cout << "Lambda expression 1:\n"; cout << " " << use_f(y, [](double u) {return u * u; }) << endl; cout << "Lambda expresson 2:\n"; cout << " " << use_f(y, [](double u) {return u + u / 2.0; }) << endl; return 0; }
3. 低效性说明
程序调用模板函数use_f()6次。在每次调用中,模板参数T都被设置为类型double。模板参数F呢? 每次调用时,F都接受一个double值并返回一个double值,因此在6次use_of()调用中,好像F的类型都相同,因此只会实例化模板一次。 但实际上静态成员count有5个不同的地址,这表明模板use_f()有5个不同的实 例化。
4. 原因
首先,来看下面的调用: use_f(y, dub); 其中的dub是一个函数的名称,该函数接受一个double参数并返回一个double值。函数名是指针,因此参数F的类型为double(*)(double):一个指向这样的函数的指针,即它接受一个double参数并返回一个double值。下面的use_f(y, square)也是如此。 在接下来的两个use_f()调用中,第二个参数为对象,F的类型分别为Fp和Fq,因为将为这些F值实例化use_f()模板两次。最后,最后两个调用将F的类型设置为编译器为lambda表达式使用的类型
18.5.2 修复问题
1. 说明
包装器function让您能够重写上述程序,使其只使用use_f()的一个实例而不是5个。上述6个函数符参数它们的调用特征标(call signature)相同。调用特征标是有返回类型以及用括号括起并用头号分隔的参数类型列表定义的,因此,这六个实例的调用特征标都是double(double)。
2. function功能
模板function是在头文件functional中声明的,它从调用特征标的角度定义了一个对象,可用于包装调用特征标相同的函数指针、函数对象或lambda表达式。例如,下面的声明创建一个名为fdci的function对象,它接受一个char参数和一个int参数,并返回一个double值: std::function<double(char, int)> fdci; 然后,可以将接受一个char参数和一个int参数,并返回一个double值的任何函数指针、函数对象或lambda表达式赋给它
3. 使用
int main() { double y = 1.21; function<double(double)> ef1 = dub; function<double(double)> ef2 = square; function<double(double)> ef3 = Fq(10.0); function<double(double)> ef4 = Fp(10.0); function<double(double)> ef5 = [](double u) {return u * u; }; function<double(double)> ef6 = [](double u) {return u + u / 2.0; }; cout << "Function pointer dub:\n"; cout << " " << use_f(y, ef1) << endl; cout << "Function pointer sqrt:\n"; cout << " " << use_f(y, ef2) << endl; ... return 0; }
18.5.3 其他方式
1. 建立临时对象
只使用一个临时function<double (double)>对象,将其用作函数use_f()的参数: typedef function<double(double)> fdd; cout << use_f(y, fdd(square)) << endl;
2. 更改模板
让use_f()的第二个实参与形参f匹配,但另一种方法是让形参f的类型与原始实参匹配。为此,可在模板use_f()的定义中,将第二个参数声明为function包装器对象,如下所示: template <typename T> T use_f(T v, std::function<T(T)> f) { return f(v); } 这样函数调用将如下: cout << " " << use_f<double>(y, dub) << endl;
18.4 Lambda函数
18.4.1 比较函数指针、函数符和Lambda函数
1. 函数指针的使用
假设您要生成一个随机整数列表,并判断其中多少个整数可被3整除,多个少整数可被13整除。生成这样的列表很简单。一种方案是,使用vector<int>存储数字,并使用STL算法generate()在其中填充随机数: #include <vector> #include <algorithm> #include <cmath> ... std::vector<int> numbers(1000); std::generate(numbers.begin(), numbers.end(), std::rand); 函数generate()接受一个区间(由前两个参数指定),并将每个元素设置为第三个参数返回的值,而第三个参数是一个不接受任何参数的函数对象
通过使用算法count_if(),很容易计算出有多少个元素可被3整除。与函数generate()一样,前两个参数应指定区间,而第三个参数应是一个返回true或false的函数对象。函数count_if()计算这样的元素数,即它使得指定的函数对象返回true: bool f3(int x) { return x % 3 == 0; } bool f13(int x) { return x % 13 == 0; } 定义上述函数后,便可计算复合条件的元素数了,如下所示: int count3 = std::count_if(numbers.begin(), numbers.end(), f3); int count13 = std::count_if(numbers.begin(), numbers.end(), f13);
2. 函数符的使用
函数符的优点之一是可使用同一个函数符来完成这两项计数任务。下面是一种可能的定义: class f_mod { int dv; public: f_mod(int d = 1) : dv(d) {} bool operator()(int x) { return x % dv == 0; } }; 这为何可行呢?因为可使用构造函数创建存储特定整数值的f_mod对象: f_mod obj(3); // f_mod.dv set to 3 构造函数本身可用作诸如count_if()等函数的参数: count3 = std::count_if(numbers.begin(), numbers.end(), f_mod(3));
3. lambda的使用
(1)lambda函数介绍
· 定义
与前述函数f3()对应的lambda如下: [](int x) { return x % 3 == 0; } 这与f3()的函数定义很像: bool f3(int x) { return x % 3 == 0; }
· 与普通函数的差别
使用[]替代了函数名(这就是匿名的由来)
没有声明返回类型。返回类型相当于使用decltype根据返回值推断得到的,这里为bool。如果lambda不包含返回语句,推断出的返回类型将为void
· 注意
仅当lambad表达式完全由一条返回语句组成时,自动类型推断才管用;否则,需要使用新增的返回类型后置语法: [](double x) -> double {int y = x; return x - y; } // return type is double
(2)使用
就这个示例而言,您将以如下方式使用该lambda: count3 = std::count_if(numbers.begin(), numbers.end(), [](int x) { return x % 3 == 0; }); 也就是说,使用使用整个lambad表达式替换函数指针或函数符构造函数
18.4.2 为何使用Lambda
1. 距离
很多程序员认为,让定义位于使用的地方附近很有用: · 无需翻阅多页的源代码,以了解函数调用count_if()的第三个参数 · 如果需要修改代码,涉及的内容都将在附近 从这种角度看,lambda是理想的选择,因为其定义和使用是在同一个地方进行的;而函数是最糟糕的选择,因为不能在函数内部定义其他函数,因此函数的定义可能离使用它的地方很远。函数符是不错的选择,因为可在函数内部定义类(包含函数符类),因此定义离使用地点可以很近
2. 简洁
从简洁的角度看,函数符代码比函数和lambda代码更繁琐。就上面的除余的例子而言,并非必须编写lambda两次,而可给lambda指定一个名称,并使用该名称两次: auto mod3 = [](int x) { return x % 3 == 0; } // mod3 a name for the lambda 您甚至可以像使用常规函数那样使用有名称的lambda: bool result = mod3(z); // result is true if z % 3 == 0 然而,不同于常规函数,可在函数内部定义有名称的lambda。mod3的实际类型随实现而异,它取决于编译器使用什么类型来跟踪lambda
3. 效率
这三种方法的相对效率取决于编译器内联那些东西。函数指针方法阻止了内联,因为编译器传统上不会内联其地址被获取的函数,因为函数地址的概念意味着非内联函数。而函数符和lambda通常不会阻止内联
4. 功能
lambad可访问作用域内的任何动态变量;要捕获要使用的变量,可将其名称放在中括号内: · [z]:按值访问变量z · [&z]:按引用访问变量z · [&]:按引用访问所有动态变量 · [=]:按值访问所有动态变量 · [ted, &ed]:按值访问ted以及按引用访问ed · [&, ted]:按值访问ted以及按引用访问其他所有动态变量 · [=, &ed]:按引用访问ed以及按值访问其他所有动态变量