[C++] 编程实践之1: Google的C++代码风格5:其它C++特性

##其它C++特性
###引用参数

所有按引用传递的参数必须加上const。

定义:

  • 在C语言中,如果函数需要修改变量的值,参数必须为指针,如int foo(int *pval)。在C++中,函数还可以声明引用参数:int foo(int &val)。

优点:

  • 定义引用参数防止出现(*pval)++这样丑陋的代码。像拷贝构造函数这样的应用也是必需的,而且更明确,不接受NULL指针。

缺点:

  • 容易引起误解,因为引用在语法上是值变量却拥有指针的语义。

结论:

  • 函数参数列表中,所有引用参数都必须是const:
void Foo(const string &in, string *out);

事实上这在Google Code是一个硬性约定:输入参数是值参或者const引用,输出参数为指针。输入参数可以是const指针,但绝不能是非const的引用参数,除非用于交换,比如swap()。

  • 有时候,在输入形参中用const T*指针比const T&更明智。比如:
    • 您会传null指针。
    • 函数要把指针或者对地址的引用赋值给输入形参。
  • 总之大多时候输入形参往往是const T&。若用const T说明输入另有处理。所以若您要用const T,则应有理有据,否则会害得读者误解。

###右值引用

只在定义移动构造函数与移动赋值操作符时使用右值引用。不要使用std::forward。

定义:

  • 右值引用是一种智能绑定到临时对象的引用的一种,其语法与传统的引用语法相似。例如,void f(string&& s);声明了一个其参数是一个字符串的右值引用的函数。

优点:

  • 用于定义移动构造函数(使用类的右值引用进行构造的函数)使得移动一个值而非拷贝之成为可能。例如,如果v1是一个vector,则auto v2(std::move(v1))将很可能不再进行大量的数据复制而只是简单地进行指针操作,在某些情况下这将带来大幅度的性能提升。
  • 右值引用使得编写通用的函数封装来转发其参数到另外一个函数成为可能,无论其参数是否是临时对象都能正常工作。
  • 右值引用能实现可移动但不可拷贝的类型,这一特性对那些在拷贝方面没有实际需求,但有时又需要将它们作为函数参数传递或者塞入容器的类型很有用。

缺点:

  • 右值引用是一个相对比较新的特性(由C++11引入),它尚未被广泛理解。类似引用崩溃,移动构造函数的自动推导这样的规则都是很复杂的。

结论:

  • 只在定义移动构造函数与移动赋值操作时使用右值引用,不要使用std::forward功能函数。你可以会使用std::move来表示将值从一个对象移动而不是复制到另一个对象。

###函数重载

若要用好函数重载,最好能让读者一看调用点(call site)就胸有成竹,不用花心思猜测调用的重载函数到底是哪一种。该规则适用于构造函数。

定义:

  • 你可以编写一个参数类型为const string&的函数,然后用另一个参数类型为const char*的函数重载它:
class MyClass {
public:
	void Analyze(const string& text);
	void Analyze(const char* text, size_t textLen);
};

优点:

  • 通过重载参数不同的同名函数,令代码更加直观。模板化代码需要重载,同时为使用者带来方便。

缺点:

  • 如果函数单单靠不同的参数类型而重载,读者就得十分熟悉C++五花八门的匹配规则,以了解匹配过程具体到底如何。另外,当派生类只重载了某个函数的部分变体,继承语言容易令人困惑。

结论:

  • 如果您打算重载一个函数,可以试试改在函数名里加上参数信息。例如,用AppendString()和AppendInt()等,而不是一口气重载多个Append()。

###缺省参数

我们不允许使用缺省函数参数,少数极端情况除外。尽可能改用函数重载。

优点:

  • 当您有依赖缺省参数的函数时,您也许偶尔会修改这些缺省参数。通过缺省参数,不用再为个别情况而特意定义一大堆函数了。与函数重载相比,缺省参数语法更为清晰,代码少,也很好地区分了“必选参数”和“可选参数”。

缺点:

  • 缺省参数会干扰函数指针,害得后者的函数签名(function signature)往往对不上所实际需要调用的函数签名。即在一个现有函数添加缺省参数,就会改变它的类型,那么调用其它地址的代码可能会出错,不过函数重载就没这个问题了。此外,缺省参数会造成臃肿的代码,毕竟它们在每一个调用点(call site)都有重复。函数重载正好相反,毕竟它们所谓的“缺省参数”只会出现在函数定义里。

结论:

  • 由于缺点并不是很严重,有些人依旧偏爱缺省参数胜于函数重载。所以除了以下情况,我们要求必须显式地提供所有参数:
    • 其一,位于.cc文件里的静态函数或者匿名空间函数,毕竟都只能在局部文件里调用该函数了。
    • 其二,可以在构造函数里用缺省参数,毕竟不可能取得它们的地址。
    • 其三,可以用来模拟变长数组。
