读书札记之 —— 《Exceptional C++ Style》

这本书的中文名叫做《C++编程剖析》,从中文名字中你很难发现这本书和《Exceptional C++》以及《More Exceptional C++》是同一个系列的书(萨特的《Exceptional》系列) 。这本书和前两本是一脉相承的,只是感觉作者的写作风格(或者是译者的翻译风格)和 前两本有略微的不同。三本书依次读下来会发现作者的写作风格越来越幽默。

这三本书是萨特的Guru of the Week的打印版本,基本上的内容似乎都可以在 GOTW 上面找到,其中#1 - #30基本上出现在《Exceptional C++》一书中,#31 - #62出现 在《More Exceptional C++》一书中,剩余的大部分则出现在这本书里面。如果你对于原 文比较感兴趣可以通过上面的连接去找找看。

正如其名《EXceptional C++ Style》一书中花了大量的篇幅去讨论编码的风格问题,这也 难怪作者会在这本书之后去写一本《C++ Coding Standards》专门讨论C++的编程风格。此 外萨特还和C++语言之父在github上起草编写了 CppCoreGuidelines

这本书一共 40 个条款,我直接跳过了其中的9-10两条和26-27两条,不看前者是因为 export这个特性很多的编译器不支持,而且在新的标准中已经废弃;至于后面两条涉及的 是优化问题,作者的核心理念是对于优化专业知识非常的重要,但是我没有关于国际象棋 方面的专业知识。

函数重载

这本书没有专门的条款去说明重载决议方面的问题,但是看完只会留下最深的印象的估计 就是它了。重载决议发生在查找模板特化之前,所以如果你特化一个函数模板,它很可能 根本起不了任何的作用,因为它很可能在重载决议阶段就已经被PASS掉了。同样的问题在 new operator中也存在,如果你为你的类重载了三种形式之一而不重载其他两种,那么 其他两种形式无法正常工作,因为在重载决议的时候,编译器只能找到你定制的那一个版 本的new operator从而导致参数不匹配的问题而调用失败。

1
2
3
4
5
6
7
8
9
class Widget {
public:
static void* operator new(std::size_t);
};

int main() {
Wdiget* w = new Widget; // OK,调用自己定制的版本
Wdiget* w = new(std::nothrow) Widget; // ERROR,无法找到对应的版本
}

此外,对于同一个成员函数的重载版本,如果分属不同的访问权限,同样很可能会出现这 种尴尬的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <string>
#include <iostream>

class Widget {
public:
void Print(const std::string &str) { // 1
std::cout << str << '\n';
}
private:
void Print(const char *str) { // 2
Print(std::string(str));
}

};

int main() {
Widget w;
w.Print("hello world");
}

上面这段代码是没有办法通过编译的,因为重载决议发生在权限判断之前。而重载决议发 现2是最合适的版本,而权限判定发现它没有权限访问,即使存在1这样的可行版本, 它也依旧无法使用。

private 到底意味着什么

private 意味者名字的可访问性,但是并不意味着名字可见性。所以才会出现上面提到的 错误。当然这里还涉及到的另一位关键字是名字,如果我们通过函数指针绕过名字的可 见性,private的成员函数依旧是可以被外界调用的,详见我的另外一篇博文《C++中访 问私有成员的方法》的第三种方法。

异常说明符

不要使用异常说明符,在C++11中这东西被废弃了,这本书中详细的讨论了为什么它会被废 弃。主要的原因就是,理想很丰满,但是现实很骨感。

虚函数

第一次听到 NVI 是在《Effective C++》中,梅耶轻描淡写的说这是很多人推崇的一种 很有意思的观点,很显然萨特就是这样的人。

通过模板方法模式,固定类的接口,把定制内容封装在 private 下的虚函数中,同时也 可以做一些额外的检测操作。我第一次发现NVI的强大是在看完《程序员修炼之道》的按合约编程之后,而这种风格的编程目前似乎被很多人推崇。

如果想要达到更好的接口和实现的分离,可以使用PImpl手法,或者进一步通过使用策略 模式达到更大的灵活性。

内存模型

四个层次,三种形式。

内存管理有四个层次

  1. 操作系统接口
  2. 编译器的默认运行时库
  3. 标准库和标准分配器
  4. 用户自定义的容器或用户自定义的分配器

new operator() 有三种形式

嗯,刚学C++那会儿只知道new operator的最基本的形式,后来读《深度探索C++对象模 型》一书的时候发现有placement new operator的存在。读完这本书才知道原来 nothrow new是它的第三种形式。

内联

《More Exceptional C++》一书中已经对于内联做了讨论,本书对讨论进行拓展。总的来 说还是那句老话,内联是一种优化,而过早的优化是一件愚蠢的事情。

我觉得迟早会有一天 inline 关键字会像 register 关键字一样形同虚设。

宏是封装的天敌,它无视作用域,无视类型,悄无声息,形同鬼魅。所以珍爱生命,远离 宏(真的,因为宏造成的BUG会一点点的耗尽你的生命)。

远离生僻特性,知道,但是当他们不存在

双字符,三字符之类的东西,大部分情况下成事不足败事有余,所以不要去动他们。

尽量把函数写成非成员非友元

第一次接受这个观点在《Effective C++》一书,而对它的进一步的认识则来自《 Exceptional C++》中关于接口规则的讨论,《Exceptional C++ Style》透侧的分析了 std::string 也就是 basic_string<char> 没有遵守这个建议而存在的问题。我开始 也不敢相信,臃肿的它会有103个成员函数(现在估计是只多不少)。