Chapter 18 探讨C++新标准
本章先复习前面的内容,增加的内容有:
- 移动语义和右值引用
- Lamdba表达式
- 包装器模板function
- 可变参数模板
18.1 复习前面介绍过的C++11功能
C++11有很多改进:
18.1.1 新类型
C++11新增了类型long long和unsigned long long,以支持64位(或更宽)的整形;新增了char16_t和char32_t,以支持16位和32位的字符表示;还新增了”原始“字符串。
18.8.2 统一的初始化
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有内置类型和用户定义的类型,可添加(=),也可不添加;
列表初始化语法也可用于new表达式中;
创建对象时,也可使用大括号括起来的列表调用构造函数;
**如果类有将模板std::initializer_list作为参数的构造函数,则只有该构造函数可以使用列表初始化形式。
1.缩窄
初始化列表语法可防止缩窄,禁止将数值赋给无法存储它的数值变量,而常规初始化允许程序员执行无意义的操作。
2.std::initializer_list
C++提供了模板类initializer_list,可将其用作构造函数的参数。
链表中的元素必须是同一种类型或可以转换为同一种类型。
STL容器提供了将initialzer_list作为参数的构造函数。
头文件initializer_list提供了对模板类的支持,可以将initializer_list用作常规函数的参数。
18.1.3 声明
简化声明:
1.auto
自动类型推断,要求进行显示初始化,让编译器能够将变量的类型设置为初始值的类型,同时可以简化模板声明。
2.decltype
decltype将变量的类型声明为表达式指定的类型。
delctype((j)) i 3; // i3 type int &
3.返回类型后置
在函数名和参数列表后面指定返回的类型:
template<typename T, typename U>
auto f(T t, U u) -> delctype(T * U);
4.模板别名:using=
using=可以用于模板部分具体化。
5.nullptr
空指针是不会指向有效数据的指针。处于清晰和安全考虑,在支持nullptr是,请使用它。
18.1.4 智能指针
C++新增了三种智能指针:unique_ptr、shared_ptr和weak_ptr
新增的智能智能都可以与STL容器和移动语义协同工作。
18.1.5 异常规范方面的修改
C++摒弃了异常规范,但添加了noexcept来指出函数不会引发异常。
18.1.6 作用域内枚举
为实现枚举的可完全移植,C++新增了一种枚举。这种枚举使用class 或struct定义。
18.1.7 对类的修改
1.显示转换运算符
C++很早支持对象自动转换,但自动类型转换可能引发意外问题,C++引入关键字explicit,禁止单参数构造函数导致的自动转换。
C++11拓展了explicit,使得可对转换函数做类似的处理。
2.类成员初始化
C++可以在类定义中,初始化成员,可以使用=和{}来初始化成员,但不能使用()进行初始化。
18.1.8 模板和STL方面的修改
1.基于范围的for循环
如果要使用基于范围的for循环修改数组或容器的每个元素,可以使用引用类型:
for(auto & x : vi)
x = rand();
2.新的STL容器
增加了forward_list和四种无序容器。forward_list是一种单向链表,只能沿一个方向遍历。
C++11新增了array模板。
3.新的STL方法
C++11新增了STL方法cbegin()和cend(),返回的是begin()和end()的const版本。
4. valarray升级
C++添加了两个函数(begin()和end()),接受valarray作为参数,并返回迭代器。
5.摒弃export
C++98新增了关键字export,提供一种途径,让程序员能够将模板定义放在接口文件和实现文件中。C++11终止了这种用法,但保留了关键字export。
6.尖括号,为了避免与运算符>>混淆,C++要求声明嵌套模板时使用空格将尖括号分开,C++不在这样要求。
18.1.9 右值引用
左值引用使标识符关联到左值,左值是一个数据的表达式(如变量名和解除引用的指针),程序可获取其地址。
C++新增了右值引用,使用&&表示,右值引用可关联到右值,即可出现在赋值表达式右边,但不能对齐应用地址运算符的值。右值包括字面常量(C-风格字符串除外)、诸如x+y表达式以及返回值的函数(该函数的的返回值不是引用)。
18.2 移动语义和右值引用
18.2.1 为何需要移动语义
有的复制过程,只是复制和创建对象,只有复制构造函数会造成大量的无用功。如果编译器不删除临时对象,将临时对象的数据所有权转让出去。
要实现移动语义,需要采取某种方式,让编译器直到什么时候需要复制,什么时候不需要。这就是右值引用发挥作用的地方。
定义时可以定义两个构造函数,一个是常规复制构造函数,另一个是移动复制构造函数。移动复制构造函数只调整记录。
18.2.2 一个移动示例
注意:转义数据所有权之后,要将初始的数据指针清零。
18.2.3 移动构造函数解析
虽然右值引用可以支持移动语义,但这并不会神奇发生。要让移动语义发生,需要两个步骤:
- 首先,右值引用让编译器直到何时使用移动语义
- 编写移动构造函数,提供所需的行为
在引用右值引用之前,如果实参为右值,const引用形参将指向一个临时变量,该临时变量被初始化为右值的值。
18.2.4 赋值
赋值也可以写两个,一个是复制赋值运算符,一个是移动赋值运算符。
18.2.5 强制移动
想要强制使用移动构造函数或移动赋值运算符来保留选定的对象,需要让左值使用移动构造函数,有两种方法:
- 运算符static_cast<>将对象的类型强制转换为Useless &&
- 使用头文件utility和函数std::move()
18.3 类的新功能
18.3.1 特殊的成员函数
在原有4个成员函数(默认构造函数、复制构造函数、复制赋值运算符和析构函数)的基础上,C++11新增了两个:
- 移动构造函数
- 移动赋值运算符
如果没有提供赋值构造函数,而代码又需要使用它,编译器将提供一个默认复制构造函数;移动构造函数也是同理。
类似情况下,编译器将提供默认的赋值运算符和默认的移动运算符。
例外:如果提供了复制构造函数或赋值运算符,编译器将不会自动提供移动构造函数和移动赋值运算符;反之也一样。
18.3.2 默认的方法和禁用的方法
提供了移动构造函数后,编译器不会自动创建默认的构造函数、复制构造函数和复制赋值构造函数,可以使用default显示声明这些方法的默认版本:
class Someclass
{
public:
Someclass(Someclass &&);
Someclass() = default;
Someclass(const Someclass &) = deafult;
Someclass & operator=(const Someclass &) = default;
...
};
同时也可以使用关键字delete用于禁止编译器使用特定方法,例如要禁止复制对象,可禁用复制构造函数和复制赋值运算符:
class Someclass
{
public:
Someclass() = default; // use compiler-generated default constructor
// disable copy consturctor and copy assignment operator
Someclass(const Someclass &) = delete;
Someclass & operator=(const Someclass &) = delete;
// use compiler-generated move constructor and move assignment operator:
Someclass(Someclass &&) = default;
Someclass & operator=(Someclass &&) = default;
Someclass & operator+(const Someclass &) const;
...
};
关键字default只能用于6个特殊成员函数,但delete可用于任何成员函数。delete可以用于禁止特定的转换。
18.3.3 委托构造函数
C++11允许在一个构造函数中,使用另一个构造函数,这被称为委托。
18.3.4 继承构造函数
C++11提供了一种让派生类可以继承基类构造函数的机制。
使用using声明。这让派生类继承基类的所有构造函数(默认构造函数、复制构造函数和移动构造函数除外),不会使用于派生类的构造函数的特征标匹配的构造函数。
18.3.5 管理虚方法:override 和 final
虚方法对实现多态类层次结果很重要。
虚说明符override指出你要覆盖一个虚函数:将其放在参数列表后面。如果声明与基类方法不匹配,编译器将视为错误。
如果想禁止派生类覆盖基类特定的虚方法,可在参数列表后面加上final。
override和final并非关键字,而是具有特殊含义的标识符。
18.4 lambda函数
lambda函数也叫lambda表达式,简称lambda。
18.4.1 比较函数指针、函数符和Lambda函数
函数指针的声明和定义的距离较远。
函数符是一个类对象,函数符可以使用同一个函数符完成多项任务。
C++11中可以使用匿名函数定义lambda作为参数。[]代替了函数名,没有声明返回类型,返回类型使用delctyp根据返回值推断得到的。
仅当lambda表达式完全由一条返回语句组成是,自动推向类型才管用,否则:
[](double x)->double{int y = x; return x - y; } // return type is double
18.4.2 为何使用lambda
从4个方面讨论这个问题:距离、简洁、效率和功能。
定义位于使用的地方附近很有用,距离:
- lambda是理想的选择,使用和定义在同一处
- 函数是最糟的选择,不能在函数内部定义其他函数
- 函数符不错,可以使定义离使用地方很近
简洁:
- 函数符繁琐
- 函数和lambda一样
效率,相对效率取决于编译器内联:
- 函数指针方法阻止了内联,
- 函数符和lambda通常不会阻止内联
功能:lambda有一些额外的功能,lambda可以访问作用域内的任何动态变量。
且可以按两种方式访问,[z],按值访问z;[&z],按引用访问z;
[&],可以按引用访问所有动态变量,[=]按值访问所有动态变量。
还可以混合这两种方式。
18.5 包装器
C++提供了多个包装器(wrapper,也叫[adapter])。这些对象用于给其他编程接口提供更一致或更合适的接口。如之前的bind1st和bind2ed。
C++11提供了其他的包装器,包括模板bind、men_fn和reference_wrapper以及包装器function,模板bind可替代bind1st和bind2nd,但更灵活;模板men_fn将成员函数作为常规函数进行传递;模板reference_wrapper可以创建行为像引用但可被复制的对象;function能否以统一的方式处理多种类似于函数的形式。
18.5.1 包装器function及模板的低效性
对于answer = ef(q);
ef可能是函数名、函数指针、函数对象、或有名称的lambda表达式。可调用类型如此丰富,可能导致模板的效率极低。然后这些类型的参数特征标和返回类型是相同的,不使用包装器可能会为每一个类型创建一个
18.5.2 修复问题
包装器function,对于调用特征标相同的定义,调用特征标是有返回类型以及用括号括起并用逗号分隔的参数类型列表定义的。std::function<double(double)>;
18.5.3 其他方式
使用typedef给function来简化声明,然后使用该声明创建初始化对象,如:
typedef std::function<double(double)> fdd;
cout << use_f(fdd(dub)) << endl;
另一种方法是将模板的第二个参数声明为包装器对象,如下所示:
#include <functional>
template <typename T>
T use_f(T V, std::function<T(T)> f) // f call signature is T(T)
18.6 可变参数模板
可变参数模板(variadic template)可以创建这样的模板函数和模板类,接受可变数量的参数。
以show_list()为例,创建一个可变参数模板:
需要理解要点:
- 模板参数包(parameter pack);
- 函数参数包;
- 展开(unpack)参数包;
- 递归
18.6.1 模板和函数参数包
模板参数列表只含T,而函数参数列表只包含value。
C++11提供了一个用省略号表示的元运算符,让您能够声明表示模板参数包的标识符,模板参数包基本上是一个类型列表。同时也可以使用表示函数参数包的标识符,函数参数包本质上是一个值列表,语法如下:
template<typename... Args> // Args is a template parameter pack
void show_list1(Args... args) // args is a function parameter pack
{
...
}
18.6.2 展开参数包
将省略号...放在函数参数包名的右边,将函数参数包展开show_list1(arg...)
template<typename... Args> // Args is a template parameter pack
void show_list1(Args... args) // args is a function parameter pack
{
show_list1(args...);
}
上述代码会导致被函数使用相同的参数不断调用自己,导致无限递归。
18.6.3 在可变参数模板中使用递归
核心理念是,将函数包展开,对列表中的第一项进行处理,再将余下的内容传递给递归调用,一次类推,直到列表为空。
template<typename T, typename... Args>
void show_list3(T value, Args... args);
还应该增加args为空时,将不调用任何参数的show_list3(),导致处理结束。
改进:
增加一个处理第一项的模板,其行为与通用模板稍有不同。
18.7 C++11新增的其他功能
18.7.1 并行编程
多线程带来很多问题,C++定义了一个支持线程化执行的内存模型,添加了关键字thread_local,提供可相关的库支持。thread_local将变量声明为静态存储,其持续性与特定线程相关。
库支持有原子操作库和线程支持库组成。
18.7.2 新增的库
C++添加了多个专用库。
random支持可拓展随机数库;
chrono提供了处理时间间隔的途径;
tuple对象可以存储任意多个类型不同的值;
ratio支持编译阶段有理数算数库让您能准确地表示任何有理数;
regex库指定了一种模式,方括号表达式与方括号任意单个字符匹配。
18.7.3 低级编程
低级指的是抽象程度,而不是编程质量。
变化之一放松了POD(Plain Old Data)地要求。
允许共用体地成员有构造函数和析构函数,让共用体更灵活;但保留了其他限制,成员不能有虚函数。
C++11解决了内存对齐问题,要获悉有关类型或对象地对齐要求,可使用运算符alignof(),要控制对齐方式,可使用说明符alignas。
constexotr机制让编译器能够在编译阶段结果为常量的表达式。
18.7.4 杂项
C99引入依赖于实现的扩展类型,C++11继承了这种传统,在使用128位整数的系统中,头文件为cstdint。
C++11提供了一种创建用户自定义字面量的机制:字面量运算符。如1001001b,相应的字面量运算符将它转换为整数值。
C++提供了调试工具assert,这是一个宏,他在运行阶段对断言进行检查,如果为true,则显示一条信息,否则调用abort()。C++11新增了关键字static_assert,可用于在编译阶段对断言进行测试。
C++11加强了对元编程的支持,元编程指的是这样的程序,它创建或修改其他程序,甚至修改自身。
18.8 语言变化
库是在现有云烟功能的基础上创建的,不需要额外的编译器支持。如果库是通过模板实现的,则可以头文件的方式分发。
18.8.1 Boost项目
有一系列使用广泛的库。
18.8.2 TR1
TR1是一个库扩展选集,TR1
的带部分内容融入TR1。
18.8.3 使用Boost
18.9 OOP开发大型项目
技术有两种:
- 用例分析:找出元素、操作和之策
- CRC分析场景,为每个类创建索引卡片,卡片上列出了类名、类责任以及类的协作者。
18.10 复习题
1.使用用大括号括起的初始化列表语法重写下述代码。重写后的代码不应使用数组ar:
class Z200
{
private:
int j;
char ch;
doule z;
public:
Z200(int jv,char chv, zv) : j(jv), ch(chv), z(zv) {}
...
};
double x = 8.8;
std::string s = "What a bracing effect!";
int k(99);
Z200 zip(200,'Z',0.675);
std::vector<int> ai(5);
int ar[5] = {3, 9, 4, 7, 1};
for (auto pt = ai.begin(), int i; pt != ai.end(); ++pt, ++i)
*pt = ar[i];
使用初始化列表语法:
double x{8.8};
std::string s {"What a bracing effect!"};
int k{99};
Z200 zip{200,'Z',0.675};
std::vector<int> ai{3, 9, 4, 7, 1};
2.在下述简短的程序中,哪些函数调用不对?为什么?对于合法的函数调用,指出其引用参数指向的是什么。
#include <iostream>
using namespace std;
double up(double x) { return 2.0 * x; }
void r1(const double &rx) { cout << rx << endl; }
void r2(double &rx) { cout << rx << endl; }
void r3(double &&rx) { cout << rx << endl; }
int main()
{
double w = 10.0;
r1(w);
r1(w+1);
r1(up(w));
r2(w);
r2(w+1);
r2(up(w));
r3(w);
r3(w+1);
r3(up(w));
return 0;
}
r1(w)合法,常引用参数形参rx指向w;
r1(w+1)不合法,常引用应该指向左值,而w+1是右值表达式合法,形参rx指向一个临时变量,这个变量被初始化为w+1;
r1(up(w))不合法,常引用应该指向左值,up(w)是右值;
r2(w)合法,形参rx指向w;
r2(w+1)不合法,引用应该指向左值,w+1是右值表达式;w+1是一个右值
r2(up(w))不合法,引用应该指向左值,up(w)是右值;up(w)返回一个右值
r3(w)合法,引用参数指向值为10.0的临时变量右值引用不能指向左值;
r3(w+1)合法,引用参数指向值为11.0的rx指向表达式w+1的临时拷贝;
r3(up(w))合法,引用参数指向返回值20.0rx指向up(w)的临时返回值;
3.a. 下述简短的程序显示什么?为什么?
#include <iostream>
using namespace std;
double up(double x) { return 2.0 * x; }
void r1(const double &rx) { cout << "const double & rx\n"; }
void r1(double &rx) { cout << "double & rx\n"; }
int main()
{
r1(w);
r1(w+1);
r1(up(w));
return 0;
}
输出:
double & rx
const double & rx
const double & rx
b. 下述简短的程序显示什么?为什么?
#include <iostream>
using namespace std;
double up(double x) { return 2.0 * x; }
void r1(double &rx) { cout << "double & rx\n"; }
void r1(double && rx) { cout << "double && rx\n"; }
int main()
{
r1(w);
r1(w+1);
r1(up(w));
return 0;
}
输出:
double & rx
double && rx
double && rx
c. 下述简短的程序显示什么?为什么?
#include <iostream>
using namespace std;
double up(double x) { return 2.0 * x; }
void r1(const double &rx) { cout << "const double & rx\n"; }
void r1(double && rx) { cout << "double && rx\n"; }
int main()
{
r1(w);
r1(w+1);
r1(up(w));
return 0;
}
输出:
const double & rx
double && rx
double && rx
4.哪些成员函数是特殊的成员函数?它们的特殊的原因是什么?
默认构造函数、复制构造函数、复制赋值函数、移动构造函数、移动赋值函数、析构函数。
特殊的原因是如果没有定义,在使用到的时候,编译器会自动创建它们。因为编译器将根据情况自动提供它们的默认版本。
5.假设Fizzle类只有如下的数据成员:
class Fizzle
{
private:
double bubbles[4000];
...
};
为什么不适合给这个类定义移动构造函数?要让这个类适合定义移动构造函数,应如何存储4000个double值的方式?
因为这个类的存储方式不是动态存储,不可以转移控制权。要让这个类适合定义构造函数,应使用
double *bubbules = new double[4000];
**在转让数据所有权(而不是复制数据)可行时,可使用移动构造函数,但对于标准数组,没有装让所有权的机制。如果Fizzle使用指针和动态内存分配,则可以将数据的地址赋给新指针,以转让其所有权。
6.修改下述简短的程序,使其使用lambda表达式而不是f1()。请不要修改show2()。
#include <iostream>
template<typename T>
void show2(double x, T& fp) {std::cout << x << "->" << fp(x) << '\n';}
double f1(double x) { return 1.8 * x + 32;}
int main()
{
show(18.0, f1);
return 0;
}
修改后的程序如下:
#include <iostream>
template<typename T>
void show2(double x, T fp) {std::cout << x << "->" << fp(x) << '\n';}
int main()
{
show(18.0, [](double x){ return 1.8 * x + 32; });
return 0;
}
7.修改下述简短而丑陋的程序,使其使用lambda表达式而不是函数符Adder。请不要修改sum()。
// ex7.cpp -- use lambda
#include <iostream>
#include <array>
const int Size = 5;
template<typename T>
void sum(std::array<double,Size> a, T& fp);
class Adder
{
double tot;
public:
Adder(double q = 0) : tot(q) { }
void operator()(double w) { tot += w; }
double tot_v() const {return tot;}
};
int main()
{
double total = 0.0;
Adder ad(total);
std::array<double, Size> temp_c = {32.1, 34.3, 37.8, 35.2, 24.7};
sum(temp_c, ad);
total = ad.tot_v();
std::cout << "total: " << ad.tot_v() << '\n';
return 0;
}
template<typename T>
void sum(std::array<double,Size> a, T& fp)
{
for (auto pt = a.begin(); pt != a.end(); ++pt)
{
fp(*pt);
}
}
修改后的程序如下:
// ex7.cpp -- use lambda
#include <iostream>
#include <array>
const int Size = 5;
template<typename T>
void sum(std::array<double,Size> a, T& fp);
int main()
{
double total = 0.0;
std::array<double, Size> temp_c = {32.1, 34.3, 37.8, 35.2, 24.7};
sum(temp_c, [&total](double x){ total += x; });
std::cout << "total: " << total << '\n';
return 0;
}
template<typename T>
void sum(std::array<double,Size> a, T& fp)
{
for (auto pt = a.begin(); pt != a.end(); ++pt)
{
fp(*pt);
}
}
注意,对于模板要想使用lambda函数,对于函数不能使用引用。