// 通过空AlphaNum以支持四个形参
string strCat(const AlphaNum& a,
			  const AlphaNum& b = gEmptyAlphaNum,
			  const AlphaNum& c = gEmptyAlphaNum,
			  const AlphaNum& d = gEmptyAlphaNum);

###变长数组和alloca()

我们不允许使用变长数组和alloca()。

优点:

  • 变长数组具有浑然天成的语法。变长数组和alloca()也都很高效。

缺点:

  • 变长数组和alloca()不是标准C++的组成部分。更重要的是,它们根据大小动态分配堆栈内存,会引起难以发现的内存越界bugs:“在我的机器上运行的好好的,发布后缺莫名其妙的挂掉了”。

结论:

  • 改用更安全的分配器(allocator),就像std::vector或者std::unique_ptr<T[]>。

###友元

我们允许合理的使用友元类以及友元函数。

通常友元应该定义在同一文件内,避免代码读者跑到其它文件查找使用该私有成员的类。经常用到友元的一个地方是将FooBuilder声明为Foo的友元,以便FooBuilder正确构造Foo的内部状态,而无需将该状态暴露出来。某些情况下,将一个单元测试类声明成待测的友元会很方便。
  友元扩大了(打没有打破)类的封装边界。某些情况下,相对于将类成员声明为public,使用友元是更好的选择,尤其是如果你只允许另一个类访问该类的私有成员时。当然,大多数类都只应该通过其提供的公有成员进行互操作。

###异常

我们不使用C++异常。

优点:

  • 异常允许应用高层决定如何处理在底层嵌套函数中“不可能发生”的失败(failures),不用管那些含糊且容易出错的错误代码。
  • 很多现代语言都用异常。引入异常使得C++与Python,Java以及其它类C++的语言更一脉相承。
  • 有些第三方C++库依赖异常,禁用异常就不好用了。
  • 异常是处理构造函数失败的唯一途径。虽然可以用工厂函数或者Init()方法代替异常,但是前者要求在堆栈分配内存,后者会导致刚创建的实例处于“无效”状态。
  • 在测试框架里很好用。

缺点:

  • 在现有函数中添加throw语句时,您必须检查所有调用点。要么让所有调用点统统具备最低限度的异常保证安全,要么眼睁睁地看异常一路欢快地往上跑,最终中断掉整个程序。例如:f()调用g(),g()又调用h(),且h()抛出的异常被f捕获。当心g,否则会没妥善清理好。
  • 还有更常见的,异常会彻底扰乱程序的执行流程并难以判断,函数也许会在您意料不到的地方返回。您或许会加一大堆何时何处处理异常的规定来降低风险,然而开发者的记忆力负担更重了。
  • 异常安全需要RAII和不同的编码时间。要轻松编写出正确的异常安全代码需要大量的支持机制。更进一步地说,为了避免读者理解整个调用表,异常安全必须隔绝从持续状态写到“提交”状态的逻辑。这一点有利有弊(因为你也许不得不为了隔离提交而混淆代码)。如果允许使用异常,我们就不得不时刻关注这样的弊端,即使有时它们并不值得。
  • 启用异常会增加二进制文件数据,延长编译时间(或许影响小),还可能加大地址空间的压力。
  • 滥用异常会变相鼓励开发者去捕获不合时宜,或本来就已经没法恢复的“伪异常”。比如,用户的输入不符合格式要求时,也用不着抛异常。如此之类的伪异常列都列不完。

结论:

  • 从表面上看来,使用异常利大于弊,尤其是在新项目中。但是对于现有代码,引入异常会牵连到所有相关代码,如果新项目允许异常向外扩散,在跟以前未使用异常的代码整合时也将是个麻烦。因为Google现有的大多数C++代码都没有异常处理,引入带有异常处理的新代码相当困难。
  • 鉴于Google现有代码不接受异常,在现有代码中使用异常比在新项目中使用的代价多少要大一些。迁移过程比较慢,也容易出错。我们不相信异常的使用是有效替代方案,如错误代码,断言等会造成严重负担。
  • 我们并不是基于哲学或者道德层面反对使用异常,而是在实践的基础上。我们希望在Google使用我们自己的开源项目,但项目中使用异常会为此带来不便,因此我们也建议不要在Google的开源项目中使用异常。如果我们需要把这些项目推倒重来显然不太现实。
  • 对于Windows代码来说,也有特例。请参加后续注解。

###运行时类型识别

我们禁止使用RTTI。

定义:

  • RTTI允许程序员在运行时识别C++类对象的类型。它通过使用typeid或者dynamic_cast完成。

优点:

  • RTTI的标准替代(下面将描述)需要对有问题的类层级进行修改或者重构。有时这样的修改并不是我们所想要的,甚至是不可取的,尤其是在一个已经广泛使用的或者成熟的代码中。
  • RTTI在某些单元测试中非常有用。比如进行工厂类测试时,用来验证一个新建的对象是否为期望的动态类型。RTTI对于管理对象和派生对象的关系也很有用。
  • 在考虑多个抽象对象时,RTTI也非常好用,例如:
