深入理解 C++11 读书笔记

声明: 所有图片均为我在腾讯博客的原创, 但是从我的腾讯微博转过来就被流氓的打了标签. 因此不涉及版权问题, 可以随意使用.


 

C++11 中弃用了 `throw(type1, type2...)` 这种异常声明方式. 但是库中大量使用了 noexcept 代替原有异常机制, 因此提高了库的效率.

默认析构函数是 noexcept(true) 的, 但是如果基类析构函数是 noexcept(false), 那么自雷析构函数默认也是 noexcept(false) 的.

 

类中成员变量的初始化顺序: 1, 就地初始化. 2, 初始化列表. 3, 构造函数体. (目前, 最新的 vs2013 preview 还不持支此特性)

注: 此版本 vs 尚不支持就地初始化, 所以此代码仅供参考.

委托构造, 即一个构造函数依赖于另一个构造函数.

 

 

曾经, C++ 是无法直接获取类的成员变量的大小的, 通常我们会 `sizeof(reinterpret_cast<X *>(0)->m_foo)`. 而 11 终结了这个蹩脚的语法, 对于普通对象成员使用 `sizeof(X::m_foo)` 即可.

 

类成员函数后面的 override 和 final 真是一对好朋友. 在编写多层次继承时可以极大避免程序员犯错误. 我们需要这样的编译器! 当然, 如果你非写成这样: `virtual foo() final = 0;` 的成员函数, 也是可以的.

关于 `virtual void foo() final = 0;`, 如果面试的时候考官问你: 怎么能让一个类不能实例化? 除了回答 private dctor 以外, 还可以使用这条技巧.

 

return rvalue 在表达式结束后, 其生命周期也就结束了, 而通过右值引用的匿名变量, 该右值生命周期将会与右值引用类型的变量的生命周期一样. 只要这个引用变量还 "活着", 该右值临时量将会一直 "存活" 下去.

在 98 中, const & 类型也可以将临时变量的声明期延长至引用变量周期. 原来如此...

 

'std::move 基本等同于一个类型转换: static_cast<T &&>(lvalue);' 这句话验证了我一年前刚学 move semantic 的疑惑. 我当时就认为, 既然 lvalue 可以通过变为匿名的手段成为 rvalue, 那么 static_cast 就可以胜任. 结果今天终于找到证实了.

std::move 和 std::forward 在实际实现的差别并不大. 不过标准库这么设计, 也是为了让每个名字对应不同的用户, 以应对未来可能的扩展.

 

标准库中, std::move_if_noexcept 如果参数有 noexcept 移动构造函数, 则返回右值引用; 否则返回左值引用. 这使得 `T tNew = std::move_if_noexcept(tOld);` 非常安全.

 

11 将 explicit 关键字扩展到类的类型转换上, 对程序的健壮性起了积极作用. 类似的改动还有类成员函数的 override 和 final.

wiki: "在 C++11 中,关键字 explicit 修饰符也能套用到类型转换子上。如同建构式一样,它能避免类型转换子被隐式转换调用。但 C++11 特别针对布林值转换提出规范,在 if 条件式,循环,逻辑运算等需要布林值的地方,编译器能为符合规范的表示式调用用户自定的布林类型转换子。" (感谢 rui)

 

匿名联合体的成员在 class 中能够直接被引用. 这是 98 中无法做到的, 通常, 98 会给 union 起一个名字, 然后再通过这个名字引用其中的成员, 着实不直观而且还麻烦. 起名字是程序员最大的痛苦.

经过测试, 在 vs2013 preview 中, 匿名 struct 和 class 也是被支持的.

 

用户自定义字面量为程序员提供了非常直观 & 方便的语法糖. 我又想起了那句话: 好的编程语言是让程序员用更少的代码完成更多的事情. 推荐阅读: http://akrzemi1.wordpress.com/2013/01/04/preconditions-part-i/ 以及 http://www.cnblogs.com/walfud/articles/3280629.html

字面值为 char: 字面量操作符参数只能为 char.

字面量为 const char *: 操作符参数只接受 (const char *, size_t_).

字面值为浮点型: 字面量操作符只接受 long double 或者 const char * 作参数. 同理, 如果 long double 容纳不下, 调用 const char * 版本处理函数.

字面量为整数: 操作符函数只能接受 unsigned long long 或者 const char * 作为参数. 如果 unsigned long long 容纳不下, 则自动调用 const char * 操作符处理函数.

 11 中, 自定义字面量操作符需要注意: 1, `operator "" _XXX` 中, 双引号和自定义后缀之间必须有一个空格. 2, 建议使用下划线开始的自定义后缀, 避免和 C/C++ 关键字冲突.

 

