diff --git a/book/2-usability.md b/book/2-usability.md index bc1efd3..ef10369 100644 --- a/book/2-usability.md +++ b/book/2-usability.md @@ -1,123 +1,117 @@ # 第二章 语言可用性的强化 -> 内容修订中 +[TOC] -## 一、本节内容 +当我们声明、定义一个变量或者常量,对代码进行流程控制、面向对象的功能、模板编程等这些都是运行时之前,可能发生在编写代码或编译器编译代码时的行为。为此,我们通常谈及**语言可用性**,是指那些发生在运行时之前的语言行为。 -本节内容包括: +## 2.1 常量 -- 语言可用性的强化 -+ `nullptr` 与 `constexpr` -+ 类型推导 -+ `auto` -+ `decltype` -+ 尾返回类型、`auto` 与 `decltype` 配合 - - - -+ 区间迭代 -+ 基于范围的 for 循环 -+ 初始化列表 -+ `std::initializer_list` -+ 统一初始化语法 -+ 模板增强 -+ 外部模板 -+ 尖括号 `>` -+ 类型别名模板 -+ 变长参数模板 -+ 面向对象增强 -+ 委托构造 -+ 继承构造 -+ 显式虚函数重载 -+ `override` -+ `final` -+ 显式禁用默认函数 -+ 强类型枚举 -+ 总结 - -## 二、nullptr 与 constexpr - -### nullptr +### nullptr `nullptr` 出现的目的是为了替代 `NULL`。在某种意义上来说,传统 C++ 会把 `NULL`、`0` 视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 `((void*)0)`,有些则会直接将其定义为 `0`。 -C++ 不允许直接将 `void *` 隐式转换到其他类型,但如果 `NULL` 被定义为 `((void*)0)`,那么当编译 +C++ **不允许**直接将 `void *` 隐式转换到其他类型(换句话说,`void *` 并不属于 C++ 语言的一部分)。但如果编译器尝试把 `NULL` 定义为 `((void*)0)`,那么在下面这句代码中: ```cpp char *ch = NULL; ``` -时,`NULL` 只好被定义为 `0`。而这依然会产生问题,将导致了 `C++` 中重载特性会发生混乱,考虑: +没有了 `void *` 隐式转换的 C++ 只好将`NULL` 定义为 `0`。而这依然会产生新的问题,将 `NULL` 定义成 0 将导致 `C++` 中重载特性发生混乱。考虑下面这两个 `foo` 函数: ```cpp -void foo(char *); +void foo(char*); void foo(int); ``` -对于这两个函数来说,如果 `NULL` 又被定义为了 `0` 那么 `foo(NULL);` 这个语句将会去调用 `foo(int)`,从而导致代码违反直观。 +那么 `foo(NULL);` 这个语句将会去调用 `foo(int)`,从而导致代码违反直观。 -为了解决这个问题,C++11 引入了 `nullptr` 关键字,专门用来区分空指针、0。`nullptr` 的类型为 `nullptr_t`,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。 +为了解决这个问题,C++11 引入了 `nullptr` 关键字,专门用来区分空指针、0。而 `nullptr` 的类型为 `nullptr_t`,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。 -你可以尝试使用 gcc 和 g++ 两个编译器同时编译下面的代码: +你可以尝试使用 clang++ 编译下面的代码: ```cpp #include +#include + void foo(char *); void foo(int); + int main() { + if (std::is_same::value) + std::cout << "NULL == 0" << std::endl; + if (std::is_same::value) + std::cout << "NULL == (void *)0" << std::endl; + if (std::is_same::value) + std::cout << "NULL == nullptr" << std::endl; -if(NULL == (void *)0) std::cout << "NULL == 0" << std::endl; -else std::cout << "NULL != 0" << std::endl; -foo(0); -// foo(NULL); // 编译无法通过 -foo(nullptr); - -return 0; + foo(0); // 调用 foo(int) + // foo(NULL); // 该行不能通过编译 + foo(nullptr); // 调用 foo(char*) + return 0; } -void foo(char *ch) { -std::cout << "call foo(char*)" << std::endl; + +void foo(char *) { + std::cout << "foo(char*) is called" << std::endl; } void foo(int i) { -std::cout << "call foo(int)" << std::endl; + std::cout << "foo(int) is called" << std::endl; } ``` 将输出: ```bash -NULL == 0 -call foo(int) -call foo(char*) +foo(int) is called +foo(char*) is called ``` -所以,当需要使用 `NULL` 时候,请养成直接使用 `nullptr`的习惯。 +从输出中我们可以看出,`nullptr` 与 `NULL` 与 `0` 均不相同。所以,请养成直接使用 `nullptr`的习惯。 + +此外,在上面的代码中,我们使用了 `decltype` 和 `std::is_same` 这两个属于现代 C++ 的语法,简单来说,`decltype` 用于类型推导,而 `std::is_same` 用于比较两个类型是否相等,我们会在后面 [decltype](#decltype) 一节中详细讨论。 ### constexpr -C++ 本身已经具备了常数表达式的概念,比如 1+2, 3*4 这种表达式总是会产生相同的结果并且没有任何副作用。如果编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。一个非常显著的例子就是在数组的定义阶段: +C++ 本身已经具备了常数表达式的概念,比如 1+2, 3*4 这种表达式总是会产生相同的结果并且没有任何副作用。如果编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。一个非常明显的例子就是在数组的定义阶段: ```cpp +#include #define LEN 10 int len_foo() { -return 5; + int i = 2; + return i; +} +constexpr int len_foo_constexpr() { + return 5; +} + +constexpr int fibonacci(const int n) { + return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2); } int main() { -char arr_1[10]; -char arr_2[LEN]; -int len = 5; -char arr_3[len]; // 非法 -const int len_2 = 10; -char arr_4[len_2]; // 合法 -char arr_5[len_foo()+5]; // 非法 -return 0; + char arr_1[10]; // 合法 + char arr_2[LEN]; // 合法 + + int len = 10; + // char arr_3[len]; // 非法 + + const int len_2 = len + 1; + char arr_4[len_2]; // 合法 + + // char arr_5[len_foo()+5]; // 非法 + char arr_6[len_foo_constexpr() + 1]; // 合法 + + std::cout << fibonacci(10) << std::endl; + // 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 + + return 0; } ``` -在 C++11 之前,可以在常量表达式中使用的变量必须被声明为 `const`,在上面代码中,`len_2` 被定义成了常量,因此 `len_2` 是一个常量表达式,所以能够合法的分配一个数组; +在 C++11 之前,可以在常量表达式中使用的变量必须被声明为 `const`,在上面代码中,`len_2` 被定义成了常量,因此 `len_2` 是一个常量表达式,所以能够合法的分配一个数组;而对于 `arr_5` 来说,C++98 之前的编译器无法得知 `len_foo()` 在运行期实际上是返回一个常数,这也就导致了非法的产生。 -而对于 `arr_5` 来说,C++98 之前的编译器无法得知 `len_foo()` 在运行期实际上是返回一个常数,这也就导致了非法的产生。 +> 注意,现在大部分编译器其实都带有自身编译优化,很多非法行为在编译器优化的加持下回变得合法,若需重现编译报错的现象需要使用老版本的编译器。 C++11 提供了 `constexpr` 让用户显式的声明函数或对象构造函数在编译器会成为常数,这个关键字明确的告诉编译器应该去验证 `len_foo` 在编译器就应该是一个常数。 @@ -126,7 +120,7 @@ C++11 提供了 `constexpr` 让用户显式的声明函数或对象构造函数 ```cpp constexpr int fibonacci(const int n) { -return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2); + return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2); } ``` @@ -134,13 +128,158 @@ return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2); ```cpp constexpr int fibonacci(const int n) { -if(n == 1) return 1; -if(n == 2) return 1; -return fibonacci(n-1)+fibonacci(n-2); + if(n == 1) return 1; + if(n == 2) return 1; + return fibonacci(n-1) + fibonacci(n-2); } ``` -## 三、类型推导 +为此,我们可以写出下面这类简化的版本来使得函数从 C++11 开始即可用: + +```cpp +constexpr int fibonacci(const int n) { + return n == 1 || n == 2 ? 1 : fibonacci(n-1) + fibonacci(n-2); +} +``` + +## 2.2 变量及其初始化 + +### if/switch 变量声明强化 + +在传统 C++ 中,变量的声明在虽然能够位于任何位置,甚至于 `for` 语句内能够声明一个临时变量 `int`,但始终没有办法在 `if` 和 `switch` 语句中声明一个临时的变量。例如: + +```cpp +#include +#include +#include + +int main() { + std::vector vec = {1, 2, 3, 4}; + + // 在 c++17 之前 + const std::vector::iterator itr = std::find(vec.begin(), vec.end(), 2); + if (itr != vec.end()) { + *itr = 3; + } + + // 需要重新定义一个新的变量 + const std::vector::iterator itr2 = std::find(vec.begin(), vec.end(), 3); + if (itr != vec.end()) { + *itr2 = 4; + } + + // 将输出 1, 4, 3, 4 + for (std::vector::iterator element = vec.begin(); element != vec.end(); ++element) + std::cout << *element << std::endl; +} +``` + +在上面的代码中,我们可以看到 `itr` 这一变量是定义在整个 `main()` 的作用域内的,这导致当我们需要再次遍历整个 `std::vectors` 时,需要重新命名另一个变量。C++17 消除了这一限制,使得我们可以在 if(或 switch)中完成这一操作: + +```cpp +// 将临时变量放到 if 语句内 +if (const std::vector::iterator itr = std::find(vec.begin(), vec.end(), 3); + itr != vec.end()) { + *itr = 4; +} +``` + +怎么样,是不是和 Go 语言很像? + +### 初始化列表 + +初始化是一个非常重要的语言特性,最常见的就是在对象进行初始化时进行使用。在传统 C++ 中,不同的对象有着不同的初始化方法,例如普通数组、POD (**P**lain **O**ld **D**ata,即没有构造、析构和虚函数的类或结构体)类型都可以使用 `{}` 进行初始化,也就是我们所说的初始化列表。而对于类对象的初始化,要么需要通过拷贝构造、要么就需要使用 `()` 进行。这些不同方法都针对各自对象,不能通用。例如: + +```cpp +#include +#include + +class Foo { +public: + int value_a; + int value_b; + Foo(int a, int b) : value_a(a), value_b(b) {} +}; + +int main() { + // before C++11 + int arr[3] = {1, 2, 3}; + Foo foo(1, 2); + std::vector vec = {1, 2, 3, 4, 5}; + + std::cout << "arr[0]: " << arr[0] << std::endl; + std::cout << "foo:" << foo.value_a << ", " << foo.value_b << std::endl; + for (std::vector::iterator it = vec.begin(); it != vec.end(); ++it) { + std::cout << *it << std::endl; + } + return 0; +} +``` + +为了解决这个问题,C++11 首先把初始化列表的概念绑定到了类型上,并将其称之为 `std::initializer_list`,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁,例如: + +```cpp +#include +class MagicFoo { +public: + std::vector vec; + MagicFoo(std::initializer_list list) { + for (std::initializer_list::iterator it = list.begin(); + it != list.end(); ++it) + vec.push_back(*it); + } +}; +int main() { + // after C++11 + MagicFoo magicFoo = {1, 2, 3, 4, 5}; + + std::cout << "magicFoo: "; + for (std::vector::iterator it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) std::cout << *it << std::endl; +} +``` + +这种构造函数被叫做初始化列表构造函数,具有这种构造函数的类型将在初始化时被特殊关照。 + +初始化列表除了用在对象构造上,还能将其作为普通函数的形参,例如: + +```Cpp +public: + void foo(std::initializer_list list) { + for (std::initializer_list::iterator it = list.begin(); it != list.end(); ++it) vec.push_back(*it); + } + +magicFoo.foo({6,7,8,9}); +``` + + +其次,C++11 还提供了统一的语法来初始化任意的对象,例如: +```cpp +Foo foo2 {3, 4}; +``` + +### 结构化绑定 + +结构化绑定提供了类似其他语言中提供的多返回值的功能。在[标准库扩充:容器]()一章中,我们会学到 C++11 新增了 `std::tuple` 容器用于构造一个元组,进而囊括多个返回值。但缺陷是,C++11/14 并没有提供一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 `std::tie` 对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型,非常麻烦。 + +C++17 完善了这一设定,给出的结构化绑定可以让我们写出这样的代码: + +```cpp +#include + +std::tuple f() { + return std::make_tuple(1, 2.3, "456"); +} + +int main() { + auto [x, y, z] = f(); + std::cout << x << ", " << y << ", " << z << std::endl; + return 0; +} +``` + +关于 `auto` 类型推导会在 [auto 类型推导](#auto)一节中进行介绍。 + +## 2.3 类型推导 在传统 C 和 C++中,参数的类型都必须明确定义,这其实对我们快速进行编码没有任何帮助,尤其是当我们面对一大堆复杂的模板类型时,必须明确的指出变量的类型才能进行后续的编码,这不仅拖慢我们的开发效率,也让代码变得又臭又长。 @@ -148,47 +287,71 @@ C++11 引入了 `auto` 和 `decltype` 这两个关键字实现了类型推导, ### auto -`auto` 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 `register` 并存。在传统 C++ 中,如果一个变量没有声明为 `register` 变量,将自动被视为一个 `auto` 变量。而随着 `register` 被弃用,对 `auto` 的语义变更也就非常自然了。 +`auto` 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 `register` 并存。在传统 C++ 中,如果一个变量没有声明为 `register` 变量,将自动被视为一个 `auto` 变量。而随着 `register` 被弃用(在 C++17 中作为保留关键字,以后使用,目前不具备实际意义),对 `auto` 的语义变更也就非常自然了。 -使用 `auto` 进行类型推导的一个最为常见而且显著的例子就是迭代器。在以前我们需要这样来书写一个迭代器: +使用 `auto` 进行类型推导的一个最为常见而且显著的例子就是迭代器。你应该在前面的小节里看到了传统 C++ 中冗长的迭代写法: ```cpp -for(vector::const_iterator itr = vec.cbegin(); itr != vec.cend(); ++itr) +// 在 C++11 之前 +// 由于 cbegin() 将返回 vector::const_iterator +// 所以 itr 也应该是 vector::const_iterator 类型 +for(vector::const_iterator it = vec.cbegin(); itr != vec.cend(); ++it) ``` 而有了 `auto` 之后可以: ```cpp -// 由于 cbegin() 将返回 vector::const_iterator -// 所以 itr 也应该是 vector::const_iterator 类型 -for(auto itr = vec.cbegin(); itr != vec.cend(); ++itr); + +#include +#include +#include + +class MagicFoo { +public: + std::vector vec; + MagicFoo(std::initializer_list list) { + // 从 C++11 起, 使用 auto 关键字进行类型推导 + for (auto it = list.begin(); it != list.end(); ++it) { + vec.push_back(*it); + } + } +}; +int main() { + MagicFoo magicFoo = {1, 2, 3, 4, 5}; + std::cout << "magicFoo: "; + for (auto it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) { + std::cout << *it << ", "; + } + std::cout << std::endl; + return 0; +} ``` 一些其他的常见用法: ```cpp -auto i = 5; // i 被推导为 int +auto i = 5; // i 被推导为 int auto arr = new auto(10) // arr 被推导为 int * ``` > **注意**:`auto` 不能用于函数传参,因此下面的做法是无法通过编译的(考虑重载的问题,我们应该使用模板): - -```cpp -int add(auto x, auto y); -``` - +> +> ```cpp +> int add(auto x, auto y); +> +> 2.6.auto.cpp:16:9: error: 'auto' not allowed in function prototype +> int add(auto x, auto y) { +> ^~~~ +> ``` +> > 此外,`auto` 还不能用于推导数组类型: - -```cpp -#include -int main() { -auto i = 5; -int arr[10] = {0}; -auto auto_arr = arr; -auto auto_arr2[10] = arr; -return 0; -} -``` +> +> ```cpp +> auto auto_arr2[10] = arr; // 错误, 无法推导数组元素类型 +> +> 2.6.auto.cpp:30:19: error: 'auto_arr2' declared as array of 'auto' +> auto auto_arr2[10] = arr; +> ``` ### decltype @@ -206,9 +369,27 @@ auto y = 2; decltype(x+y) z; ``` -### 尾返回类型、auto 与 decltype 配合 +你已经在前面的例子中看到 `decltype` 用于推断类型的用法,下面这个例子就是判断上面的变量 `x, y, z` 是否是同一类型: -你可能会思考,`auto` 能不能用于推导函数的返回类型。考虑这样一个例子加法函数的例子,在传统 C++ 中我们必须这么写: +```cpp +if (std::is_same::value) + std::cout << "type x == int" << std::endl; +if (std::is_same::value) + std::cout << "type z == float" << std::endl; +if (std::is_same::value) + std::cout << "type z == type x" << std::endl; +``` + +其中,`std::is_same` 用于判断 `T` 和 `U` 这两个类型是否相等。输出结果为: + +``` +type x == int +type z == type x +``` + +### 尾返回类型推导 + +你可能会思考,在介绍 `auto`时,我们已经提过类型推导不能用于函数形参,那么`auto` 能不能用于推导函数的返回类型呢?还是考虑一个加法函数的例子,在传统 C++ 中我们必须这么写: ```cpp template @@ -216,7 +397,7 @@ R add(T x, U y) { return x+y } ``` -> typename 和 class 在模板中没有区别,在 typename 这个关键字出现之前,都是使用 class 来定义模板参数的 +> 注意:typename 和 class 在模板中没有区别,在 typename 这个关键字出现之前,都是使用 class 来定义模板参数的 这样的代码其实变得很丑陋,因为程序员在使用这个模板函数的时候,必须明确指出返回类型。但事实上我们并不知道 `add()` 这个函数会做什么样的操作,获得一个什么样的返回类型。 @@ -230,8 +411,8 @@ decltype(x+y) add(T x, U y) ```cpp template -auto add(T x, U y) -> decltype(x+y) { -return x+y; +auto add2(T x, U y) -> decltype(x+y){ + return x + y; } ``` @@ -239,127 +420,128 @@ return x+y; ```cpp template -auto add(T x, U y) { -return x+y +auto add3(T x, U y){ + return x + y; } ``` - +> 直至往后的内容正在更新,尽请期待 - -## 四、区间迭代 - -### 基于范围的 for 循环 - -终于,C++11 引入了基于范围的迭代写法,我们拥有了能够写出像 Python 一样简洁的循环语句: - -```cpp -int array[] = {1,2,3,4,5}; -for(auto &x : array) { -std::cout << x << std::endl; -} -``` - -最常用的 `std::vector` 遍历将从原来的样子: - -```cpp -std::vector arr(5, 100); -for(std::vector::iterator i = arr.begin(); i != arr.end(); ++i) { -std::cout << *i << std::endl; -} -``` - -变得非常的简单: - -```cpp -// & 启用了引用, 如果没有则对 arr 中的元素只能读取不能修改 -for(auto &i : arr) { -std::cout << i << std::endl; -} -``` - -## 五、初始化列表 - -初始化是一个非常重要的语言特性,最常见的就是对对象进行初始化。在传统 C++ 中,不同的对象有着不同的初始化方法,例如普通数组、POD (plain old data,没有构造、析构和虚函数的类或结构体)类型都可以使用 `{}` 进行初始化,也就是我们所说的初始化列表。而对于类对象的初始化,要么需要通过拷贝构造、要么就需要使用 `()` 进行。这些不同方法都针对各自对象,不能通用。 - -```cpp -int arr[3] = {1,2,3}; // 列表初始化 - -class Foo { -private: -int value; -public: -Foo(int) {} -}; - -Foo foo(1); // 普通构造初始化 -``` - -为了解决这个问题,C++11 首先把初始化列表的概念绑定到了类型上,并将其称之为 `std::initializer_list`,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁,例如: - -```cpp -#include - -class Magic { -public: -Magic(std::initializer_list list); -}; - -Magic magic = {1,2,3,4,5}; - -std::vector v = {1, 2, 3, 4}; -``` - -这种构造函数被叫做初始化列表构造函数,具有这种构造函数的类型将在初始化时被特殊关照。 - -初始化列表除了用在对象构造上,还能将其作为普通函数的形参,例如: - -``` -void foo(std::initializer_list list); - -foo({1,2,3}); -``` - - -其次,C++11 提供了统一的语法来初始化任意的对象,例如: -```cpp -struct A { -int a; -float b; -}; -struct B { - -B(int _a, float _b): a(_a), b(_b) {} -private: -int a; -float b; -}; - -A a {1, 1.1}; // 统一的初始化语法 -B b {2, 2.2}; -``` - -## 六、模板增强 +## 2.5 模板 ### 外部模板 @@ -434,7 +616,7 @@ return x+y ### 变长参数模板 -模板一直是 C++ 所独有的黑魔法(一起念:**Dark Magic**)之一。在 C++11 之前,无论是类模板还是函数模板,都只能按其指定的样子,接受一组固定数量的模板参数;而 C++11 加入了新的表示方法,允许任意个数、任意类别的模板参数,同时也不需要再定义时将参数的个数固定。 +模板一直是 C++ 所独有的**黑魔法**(一起念:**Dark Magic**)之一。在 C++11 之前,无论是类模板还是函数模板,都只能按其指定的样子,接受一组固定数量的模板参数;而 C++11 加入了新的表示方法,允许任意个数、任意类别的模板参数,同时也不需要再定义时将参数的个数固定。 ```cpp template class Magic; @@ -489,14 +671,14 @@ magic(1, ""); // 输出2 ```cpp #include -template -void printf(T value) { -std::cout << value << std::endl; +template +void printf(T0 value) { + std::cout << value << std::endl; } template void printf(T value, Args... args) { -std::cout << value << std::endl; -printf(args...); + std::cout << value << std::endl; + printf(args...); } int main() { printf(1, 2, "123", 1.1); @@ -504,6 +686,18 @@ return 0; } ``` +你应该感受到了这很繁琐,在 C++17 中增加了变参模板展开的支持,于是你可以在一个函数中完成 `printf` 的编写: + +```cpp +template +void printf(T0 t0, T... t) { + std::cout << t0 << std::endl; + if constexpr (sizeof...(t) > 0) printf(t...); +} +``` + + + **2. 初始化列表展开** > 这个方法需要之后介绍的知识,读者可以简单阅读以下,将这个代码段保存,在后面的内容了解过了之后再回过头来阅读此处方法会大有收获。 @@ -533,10 +727,24 @@ return 0; > 事实上,有时候我们虽然使用了变参模板,却不一定需要对参数做逐个遍历,我们可以利用 `std::bind` 及完美转发等特性实现对函数和参数的绑定,从而达到成功调用的目的。 -> 关于这方面的使用技巧,可以通过项目课:[100 行 C++ 代码实现线程池](https://www.shiyanlou.com/teacher/courses/565) 进行进一步巩固学习。 +> 关于这方面的使用技巧,请参考习题。~~可以通过项目课:[100 行 C++ 代码实现线程池](https://www.shiyanlou.com/teacher/courses/565) 进行进一步巩固学习。~~ +### 折叠表达式 -## 七、面向对象增强 +C++ 17 中将变长参数这种特性进一步带给了表达式,考虑下面这个例子: + +```cpp +#include +template +auto sum(T ... t) { + return (t + ...) +} +int main() { + std::cout << sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) << std::endl; +} +``` + +## 2.6 面向对象 ### 委托构造 @@ -658,7 +866,7 @@ Magic(int magic_number); } ``` -## 八、强类型枚举 +### 强类型枚举 在传统 C++中,枚举类型并非类型安全,枚举类型会被视作整数,则会让两种完全不同的枚举类型可以进行直接的比较(虽然编译器给出了检查,但并非所有),**甚至枚举类型的枚举值名字不能相同**,这不是我们希望看到的结果。 @@ -703,20 +911,51 @@ std::cout << new_enum::value3 << std::endl ## 总结 -本节介绍了 C++11/14 中对语言可用性的增强,其中笔者认为最为重要的几个特性是几乎所有人都需要了解并熟练使用的: +本节介绍了 C++11/14/17 中对语言可用性的增强,其中笔者认为最为重要的几个特性是几乎所有人都需要了解并熟练使用的: 1. auto 类型推导 2. 范围 for 迭代 3. 初始化列表 4. 变参模板 -## 进一步阅读的参考资料 +## 习题 -1. 深入理解 C++11: C++11 新特性解析与应用. Michael Wong, IBM XL 编译器中国开发团队著 -2. 深入应用 C++11: 代码优化与工程级应用. 祁宇著 +1. 使用结构化绑定,仅用一行函数内代码实现如下函数: + + ```cpp + template + void update(std::map& m, F foo) { + // TODO: + } + int main() { + std::map m { + {"a", 1}, + {"b", 2}, + {"c", 3} + }; + update(m, [](std::string key) { + return std::hash{}(key); + }); + for (auto&& [key, value] : m) + std::cout << key << ":" << value << std::endl; + } + ``` + +2. 尝试用[折叠表达式](#折叠表达式)实现用于计算均值的函数,传入允许任意参数。 + +3. ​ + +> 参考答案[见此](../code/2/solutions)。 + +[返回目录](./toc) | [上一章](./1-intro) | [下一章:运行时强化](./3-runtime) + +## 进一步阅读的参考文献 + +1. [深入理解 C++11: C++11 新特性解析与应用. Michael Wong, IBM XL 编译器中国开发团队著](https://www.amazon.cn/dp/B00ETOV2OQ/ref=sr_1_1?ie=UTF8&qid=1522429506&sr=8-1&keywords=%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3+C%2B%2B11%3A+C%2B%2B11+%E6%96%B0%E7%89%B9%E6%80%A7%E8%A7%A3%E6%9E%90%E4%B8%8E%E5%BA%94%E7%94%A8) +2. [深入应用 C++11: 代码优化与工程级应用. 祁宇著](https://www.amazon.cn/dp/B00YGVLVA2/ref=sr_1_1?ie=UTF8&qid=1522429525&sr=8-1&keywords=%E6%B7%B1%E5%85%A5%E5%BA%94%E7%94%A8+C%2B%2B11%3A+%E4%BB%A3%E7%A0%81%E4%BC%98%E5%8C%96%E4%B8%8E%E5%B7%A5%E7%A8%8B%E7%BA%A7%E5%BA%94%E7%94%A8) ## 许可 知识共享许可协议 -本教程由[欧长坤](https://github.com/changkun)撰写,采用[知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议](http://creativecommons.org/licenses/by-nc-nd/4.0/)许可。项目中代码使用 MIT 协议开源,参见[许可](../LICENSE)。 \ No newline at end of file +本书系[欧长坤](https://github.com/changkun)著,采用[知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议](http://creativecommons.org/licenses/by-nc-nd/4.0/)许可。项目中代码使用 MIT 协议开源,参见[许可](../LICENSE)。 \ No newline at end of file diff --git a/book/9-cpp17.md b/book/9-cpp17.md index dac1ad3..6a9405f 100644 --- a/book/9-cpp17.md +++ b/book/9-cpp17.md @@ -78,41 +78,7 @@ return _tuple_index<0>(i, tpl); } ``` -### 结构化绑定(Structured bindings) -结构化绑定提供了类似其他语言中提供的多返回值的功能。到目前为止,我们可以通过 `std::tuple` 来构造一个元组,囊括多个返回值。但缺陷是显而易见的,我们没有一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 `std::tie` 对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型。 - -C++17 给出的结构化绑定可以让我们写出这样的代码: - -```cpp -std::tuple f() { -return std::make_tuple(1,2.3,"456"); -} -auto [x,y,z] = f(); // x,y,z 分别被推导为int,double,std::string -``` - -### 变量声明的强化 - -变量的声明在虽然能够位于任何位置,甚至于 `for` 语句内能够声明一个临时变量 `int`,但始终没有办法在 `if` 和 `switch` 语句中声明一个临时的变量。例如: - -```cpp -auto p = map_container.try_emplace(key, value); -if(!p.second) { -//... -} else { -//... -} -``` - -C++17 消除了这一限制,使得我们可以: - -```cpp -if (auto p = m.try_emplace(key, value); !p.second) { -//... -} else { -//... -} -``` ## 三、未入选特性