bool Base::Equal(Base* other) = 0;
bool Derived::Equal(Base* other) {
	Derived* that = dynamic_cast<Derived*>(other);
	if (that == NULL)
		return false;
	...
}

缺点:

  • 在运行时判断类型通常意味着设计问题。如果你需要在运行期间确定一个对象的类型,这通常说明你需要考虑重新设计你的类。
  • 随意地使用RTTI会使得你的代码难以维护。它使得基于类型的判断树或者switch语句散步在代码各处。如果以后要进行修改,你就必须检查它们。

结论:

  • RTTI有合理的用途但是容易被滥用,因此在使用时请务必注意。在单元测试中可以使用RTTI,但是在其他代码中请尽量避免,尤其是在新代码总,使用RTTI前务必三思。如果你的代码需要根据不同的对象类型执行不同的行为的话,请考虑用一下的两种替代方案之一查询类型:
    • 虚函数可根据子类类型的不同而执行不同代码。这是把工作交给了对象本身去处理。
    • 如果这一工作需要在对象之外完成,可以考虑使用双重分发的方案,例如使用访问者设计模式。这就能够在对象之外进行类型判断。
  • 如果程序能够保证给定的基类实例实际上都是某个派生类的实例,那么就可以自由使用dynamic_cast,在这种情况下,使用dynamic_cast也是一种替代方案。
  • 基于类型的判断树是一个很强的暗示,它说明你的代码已经偏离正轨了。不要像下面这样。因为一旦在类层级中加入新的子类,像这样的代码往往会崩溃。而且一旦某个子类的属性改变了,你很难找到并修改所有受影响的代码块。
if (typeid(*data) == typeid(D1)) {
	...
} else if (typeid(*data) == typeid(D2)) {
	...
} else if (typeid(*data) == typeid(D3)) {
	...
}
  • 不要去手工实现一个类似RTTI的方案。反对RTTI的理由同样适用于这些方案,比如带类型标签的类继承体系。而且,这些方案会掩盖你的真实意图。
    ###类型转换

使用C++的类型转换,如static_cast。不要使用int y = (int)x或者int y = int(x)等转换方式。

定义:

  • C++采用了有别于C的类型转换机制,对转换操作进行归类。

优点:

  • C语言的类型转换问题在于模棱两可的操作;有时是在做强制转换(如(int)3.5),有时是在做类型转换(如(int)“hello”)。另外,C++的类型转换在查找时更醒目。

缺点:

  • 恶心的语法。

结论:

  • 不要使用C风格类型转换,而应该使用C++风格。
    • 用static_cast替代C风格的值转换,或某个类指针需要明确的向上转换为父类指针时。
    • 用const_cast去掉const限定符。
    • 用reinterpret_cast指针类型和整型或其它指针之间进行不安全的相互转换。仅在你对所做的一切了然于心时使用。
  • 至于dynamic_cast,可以参见运行时类型识别

###

只在记录日志时使用流。

定义:

  • 流用来替代printf()和scanf()。

优点:

  • 有了流,在打印时不需要关心对象的类型,不用担心格式化字符串与参数列表不匹配(虽然在gcc中使用printf也不存在这个问题)。流的构造和析构函数会自动打开和关闭对应的文件。

缺点:

  • 流使得pread()等功能函数很难执行。如果不使用printf风格的格式化字符串,某些格式化操作(尤其是常用的格式字符串%.*s)用流处理性能是很低的。流不支持字符串操作符重新排序(%1s),而这一点对于软件国际化很有用。

结论:

  • 不要使用流,除非是日志接口需要。使用printf之类的代替。
  • 使用流还有很多利弊,但代码一致性胜过一切。不要在代码中使用流。

拓展讨论:

  • 对于这一条规则存在一些争议,这人给出点深层次的原因。回想一下唯一性原则(only one way):我们希望在任何时候都只使用一种确定的I/O类型,使得代码在所有I/O处都保持一致。因此,我们不希望用户来决定是使用流还是printf+read/write。相反,我们应该决定到底用哪一种方式。把日志作为特例是因为日志时一个非常独特的引用,还有一些是历史原因。
  • 流的支持者们主张流是不二之选,但观点并不是那么清晰有力。他们指出的流的每一个优势也都是其劣势。流最大的优势是在输出时不需要关心打印对象的类型,这是一个亮点。同时也是一个不足:你很容易用错类型,而编译器不会报警。使用流时容易造成下面这类错误。由于<<被重载,编译器不会报错。就因为这一点我们反对使用操作符重载。
cout << this;	// 输出地址
cout << *this;	// 输出值
  • 有人说printf的格式化丑陋不堪,易读性差。但是流也好不到哪儿去。看看下面两段代码吧。你可能会说:“把流封装一下就会比较好了”,这儿可以,其他地方呢?而且不要忘了,我们的目标是使得语言更紧凑,而不是添加一些别人需要学习的新装备。
cerr << "Error connecting to '" << foo->bar()->hostname.first << ":"
	 << foo->bar()->hostname.second << ": " << strerror(errorno);