inline namespace 我看不出你有什么好. 命名就是破坏了 namespace 的隔离性. 所以 Do NOT use it unless you know what you are doing.

 

使用 using 定义别名的好处是可以支持 template. 比如 `template <typename T> using MapString = std::map<T, std::string>`. 这是 typedef 无法做到的.

 

auto 对于 cv-qualifier 的支持: auto 并不从初始化表达式中带走 cv-qualifier. 但是对于指针和引用则会保持原有的 cv 类型. 见图片.

在 11 中, 有几种情况是 auto所不能的:

  1: 不能作为函数参数类型, 更不能在函数参数中自动推导.

  2: 不能作为结构体非静态成员, 同样也不能推导.

  3: 不能声明 auto arr[n] 这样的数组.

  4: 不能作为模板的参数.

 

decltype 与 auto 不同, decltype 能够带走 "cv" 限定符, 不过, decltype 不会继承对象的 cv 属性. 如图:

decltype(v) 也有一些诡异的规则, 且看:

1, 如果 v 是一个不带括号的标记符表达式(id-expr) 或类成员表达式, 则 decltype(v) 代表 v 的类型. 此外, 如果 v 是一个被重载的函数, 则导致编译错误.

2, 否则, 假设 v 类型是 T, 如果 v 是一个将亡值(xvalue), 那么 decltype(v) 为 T &&.

3, 否则, 假设 v 类型 T, 如果 v 是一个左值, 则 decltype(v) 为 T &.

4, 否则, 假设 v 类型 T, 则 decltype(v) 为 T.

其中最蹩脚的就是规则 3. 看几个例子: int i = 0; int *pI = &i; decltype(i) => int, decltype(*pI) => int &, decltype(i++) => int &, decltype(++i) => int. <深入理解 C++11> 这书讲的不错, 详细内容请看 4.3.3

此外, decltype 能够允许冗余的 cv 限定符. 比如 const int i = 0; const decltype(i) ci = 0; 也是允许的, 虽然 decltype(i) 已经是 const int 类型.

  

有人认为宏和常量都要全大写, 这是对全大写作用的误读. 在传统的 C 语言中, 全大写是因为宏处理发生在预编译阶段, 任何包含该该字母串的内容都会被替换, 因此会扰乱正常代码. 而开发者为了避免这种情况, 才将宏全大写. 枚举虽然也是常量, 但是会扰乱正常代码么? 显然不会, 因此就无需全大写.

 

11 终于把 enum 的超级作用域做了完善. 在 98 中, enum 作用域是会被自动导出到父作用域的, 导致不同的枚举中也不能存在相同的枚举值, 这是何等违背作用域的理念. 在 11 中, enum class 将枚举限制在了枚举类中, 增强了作用域的概念, 因此不同枚举类中完全可以存在同名同值的枚举值, 这让代码更加一致.

 

11 中加入了很多对并行编程的支持. 比如 atomic 模板, thread 等, 极大方便了并行程序设计. 让程序员将精力集中在要解决的问题上, 而不是互斥访问变量和如何创建线程这些问题上.

在 11 的并行支持中, 有个重要的概念叫做 "内存模型". 目前 11 支持 6 种内存模型. 这些模型分门别类的作为 atomic 对象的 load 和 store 第二参数使用. 其作用就是约束机器指令的执行顺序以及添加内存栏栅.

memory_order_relaxed: 不对执行顺序作任何保证.

memory_order_acquire: 本线程中, 所有后续的读操作必须在本原子操作完成后执行.

memory_order_release: 本线程中, 所有之前的写操作完成之后才能执行本原子操作.

memory_order_acq_rel: 同时包含 memory_order_acquire 和 memory_order_release 标记.

memroy_order_consume: 本线程中, 所有后续的有关本原子类型的操作, 必须在本条原子操作完成之后执行.

memory_order_seq_cst: 全部存取都按顺序执行. 这也是 C++11 所有 atomic 原子操作的默认值.

 

constexpr 可以作为编译期常量使用, 不过大部分功能还是可以使用传统 enum 来实现. 但是, 使用 constexpr 进程 metal programming 是一个新趋势, 如图: 其中 f 是一个编译期常量, 通过单步调试可以发现, Fabonacci 无法 step into.

 