fprintf(stderr, "Error connecting to '%s:%u:%s",
		foo->bar()->hostname.first, foo->bar()->hostname.second, strerror(errorno));
  • 每一种方式都是各有利弊,“没有最好,只有更适合”。简单性原则告诫我们必须从中选择其一,最后大多数决定采用printf + read/write。

###前置自增和自减

对于迭代器和其他模板对象使用前缀形式(++i)的自增自减运算符。

定义:

  • 对于变量在自增(++i或者i++)或自减(–i或者i–)后表达式的值又没有用到的情况下,需要确定到底是使用前置还是后置的自增(自减)。

优点:

  • 不考虑返回值的话,前置自增(++i)通常要比后置自增(i++)效率更高。因为后置自增(或自减)需要对表达式的值i进行一次拷贝。如果i是迭代器或其他非数值类型,拷贝的代价是比较大的。既然两种自增方式实现的功能一样,为什么不总是使用前置自增呢?

缺点:

  • 在C开发中,当表达式的值未被使用时,传统的做法是使用后置自增,忒儿是在for循环中。有些人觉得后置自增更加易懂,因为这很像自然语言,主语(i)在谓语(++)前。

结论:

  • 对简单数值(非对象),两种都无所谓。对迭代器和模板类型,使用前置自增(自减)。

###const的用法

我们强烈建议你在任何可能的情况下都要使用const。此外有时改用C++11推出的constexpr更好。

定义:

  • 在声明的变量或者参数前加上关键字const用于指明变量值不可被篡改(如const int foo)。为类中的函数加上const限定符,表明该函数不会修改类成员变量的状态。

优点:

  • 大家很容易理解如何使用变量。编译器可以更好地进行类型检测,相应地,也能生成更好的代码。人们对编写正确的代码更加自信,因为他们知道所谓的函数被限定了能或不能修改变量值。即使是在无锁的多线程编程中,人们也知道什么样的函数是安全的。

缺点:

  • const是入侵性的:如果你向一个函数传入const变量,函数原型声明也必须对应const参数(否则变量需要const|_cast类型转换),在调用库函数时显得尤其麻烦。

结论:

  • const变量、数据成员,函数和参数为编译时类型检测增加了一层保障,便于尽早发现错误。因此,我们强烈建议在任何可能的情况下使用const:
    • 如果函数不会修改你传入的引用或者指针类型参数,该参数应声明为const。
    • 尽可能将函数声明为const。方位函数应该总是const。其它不会修改任何数据成员,未调用非const函数,不会返回数据成员非const指针或引用的函数也应该声明为const。
    • 如果数据成员在对象构造之后不再发生变化,可将其定义为const。
  • 然而,也不要发了疯似的使用const。像const int * const * const x;就有些过了,虽然它非常精确地描述了常量x,关注真正有帮助意义的信息:前面的例子写成const int** x就够了。
  • 关键字mutable可以使用,但是在多线程中是不安全的,使用时首先要考虑线程安全。

const的位置:

  • 有人喜欢int const *foo形式,不喜欢const int* foo,他们认为前者更一致因此可读性也更好:遵循了const总位于其描述的对象之后的原则。但是一致性原则不适用于此,“不要过度使用”的声明可以取消大部分你原本想保持的一致性。将const放在前面才更易读,因为在自然语言中形容词(const)是在名词(int)之前。
  • 这是说,我们提倡但不强制const在前,但是要保持代码一致性!

###constexpr用法

在C++11里,用constexpr来定义真正的常量,或实现常量初始化。

定义:

  • 变量可以被声明成constexpr以表示它是真正意义上的常量,即在编译时和运行时都不变。函数或构造函数也可以被声明成constexpr,以用来定义constexpr变量。

优点:

  • 如今constexpr就可以定义浮点式的真常量,不用再依赖字面值了;也可以定义用户自定义类型的常量;甚至也可以定义函数调用所返回的常量。

缺点:

  • 若过早地把变量优化成constexpr变量,将来又要把它改为常规变量时,挺麻烦的;当前对constexpr函数和构造函数中允许 的限制可能会导致这些定义中解决的方法模糊。

结论:

  • 靠constexpr特性,方才实现了C++在接口上打造真正常量机制的可能。好好用constexpr来定义真常量以及支持常量的函数。避免复杂的函数定义,以使其能够与constexpr一起使用。千万别痴心妄想地想靠constexpr来强制代码“内联”。

###整型

C++內建整型中,仅使用int。如果程序中需要不同大小的变量,可以使用<stdint.h>中长度确定的整型,例如int16_t。如果您的变量可能不小于2^31(2GiB),就用64位变量比如int64_t。此外要留意,哪怕您的值并不会超出int所能够表示的范围,在计算过程中也可能会溢出。所以拿不准时,干脆用更大的类型。

定义:

  • C++没有指定整型的大小。通常人们假定short是16位,int是32位,long是32位,long long是64位。

优点:

  • 保持声明统一。

缺点:

  • C++中整型大小因编译器和体系结构的不同而不同。

结论:

  • <stdint.h>定义了int16_t,uint32_t,int64_t等整型,在需要确保整型大小时可以使用它们代替short,unsigned long long等。在C整型中,只使用int。在合适的情况下,推荐使用标准类型如size_t和ptrdiff_t。
  • 如果已知证书不会太大,我们常常会使用int,如循环计数。在类似的情况下使用原生类型int。你可以认为int至少为32位,但不要认为它会多于32位。如果需要64位整型,用int64_t或者uint64_t。
  • 对于大整数,使用int64_t。
  • 不要使用uint32_t等无符号整型,除非你是在表示一个位组而不是一个数值,或是你需要定义二进制补码溢出。尤其是不要为了指出数值永远不会为负,而使用无符号类型。相反,你应该使用断言来保护数据。
  • 如果您的代码涉及容器返回的大小(size),确保其类型足以应付容器各种可能的用法。拿不准时,类型越大越好。
  • 小心整型类型转换和整型提升(比如int与unsigned int运算时,前者被提升为unsigned int而有可能溢出),总有意想不到的后果。

关于无符号整数:

  • 有些人,包括一些教科书作者,推荐使用无符号类型表示非负数。这种做法试图达到自我文档化。但是在C语言中,这一优点被由其导致的bug所淹没。看看下面的例子:
for (unsigned int i = foo.Length() - 1; i >= 0; --i) ...

上述循环永远不会退出!有时gcc会发现该bug并报警,但是大部分情况下都不会。类似的bug还会出现在比较有符号变量和无符号变量时。主要是C的类型提升机制会导致无符号类型的行为出乎你的意料。

  • 因此,使用断言来指出变量为非负数,而不是使用无符号类型!

###64位下的可移植性

代码应该同时对64位和32位系统友好,处理打印、比较结构体时应该切记。

  • 对于某些类型,printf()的指示符在32位和64位系统上可移植性不是很好。C99标准定义了一些可移植的格式化指示符。不幸的是,MSVC7.1并非全部支持,而且标准中也有所遗漏,所以有时我们不得不自己定义一个丑陋的版本(头文件inttypes.h仿标准风格):
// printf macros for size_t, in the style of inttypes.h
#ifdef _LP64
#define __PRIS_PREFIX "z"
#else
#define __PRIS_PREFIX
#endif

// Use these macros after a % in a printf format string
// to get correct 32/64 bit behavior, like this:
// size_t size = records.size();
// printf("%"PRIuS"\n", size);
#define PRIdS __PRIS_PREFIX "d"
#define PRIxS __PRIS_PREFIX "x"
#define PRIuS __PRIS_PREFIX "u"
#define PRIXS __PRIS_PREFIX "X"
#define PRIoS __PRIS_PREFIX "o"