关于可变数量模板, 这里有一份极好的解释: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2087.pdf请搜索 printf 的实现. 11 把更多的工作留给了编译器去做, 这会不会让原本已经非常慢的编译变得更加慢? 我心有余悸, 或是杞人忧天.

变长模板中也存在一些易错点: 如模板解包的时候, 比如有 `typename ...Arg`, 那么 `class C<Arg...>` 和 `class C<Arg>...` 是不一样的. 前者代表 `class C<X, Y>`, 后者代表 `class C<X>, class C<Y>`.

 

空指针一直是 C++ 的一个痛处. 因为 NULL 要么是 0, 那么是 (void *)0. 然而两者都是用语法技巧来躲避编译错误, 它们的问题如下图. 对于空指针, 11 给出了更好的答案: nullptr.

nullptr 的类型是 nullptr_t, nullptr_t 的 ==, <=, >= 可以和任何 nullptr_t 以及指针类型做运算, 而且都返回 true. 但是在我的 vs2013preview 里测试, 发现有地址的指针总是大于 nullptr, 因此推断 vs2013preview 的实现还是一个整数.

此外, 11 规定, nullptr 的大小和 void * 的大小保持一致.

 

`=default` 可以在类中产生默认的构造函数和 operator= 函数. 这有助于保持一个类是 POD 的. `=delete` 则显示删除某个函数, 即不能够生成或使用该函数. 但是标准不建议将 `=delete` 与 'explicit' 共用, 这会产生一些混乱, 请参考 <深入理解 C++11> p.232

 `=delete` 除了可以作用在类中, 还可以作用在类外, 这种用法通常是禁止类型转换带来的副作用. 再次声明, 不要将 `explicit` 与 `=delete` 合用. 因为两者合用代表 '删除显示的构造', 也就是阻止显示构造而允许隐士构造, 这往往不是我们想要的.

因为 `=delete` 可以作用在普通函数上, 所以我们可以对全局的 new 进行删除, 这样就阻止了某个对象在堆上创建. 这真是一个歪点子.

 

如果你没使用过 lambda, 那就没使用过 C++11! lambda 可以方便的将 "短小精悍" 的功能随意的插入代码之间. 我们曾经为了和 stl 配合遍历一个稍复杂些的容器, 通常都写一个 class 并重载 `operator()`, 这既不直观, 书写也不方便. lambda 你姗姗来迟啊...

lambda 的全貌: `[capture](paramters) mutable -> return-value {statement}`. 默认情况下, lambda 总是一个 const 函数, 不过 mutable 可以取消其常量性.

lambda 有一个重要的 "陷阱". 即: 通过 "值传递" 方式捕获的变量, 在其声明的时候值就已经定好了. 而 "引用传递" 方式捕获的变量, 会随着程序运行而修改.

只有具有函数作用域的变量, 才能被 lambda 捕获, 全局变量是不行的. 这也避免了全局变量没有初始化就被 lambda 使用值传递方式捕获, 而产生错误的行为.

有的时候, lambda 会报和构造函数有关的错误, 这是因为 lambda 的底层实现可能就是一个匿名重载了 `operator()` 的 class, 也就是我们熟悉的仿函数.

`[](){}` 这就是一个合法的 lambda, 更有甚者, 会写出这样的 lambda `[]{}`.

`auto i = [](){return 1;}();` 注意最后的一对小括号, lambda 允许你定义的时候直接调用.

每个 lambda 的作用域都是其所在函数.

lambda 在 11 标准中默认都是内联的.

lambda 美中不足, 没有 const & 捕获方式.

 


 

另:

关于本书的勘误, 是我自己总结的, 可能不是很全, 希望能对你有帮助并期待你的补充: http://www.cnblogs.com/walfud/articles/2057799.html (搜索 "<深入理解 C++11: C++11 新特性解析与应用>").

我的一篇关于 C++11 最讨人喜欢的几个特性的博客: http://www.cnblogs.com/walfud/articles/3280629.html

我的另一篇关于 C++11 FAQ 的博客: http://www.cnblogs.com/walfud/articles/2846227.html

某日, 我看了一篇博客, 是说 c++11 中几个违背直觉的地方, 虽说有少许错误, 但是还是值得思考的: http://url.cn/GYElGe

11 中引入了正则表达式, 这是一个卓越的特性, 但是本书没有提及也没有介绍. 请参考: http://www.cnblogs.com/walfud/articles/2846227.html 中搜索 "regular expression".

posted @ 2013-09-01 10:30  walfud  阅读(8380)  评论(0编辑  收藏  举报