|类型| 不要使用 | 使用 |备注|
| ------- |:---------| :-----|
| void*(或其它指针类型) | %lx | %p |
| int64_t | %qd, %lld | %“PRId64” |
| uint64_t | %qu, %llu, %llx | %“PRIu64”, %“PRIx64” |
| size_t | %u | %“PRIuS”, %“PRIxS” | C99规定%zu |
| ptrdiff_t | %d | %“PRIdS” | C99规定%zd |
注意PRI*宏会被编译器扩展为独立字符串。因此如果使用非常量的格式化字符串,需要将宏的值而不是宏名插入格式中。使用PRI*宏同样可以在%后包含长度指示符。例如printf(“x=%30"PRIuS”\n, x)在32位Linux上将被展开为printf(“x = %30” “u” “\n”, x),编译器当成printf(“x = %30u\n”, x)处理。

  • 记住sizeof(void*) != sizeof(int)。如果需要一个执政大小的整数,要用intptr_t。
  • 你要非常小心的对待结构体对齐,尤其是要持久化到磁盘上的结构体。在64位系统中,任何含有int64_t/uint64_t成员的类/结构体,缺省都以8字节在结尾对其。如果32位和64位代码要共用持久化的结构体,需要雪豹两种体系结构下的结构体对齐一致。大多数编译器都允许调整结构体对齐。gcc中可使用__attribute__((packed)). MSVC 则提供了 #pragma pack() 和 __declspec(align())(解决方案的项目属性里面也可以直接设置)。
  • 创建64位常量时使用LL或者ULL作为后缀,例如:
int64_t my_value = 0x123456789LL;
uint64_t my_mask = 3ULL << 48;
  • 如果你确实需要32位和64位系统具有不同代码,可以使用#ifdef _LP64指令来切分32/64位代码。(尽量不要这么做,如果非用不可,尽量使修改局部化)

###预处理宏

使用宏是要非常谨慎,尽量以内联函数,枚举和常量替代之。

宏意味着你和编译器看到的代码是不同的。这可能会导致异常行为,尤其因为宏具有全局作用域。

值得庆幸的是,C++中,宏不像在C中那么必不可少。以往用宏展开性能关键的代码,现在可以用内联函数代替。用宏表示常量可被const变量代替,用宏“缩写”长变量名可被引用代替。用宏进行条件编译…这个,千万别这么做,会令测试更加痛苦(#define防止头文件重包含当然是个特例)。

宏可以做一些其他技术无法实现的事情,在一些代码库(尤其是底层库中)可以看到宏的某些特性(如用#字符串化,用##连接等等)。但在使用前,仔细考虑一下能不能不适用宏达到同样的目的。

下面给出的用法模式可以避免使用宏带来的问题;如果你要使用宏,尽可能遵守:

  • 不要在.h文件中定义宏。
  • 在马上要使用时才进行#define,使用后要立即#undef。
  • 不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称。
  • 不要试图使用展开后会导致C++构造不稳定的宏,不然也至少要附上文档说明其行为。
  • 不要用##处理函数、类和变量的名字。

###0,nullptr和NULL

整数用0,实数用0.0,指针用nullptr或者NULL,字符(串)用"\0"。

整数用0,实数用0.0,这一点是毫无争议的。

对于指针(地址值),到底是用0,NULL还是nullptr,C++11项目用nullptr;C++03项目则用NULL,毕竟它看起来更像指针。实际上,一些C++编译器对NULL的定义比较特殊,可以输出有用的警告,特别是sizeof(NULL)就和sizeof(0)不一样。

字符(串)用’\0’,不仅类型正确而且可读性好。

###sizeof()函数

尽可能是用sizeof(varname)代替sizeof(type)。

使用sizeof(varname)是因为当代码中变量类型改变时会自动更新,您或许会用sizeof(type)处理不涉及任何变量的代码,比如处理来自外部或者内部的数据格式,这时用变量就不合适了。

Struct data;
memset(&data, 0, sizeof(data));

###auto

用auto绕过烦琐的类型名,只要可读性好就继续用,别用在局部变量之外的地方。

定义:

  • C++11中,若变量被声明为auto,那么它的类型就会被自动匹配成初始化表达式的类型。您可以用auto来复制初始化或者绑定引用。
vector<string> v;
...
auto s1 = v[0];			// 创建一份v[0]的拷贝
const auto s2 = v[0];	// s2是v[0]的一个引用

优点:

  • C++类型名有时又长又臭,特别是涉及模板或者命名空间的时候。可以比较下面的代码:
sparse_hash_map<string, int>::iterator iter = m.find(val);
auto iter = m.find(val);	// after reconstruction
  • 没有auto的话,我们不得不在同一个表达式里写同一个类型名两次,无谓的重复,就像下面的代码。有了auto,可以更方便地用中间变量,显式地编写它们的类型,可以更轻松点。
diagnostics::ErrorStatus* status = new diagnostics::ErrorStatus("xyz");

缺点:

  • 类型够明显时,特别是初始化变量时,代码才会够一目了然。但是以下就不一样了。看不出来其类型是啥,x的类型声明恐怕远在几百行之外了。
auto i = x.Lookup(key);
  • 程序员必须会区分auto和const auto&的不同之处,否则会复制错东西。
  • auto和C++11列表初始化的合体令人摸不着头脑,如下所示。它们其实不是同一回事——x是int,y则是std::initializer_list.其它一般不可见的代理类型也有大同小异的陷阱。
auto x(3);		// 圆括弧
auto y{3};		// 大括弧
  • 如果在接口里用auto,比如声明头文件里的一个常量,那么只要仅仅因为程序员一时修改其值而导致类型变化的话——API就要翻天覆地了。

结论:

  • auto只能在局部变量里用。别用在文件作用域变量,命名空间作用域变量和类数据成员里。永远别列表初始化auto变量。
  • auto还可以和C++特性“尾置返回类型(trailing return type)”一起用,不过后者只能用在lambda表达式里。

###列表初始化

你可以用列表初始化。

早在C++03里,聚合类型(aggregate types)就已经可以被列表初始化了,比如数组和不自带构造函数的结构体:

struct Point { int x; int y; };
Point p = {1, 2};

C++11中,该特性得到进一步的推广,任何对象类型都可以被列表初始化,示范如下:

vector<string> v{"foo", "bar"};					// Vector接受了一个初始化列表
vector<string> v = {"foo", "bar"};				// 不考虑细节上的微妙差别,和上一行大致相同
auto p = new vector<string>{"foo", "bar"};		// 可以配合new一起使用
map<int, string> m = {{1, "one"}, {2, "2"}};	// map接收了一些pair,列表初始化大显神威!
vector<int> test_function() {return {1, 2, 3}};	// 初始化列表也可以用在返回类型的隐式转换
for(int i : {-1, -2, -3}) {}					// 初始化列表可迭代
void TestFunction2(vector<int> v) {}			// 在函数调用里用列表初始化
TestFunction2({1, 2, 3});

用户自定义类型也可以定义接收std::initializer_list的构造函数和赋值运算符,以自动列表初始化:

class MyType {
public:
	MyType(std::initializer_list<int> init_list) {
		for (int i : init_list)
			append(i);
	}
	MyType& operator=(std::initilizer_list<int> init_list) {
		clear();
		for (int i : init_list)
			append(i);
	}
};

最后,列表初始化也适用于常规数据类型的构造,哪怕没有接收std::initializer_list的构造函数:

double d{1.23};
// MyOtherType没有std::initializer_list构造函数
// 直接上接收常规类型的构造函数
classMyOtherType {
public:
	explicit MyOtherType(string);
	MyOtherType(int, string);
};
MyOtherType m{"b"};			// 如果构造函数时显式的,您就不能用“= {}”了
MyOtherType m = {1, "b"};

千万别直接列表初始化auto变量,看下面一句,请对比下面两行代码:

auto d = {1.23};		// d即是std::initializer_list<double>
auto d = double{1.23};	// 善哉 -- d即为double,并非std::initializer_list.

###Lambda表达式

适当使用lambda表达式。别用默认lambda捕获,所有捕获都要显式写出来。

定义:

  • Lambda表达式是创建匿名函数对象的一种简易途径,常用于把函数当参数传,例如:
std::sort(v.begin(), v.end(), 
	[](int x, int y) { return Weight(x) < Weight(y); }
);
  • C++首次提出Lambdas,还提供了一系列处理函数对象的工具,比如多态包装器(polymorphic wrapper)std::function。

优点:

  • 传函数对象给STL算法,Lambda最简易,可读性也好。
  • Lambdas,std::functions和std::bind可以搭配成通用回调机制(general purpose callback mechanism):写接收有界函数为参数的函数也很容易了。

缺点:

  • Lambdas的变量捕获略旁门左道,可能会造成悬空指针。
  • Lambdas可能会失控:层层嵌套的匿名函数难以阅读。

结论:

  • 按format小用lambdas表达式怡情。
  • 禁用默认捕获,捕获都要显式写出来。打比方,比起[=](int x) { return x + n; },您该写成[n](int x) { return x + n; }才对,这样读者也好一眼看出n是被捕获的值。
  • 匿名函数始终要简短,如果函数体超过了五行,那么还不如起名,或改用函数。
  • 如果可读性更好,就显式写出lambdas的尾置返回类型,就像auto。

###模板编程

不要使用复杂的模板编程。

定义:

  • 模板编程指的是利用C++模板实例化机制,可以被用来实现编译时刻的类型判断的一系列编程技巧。

优点:

  • 模板编程能够实现非常灵活的类型安全的接口和极好的性能,一些常见的工具比如Google Test,std::tuple, std::function和Boost.Spirit,这些工具如果没有模板是实现不了的。

缺点:

  • 模板编程所使用的技巧对于使用C++不是很熟练的人是比较晦涩难懂的。在复杂的地方使用模板的代码让人更不容易读懂,并且debug和维护起来都很麻烦。
  • 模板编程经常会导致编译出错的信息非常不友好:在代码出错的时候,即使这个接口非常的简单,模板内部复杂的实现细节也会在出错信息中显示,导致这个编译出错信息看起来非常难以理解。
  • 大量的使用模板编程接口会让重构工具(Visual Assist X,Refactor for C++等等)更难发挥用途。首先模板的代码会在很多上下文里面扩展开来,所以很难确认重构对所有的这些展开的代码有用,其次有些重构工具只对已经做过模板类型替换的代码的AST有用。因此重构工具对这些模板实现的原始代码并不有效,很难找出哪些需要重构。

结论:

  • 模板编程有时候能够实现更简洁更易用的接口,但是更多的时候却适得其反。因此模板编程最好只用在少量的基础组件,基础数据结构上,因为模板带来的额外的维护成本会被大量的使用给分担掉。
  • 在使用模板编程或者其他复杂的模板技巧的时候,你一定要再三考虑一下。考虑一下你们团队成员的平均水平是否能够读懂并且能够维护你写的模板代码,或者一个非C++程序员和一些只是在出错的时候偶尔看一下代码的人能够读懂这些错误信息或者能够跟踪函数的调用流程。如果你使用递归的模板实例化,或者类型列表,或者友元函数,又或者表达式模板,或者依赖SFINAE,或者sizeof的trick手段来检查函数是否重载,那么这说明你模板用的太多了,这些模板太复杂了,我们不推荐使用。
  • 如果你使用模板编程,你必须考虑尽可能把复杂度最小化,并且尽量不要让模板对外暴露。你最好只在实现里面使用模板,然后给用户暴露的接口里面并不使用模板,这样能提高你的接口的可读性,并且你应该在这些使用模板的代码上写尽可能详细的注释。你的注释里面应该详细的包含这些代码是怎么用的,这些模板生成出来的代码大概是什么样子的。还需要额外注意在用户错误使用你的模板代码的时候,需要输出更人性化的出错信息。因为这些出错信息也是你的接口的一部分,所以你的代码必须吊证到这些错误在用户看来应该是非常容易理解,并且用户很容易知道如何修改这些错误。

###Boost库

只使用Boost中被认可的库。

定义:

  • Boost库集是一个广受欢迎,经过同行鉴定,免费开源的C++库集。

优点:

  • Boost代码质量普遍较高,可移植性好,填补了C++标准库很多空白,如型别的特性,更完善的绑定器,更好的智能指针。

缺点:

  • 某些Boost库提倡的编程实践可读性差,比如元编程和其他高级模板技术,以及过度“函数化”的编程风格。

结论:

  • 为了向阅读和维护代码的人员提供更好地可读性,我们只允许使用Boost一部分经过认可的特性子集。目前允许使用以下库。我们正在积极考虑增加其它Boost特性,所以列表中的规则将不断变化。
    • Call Traits: boost/call_traits.hpp
    • Compressed Pair: boost/compressed_pair.hpp
    • The Boost Graph Library (BGL): boost/graph,except serialization (adj\_list\_serialize.hpp) and parallel/distributed algorithms and data structures (boost/graph/parallel/* and boost/graph/distributed/*)
    • Property Map: boost/property_map.hpp
    • The part of Iterator that deals with defining iterators: boost/iterator/iterator\_adaptor.hpp, boost/iterator/iterator\_facade.hpp, and boost/function_output\_iterator.hpp.
    • The part of Polygon that deals with Voronoi_diagram comstruction and doesn’t depend on the rest of Polygon: boost/polygon/voronoi\_builder.hpp, boost/polygon/voronoi\_diagram.hpp, and boost/polygon/voronoi\_geometry\_type.hpp.
    • Bimap: boost/bimap
    • Statistical Distribution and Functions: boost/math/distributions
    • Multi-index: boost/multi\_index
    • Heap: boost/heap
    • The flat containers from container: boost/container/flat\_map, and boost/container/flat\_set.
  • 以下库可以用,但是由于如今已经被C++标准库所取代,不再鼓励:
    • Pointer Container: boost/ptr\_container,改用std::unique\_ptr
    • Array: boost/array.hpp,改用std::array

###C++11

适当用C++(前身是C++0x)的库和语言扩展,在贵项目用C++特性前三思可移植性。

定义:

  • C++有众多语言和库上的变革,具体可参见维基百科

优点:

  • 在2014年8月之前,C++一度是官方标准,被大多数C++编译器支持。它标准化很多我们早先就在用的C++扩展,简化了不少操作,大大改善了性能和安全。

缺点:

  • C++11相对于前身,复杂极了:1300页vs800页!很多开发者也不怎么熟悉它。于是从长远来看,前者特性对代码可读性以及维护代价难以预估。我们说不准什么时候采纳其特性,特别是在被迫依赖老式工具的项目上。
  • 和Boost库一样,有些C++扩展提倡实则对可读性有害的编程实践——就像去除冗余检查(比如类型名)以帮助读者,或者鼓励模板元编程等等。有些扩展在功能上与原有机制冲突,容易招致困惑以及迁移代价。

结论:

  • C++特性除了个别情况下,可以用一用。除了本指南会有不少章节会讨论若干C++特性之外,以下特定最好不要使用:
    • 尾置返回类型,比如用auto foo()->int代替int foo()。为了兼容与现有代码的声明风格。
    • 编译时合数<ratio>,因为它涉及一个重模板的接口风格。
    • <cfenv>和<fenv.h>头文件,因为编译器尚不支持。
    • 默认lambda捕获。

###总结

  1. 实际上,缺省参数会改变函数签名的前提是改变了它接收的参数数量,比如把void a()改成void a(int b = 0),开发者改变其代码的初衷也许是,在不改变“代码兼容性”的同时,又提供了可选int参数的余地,然而这终究会破坏函数指针上的兼容性,毕竟函数签名确实变了。
  2. 此外把自带缺省参数的函数地址赋值给指针时,会丢失缺省参数信息。
  3. 还发现滥用缺省参数会害得读者光只看调用代码的话,会误以为其函数接收的参数数量比实际上还要少。
  4. friend实际上只对函数/类赋予了对其所在类的访问权限,并不是有效的声明语句。所以除了在头文件类内部写friend函数/类,还要在类作用域之外正式地声明一遍,最后在对应的.cc文件加以定义。
  5. 本风格指南都强调了“友元应该定义在同一文件内,避免代码阅读者跑到其它文件查找使用该私有成员的类”。那么可以把其声明放在类声明所在的头文件,定义也放在类定义所在的文件。
  6. 由于友元函数/类并不是类的一部分,自然也不会是类可调用的公共接口,于是我主张全集中放在累的尾部,即类的数据成员之后。
  7. 对使用C++异常处理应具有怎样的态度?非常值得一读。
  8. 注意初始化const对象时,必须在初始化的同时值初始化。
  9. 用断言代替无符号整型类型,深有启发。
  10. auto在涉及迭代器的循环语句里挺常用。
  11. 关于auto与尾置返回类型一起用的全新编码风格,参见这里
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值