#编码风格# #Google C++# 其他C++特性(OtherC++Fetures)

目录

 

引用参数(Reference Arguments)

函数重载(Function Overloading)

默认参数(Default Arguments)

可变长度数组和内存申请(Variable-Length Arrays and alloca())

友元(Friends)

异常处理(Excpetions)

运行时类型信息(Run-Time Type Information, RTTI)

类型转换(Casting)

流(Streams)

前置自增和前置自减(Preincrement and Predecrement)

const修饰符的使用(Use of const)

整型类型(Integer Types)

64位兼容性(64-bit Portability)

预处理宏(Preprocessor Macros)

0和空(0 and NULL)

存储容量运算符(sizeof)

增强库(Boost)

C++ 0x库


引用参数(Reference Arguments

        所有用引用传值的变量应该被const修饰。

定义:

        在C语⾔言中,如果函数需要修改一个变量,必须使用指针作为其参数。比如int foo(int*pval)。但在C++中,有了另一种方式,即引用:int foo(int &val)

  • 利:把一个参数定义为引用可以避免丑陋的代码(比如*pval++)。有些程序需要,比如复制构造函数。使程序更明确,不像指针能取得NULL值。
  • 弊:由于引用兼具值表达式和指针的主义,会引起迷惑。

结论:

        所有函数引用参数都应该定义为const引用。

void Foo(const string &in, string *out);

        实际上,将值或者常引用作为输入参数而将指针作为输出参数是谷歌的一个惯例。输入参数也可以是常指针,但不允许非const(non-const)引用。将常指针作为输入参数的一种情况是,你想强调这个参数将不被复制,它在对象的整个⽣生命周期内必须存在,但最好在注释中说明。标准模板库的适配器(比如bind2ndmem_fun)不允许引用参数,这时只有用指针了。

 

函数重载(Function Overloading)

        看到一个函数的调用⽴立即能知道其操作而不是需要⾸首先找出是哪个重载版本被调用了时,才使用重载函数。

class MyClass{

Public:

    void Analyze(const string &text);

    void Analyze(const char *text,size_t textlen);

};

定义:

        利用重载,你可以定义接收不同参数的同名函数,比如接收const string&const char*的同名函数。

  • 利:重载可以使代码更直观。对于模板化的代码,重载可能是必须的;对于访问控制器(Visitor)的实现,重载也是很方便的。
  • 弊:如果函数仅以参数类型不同来重载,读者可能需要深入理解C++复杂的参数匹配规则才能知道是怎么回事。在继承中,子类只重写基类函数的某些版本也会引起迷惑。

结论:

        要想重载函数,考虑根据其参数来命名函数。比如AppendString()AppendInt()就比Append()好。

 

默认参数(Default Arguments)

        除非在以下情况下,不允许使用函数默认参数。

  • 利:你经常会写带有很多默认值的函数,但有时又不得不重载这些默认值。默认参数提供了实现它的简单方法,且不用为了少量例外而定义大量函数。
  • 弊:程序员常常通过查看已有代码来找出调用一个API的方法。默认参数将变得更难维护,因为从其他地方复制-粘贴代码,默认参数可能未被显⽰示。当默认参数不适用于新代码时,复制-粘贴部分将引起问题。

结论:

        除了以下情况,函数必须明确定义每个参数来强制程序员在调用API时考虑传入参数值,而不是简单地接受默认参数。

        一种特殊的例外是当默认参数是用来模拟可变长参数时:

// 通过默认参数,最多⽀支持4个参数
string StrCat(const AlphaNum &a,
    const AlphaNum &b = gEmptyAlphaNum,
    const AlphaNum &c = gEmptyAlphaNum,
    const AlphaNum &d = gEmptyAlphaNum);

 

可变长度数组和内存申请(Variable-Length Arrays and alloca())

        不允许变长数组和内存申请。

  • 利:可变长度数组语句自然,且与alloca()一样,很高效。
  • 弊:可变长度数组和alloc()不是C++标准的一部分。重要的是,它们根据程序栈容量来申请空间,这可能引起内存覆盖缺陷。“在我的机器上一切正常,但做成产品后却神秘死机。”

结论:

使用更安全的内存申请函数,比如scoped_ptr/scoped_array

 

友元(Friends)

        适度使用友元类和友元函数是允许的。

        友元应该和其友类定义在同一个文件中,这样,读者不必再去另一个文件中查看友元使用了该类中的哪些私有成员。友元的一个常见作用是在不暴露一个类的内部细节时利用友元类来正确地构造其内部状态。比如FooBuilderFoo。有时,将一个单元测试类定义为其测试类的友元会很有用。

        友元仅仅是扩展而不是打破类的封装性。当一个类需要访问另一个类的私有成员时,友元比将这个成员公有化更好。然而,类与类的协作只能通过公共成员。

 

异常处理(Excpetions)

        通常不使用C++的异常处理。

  • 利:

    ● 异常处理使在程序的更高层次来处理多层函数嵌套调用的“不可发⽣生”错误成为可能,而且不需要使用隐蔽且容易出错的错误代码簿记;

    ● 异常处理被大多数其他现代程序设计语⾔言采用。在C++中使用异常处理将使其与其他语⾔言如Python、Java保持一致性。

    ● 一些第三方库使用异常处理,如果关闭,可能导致很验证使用这些API。

    ● 异常是唯一能导致构造失败的方式。尽管可以使用工⼚厂函数或者Init()方法来实现构造,但它们分别需要堆申请和“无效”状态。

    ● 异常处理常被用于架构测试。

  • 弊:

    ● 当给一个函数加上抛出(throw)语句时,必须检查其调用链。它们要么进行基本的异常处理,要么忽略异常且无视程序由此而终止运行。举个例子,如果f()调用g(),g()又调用f(),h抛出一个异常,f捕捉到了这个异常,那么g必须注意在异常发⽣生时的清理工作。

    ● 更一般地,异常处理使程序很难从其代码中看出其控制流:程序可能从意想不到的地方返回。这将使维护和调试变得困难重重。你可以通过使用一些异常处理规则来减小这些开销,但一定比开发⼈人员需要了解和理解的多。

    ● 异常安全性需要资源获取即初始化(RAII)和不同的编码实践的支持。为简化正确开发异常安全代码的工作,也需要很多支持机制。进一步,为避免陷入寻找完整函数调用链的⿇麻烦,异常安全代码必须把用于将状态持久化为“提交”阶段的逻辑进行隔离。这既有好处又有开销(也许在你被迫隐藏代码来隔离提交的地方)。允许异常处理,在不值得的情况下也得付出这些代价。

    ● 使用异常处理将增加目标代码量,增加编译时间(通常不明显)并可能增大地址空间的压⼒力。

    ● 异常的可用性可能使开发者在不合时宜时抛出异常或者从异常状态恢复并不安全时使用。比如,无效的用户输入不应该导致异常抛出。

结论:

        表面上,使用异常处理的好处比开销多,尤其是新项目。然而,如果为已有代码引入异常处理,将会引起所有相关代码的变动。如果异常在新项目外可被抛出,则其与未进行异常处理的旧项目的互操作将变得困难重重。由于大多数谷歌的C++项目都不准备采用异常处理机制,因而将很难使用产⽣生异常的新代码。

        考虑到谷歌已有代码未进行异常处理,异常处理的机制引入的花销将远大于新项目。而且,转换过程也将缓慢而且容易出错。再者,异常处理的替代方式,比如错误处理和断⾔言也不会增加很多编程负担。

        我们并不是站在哲学或者道德的立场反对使用异常处理机制,而是站在实用的立场上。因为我们需要使用谷歌开源项目,而且对这些项目引入异常处理很困难,我们不得不在谷歌开源项目中建议不要采用异常处理。如果从头开发这些项目将困难重重。

        对于Windows代码来说例外。

 

运行时类型信息(Run-Time Type Information, RTTI)

        不建议使用RTTI

定义:

        RTTI允许程序员在运行时查看一个对象的类类型。

利:在单元测试时会有用,比如进行工⼚厂类测试时,必须证实一个新创建对象是否是应有动态类型。测试之外罕见其用。

弊:运行时检查类型通常意味着类设计有问题。

结论:

        除了单元测试,不要使用RTTI。确实需要基于对象类型来完成不同的功能时,考虑替代方案。虚方法是使子类执行不同代码的⾸首选方案。这将使对象自己来完成自己特定的工作。如果这些工作位于项目的一些处理代码中,考虑双分派(Double-Dispatch),比如访问器设计模式(Visitor Design Pattern)。这允许对象外的设施可以利用系统内建类型来决定一个类的类型。如果你不赞成这些观点,你可以使用RTTI。但请三思再三思。不要⼿手动 .现 .似RTTI的变通方案。反对RTTI及其变通方案应用的观点就和反对用类型标签对类分层一样多。

 

类型转换(Casting)

        在C++中,需要类型转换时请使用static_cast<>(),不要使用诸如int y = (int)xint y = int(x)的其他形式。

定义:

        C++提供了一种与C不同的因类型而异的类型转换操作。

  • 利:C语⾔言的问题是转换操作的⼆二义性:有时是转换(比如(int)3.5),而有时却是cast(比如(int)”hello”))。C++的类型转换则不存在这个问题,而且,C++的类型转换是显式的和可追踪的。
  • 弊:语句繁琐。

结论:

  • 使用static_cast进行C风格的值转换或者将子类指针提升为基本指针;
  • 使用const_cast去掉const修饰(参见const);
  • 使用reinterpret_cast进行不安全的指针类型转换(比如转换到或者从整型指针和其他类型指针)。当你确定你的做法及其引用的问题时才进行这种操作;
  • 除了在测试中,不要用dynamic_cast。如果在单元测试之外需要使用这种方法来测试一个类的类型,通常意味着你的设计有问题。

 

(Streams)

        流只用于日志记录。

定义:

        流是Cprintf()scanf()的替代实现。

  • 利:使用流,不需要关⼼心输出对象的类型,也不需要像C那样定义一大串格式符了。流的构造和析构函数会自动打开和关闭相关文件。
  • 弊:流不利于随机读取。一些格式(尤其字符串格式习惯用法:%.*s)也不像使用类似printf技巧那样方便。流也不支持有利于国际化的运算符重定序(比如%1s指令)。

结论:

        除非需要日志接口,否则不要使用流。使用类似printf的例程。关于流的争论很多,但正如我们一致强调的,不要使用流。

扩展讨论:

        关于这些问题的争论此起彼伏,所以这⾥里我们深入讨论。重申一下唯一指南原则:我们希望确保每次对于特定类型的I/O的代码都是一样的。为此,我们不允许使用者在使用流还是printf(加上读//等等)之间进行选择,我们的做法是确定一个(即printf)。之所以日志例外,是出于历史原因考虑的,日志是一个很特殊的程序。

        流的支持者认为流是明智的选择,但并不是这样。他们指出的每一个优点,都有相应的缺点。最大的优点是,你根本不需要知道输出对象的类型。但仔细想想,你可能使用了错误类型,而编译器不会警报。在使用流时,很容易犯这种错误:

cout<<this; //打印地址
cout<<*this; //打印指针内容

        由于运算符<<被重载,编译器将不会报错。这也是我们不提倡重载的原因。

        一些⼈人说printf格式繁琐不易阅读,但流也好不到哪⾥里。

        看看下面的两段代码,处理同一类型的数据。哪一个更简单呢?

cerr<<”Error connecting to ‘”<<foo->bar()->hostname.first
    <<”:”<<foo->bar->hostname.second<<”:”<<strerror(errno);
Fprintf(stderr,”Error connecting to ‘%s:%u:%s”,
    foo->bar()->hostname.first,foo->bar()->hostname.xxx);

        等等你可能发现的其他问题。(你也许会争论,正确地封装更好。但如果它对一个模式正确,对另一个呢?再者,我们的目标是尽量简化一种语⾔言,而不是增加更多需要学习的机制。)

        关于两者优缺点的争论还在继续,也找不到一种更合理的解决方案。尽管我们简单地规定了选择它们中的一种,但还是使用printf+read/write的居多。

 

前置自增和前置自减(Preincrement and Predecrement)

        在操作迭代器和模板对象的时候使用前置自增和前置自减。

定义:

        当一个变量自增(++ii--)(自减(--ii--)且不需要使用其值时,必须考虑是前置还是后置++/--

  • 利:不考虑返回值时,前置性能总是优于后置。这是因为后置需要复制i的值。如果i是一个迭代器或者非标量类型,复制i将是很大的开销。既然两者的作用相同,为何不选择前置运算呢。
  • 弊:在C传统开发中,常使用后置方式,尤其在for循环中。有些⼈人发现后置运算更易读,与英语语法一样,后置运算的对象(i)在动词(++)之前。

 

const修饰符的使用(Use of const)

        当需要的时候尽量使用const修饰。

定义:

        使用const来限制变量或者参数不被修改(比如const int foo)。类方法也可用const修饰以说明其不会改变类的状态。(比如class Foo{(int Bar(char c) const;))

  • 利:有利于程序员理解变量的使用方式。编译器能进行更严格的类型检查,而且常常能产⽣生更高效的代码。说服程序员相信其程序的正确性,毕竟他们知道其调用的函数对这些变量的修改是有限制的。这可以帮助程序员知道在多线程编程中哪些函数不需要锁也能正确地运行。
  • 弊:const是传递的,如果你给函数传递const变量,那么这个函数在声明时必须加上const声明(否则变量需要const_cast)。在使用库函数时,这将是一个问题。

结论:

        const变量、数据成员、方法和参数使编译器进行编译时类型检查,因此能尽早发现程序错误。以下情况强烈建议使用const修饰:

函数不修改通过引用或者指针传递的参数值,应该使用const修饰;

类方法应该尽量定义成const。访问器(Accessor)应该永远是const的。其他不修改类数据成员、不调用非const方法不返回非const引用或指针的函数也应该是const的;

把初始化后不需要修改的数据成员定义成const。但不要滥用const。像const int* const * const x就是滥用,即使你能准确地解释清楚const x是什么意思。注意什么是真正有用的,这种情况下,const int **x就足够了。当进行多线程编程时,mutable关键字可能不安全,所以在使用时应该⾸首先考虑线程安全。

        应该将const放在什么位置

        某些⼈人更喜欢将const放在类型后(int const *foo而不是const int* foo)。他们认为这样更易读和一致,const修饰的对象总是紧跟在它后面。然而,这种一致性在这⾥里却不适用,因为不要太过分宣言排除大多数你认为的一致性用法。将const放在前面更具可读性,因为它与英语语法一样,把形容词(const)放在名词(int)前面。这并不是说我们鼓励你将const放在最前面你就一定要这么做,注意与你的代码保持一致性。

 

整型类型(Integer Types)

        所有C++整型中,唯一可能用到就是int。当需要其他长度的整型时,使用stdint.h头文件中的精确整型,比如int16_t

定义:

        C++未明确定义整型的长度。人们通常的假设是:short:16位,int:32位,long:32位,long long:64位

  • 利:一致的声明;
  • 弊:整型的长度因编译器和计算机结构不同而不同。

结论:

        在stdint.h头文件中定义了int16_t, uint32_t,int64_t等整型,用到整型时,应该⾸首选这些精确的类型。对于C的整型,只可以使用int。适当的时候,你还应该使用其他标准类型,比如size_tptrdiff_t

        对于已知不会太大的整数,可以使用int,比如循环变量。但应该考虑int至少是32位的,但不要认为int会超过32位。当需要64位整型时,使用int64_t或者uint64_t。如果已知整数会比较大,使用int64_t

        不要使用无符号的uint32_t,除非你确切地知道要存储是一个位组而不是一个数字,或者你定义⼆二进制补码溢出。特别地,不要用无符号类型来说明一个数是非负的,用断⾔言来说明。

        关于无符号整型

        一些⼈人,包括教科书的作者在内,推荐使用无符号类型来代表非负数值。这被认为是一种自文档(即代码本⾝身就能说明其含义而不需要文档(比如注释)来说明:unsigned int book_amount;就是一种自文档)。然而,C中,这种自文档的优点却无法掩盖其带来的程序缺陷:考虑下面的代码:

for(unsigned int i = foo.Length()-1; i >= 0; --i) ...

        这是一个死循环!GCC可能会提醒这个缺陷并给出警告,但大多数情况下会被忽略。相似的严重缺陷在进行有符号和无符号变量比较时也会出现。根本原因是C语⾔言的类型提升机制使无符号类型的行为变得无法预期。所以,使用断⾔言来说明一个变量的非负性。不要使用无符号类型。

 

64位兼容性(64-bit Portability)

        程序代码应该保持32位和64位的兼容性。考虑打印(Printing)、比较(Comparisons)和数据对齐(Structure alignment)

对于某些类型,指⽰示符printf()不能很好在3264位系统间兼容。C99定义了一些格式兼容的指⽰示符。但MCVC7.1对某些不支持,所以某些情况下,只有自己基于inttypes.h的风格来自己定义自己比较丑陋的版本了。

// size_t的输出宏,基于inttypes.h的⻛风格
#ifdef _LP64

#编码风格# #Google C++# 其他C++特性(OtherC++Fetures)

注意PRI*宏作为独立字符串的扩展由编译器进行连接。因为,在使用非常量字符串时,需要将宏体插入格式说明中而不是命名它。将长度指⽰示符加在%后也是允许的。比如printf(“x = %30”PRIuS”\n”,x),在32Linux上展开时为printf(“x = %32”u””\n”,x),编译器识 .为printf(“ = %30u\n”x)

注意,sizeof(void *) != sizeof(int)。如果需要指针 .大小的整数,使用intptr_t

注意数据尤其是保存在磁盘上的数据的对齐问题。任何64位系统的类或结构体,如果有int64_tuint64_t类型的数据成员,默认情况下,会以8字节结尾来保持数据对齐。在3264位兼容代码中,如果有这种结构,应该确保它们在两种体系下被一致地封装。大多数编译器提供了数据对齐转换功能。比如gcc,使用命令__attribute__((packed))MSVC则提供了#pragma pack()__declspec(align())

声明64位常量时使用LLULL后缀:

int64_t my_value = 0x123456789LL;

如果确实需要在3264位系统上开发不同的代码,可以#ifdef _LP64命令来在两种环境中切换。但尽量避免这样做并保持修改局部化。

 

预处理宏(Preprocessor Macros)

        宏的使用应倍加小⼼心。尽量用内联函数、枚举和常量来代替宏。

        宏意味着编译器对代码的理解和你很不同。这可能导致不可预期的行为,尤其因为宏是全局作用的。

        还好,C++对宏的需求不像C那样必要。可以使用内联函数来代替由宏定义的性能要求高的代码;使用const常量来代替由宏定义的常量;使用引用来代替由宏定义的长变量名的缩写;除包含保护外,忽略所有由宏定义的条件编译指令,这些指令使代码难以测试。

        当然,宏具有其他技术做不到的特殊应用。这些在底层的库定义和代码库中较常见。而且,它们具有一些语⾔言本⾝身都无法完全实现的特征(比如分词(stringifying)、连接(Concatenation))。但使用宏前,考虑一下有没有不使用宏也能达到目的的方法。

        下面的用法可以避免大量使用宏引起的问题,如果使用宏,请严格遵守它们:

  1. 不要在头文件中定义宏;
  2. 使用宏前定义(#define)它们,使用后立即解定义(#undef)
  3. ● 不要解定义一个宏后又接着使用它定义另一个宏,选择一个不同的唯一的宏名称;
  4. 不要使用导致不平衡C++构造的宏,如果使用,应该详细这种行为的正确性;
  5. 不要使用#作为函数、类或变量名称的开始

 

0和空(0 and NULL)

        整型用0、实数用0.0、指针用NULL、字符用’\0’。对于整型和实数这是毫无争议的。

        但是对于指针(地址值),有两个选择:0NULLBjarne Stroustrup建议使用无修饰的0,但我们建议使用NULL,它看上去更像一个指针。事实上,一些C++编译器,比如gcc4.1.0提供了特殊的NULL定义以使其能够检测出错误并给出有用的警告,此时,sizeof(NULL)sizeof(0)是不等的。

        用’\0’代表空字符则使代码更易读。

 

存储容量运算符(sizeof)

        尽量使用sizeof(varname)而不是sizeof(Type)来确定一个变量或类型的大小。之所以使用sizeof(varname)是因为一旦变量的类型改变,仍可正确计算其存储容量。

        sizeof(type)只在某些情况有用,应该避免它的使用,在变量发⽣生变化时,它可能不再与变量同步。

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

增强库(Boost)

        从增强库中选择那些被广泛认可的库来使用。

定义:

        Boost是一个流行的,同行评论的、免费开源的C++库系列。

  • 利:Boost代码非常高效,兼容性强并且填补了C++标准库的许多空白。比如类型萃取、改进的函数绑定和改进的智能指针。同时,它还提供了标准库的扩展库-TR1的实现。
  • 弊:一些Boost库可能鼓励可读性差的编程实践,比如元编程和其他高级模板技术以及过度功能化的编程风格。

结论:

        为保持高度的可读性,只有被认可的部分Boost特性允许使用。目前,仅限以下库:

boost/call_straits.hpp中的Call Traits

boost/compressed_pair.hpp中的CompressedPair

C++03标准中未定义的容器持久化和封装工具(ptr_circlar_buffer.hpp和ptr_unordered*)外的boost/prt_container中的Pointer Container

boost/array.hpp中的Array

boost/graph中除持久化(adj_list_serialize.hpp)和并发、分布式算法和数据结构(boost/graph/parallel/*boost/graph/distributed/*)

boost/property_map中除并发、分布式(boost/property_map/parallel/*)的属性映射;

迭代器部分用于处理迭代器定义的部分:boost/iterator/iterator_adaptor.hpp,boost/iterator/iterator_facade.hpp,boost/function_output_iterator.hpp

        我们经常考虑增加一些Boost特性,所以这些规则可能会改变。

 

C++ 0x

        使用已认可C++0x库和语⾔言扩展,目前还没有被认可的。

定义:

        C++0x是下一个ISOC++标准,目前正处于最终委员会草案阶段。包括对C++语⾔言和库的重要变化。

  • 利:我们希望C++0x成为下一个C++标准并最终被大多数编译器支持。它将一些目前广泛使用的C++扩展库(第三方)纳入标准,为某些操作提供了简化方法并在安全方面作了改进。
  • 弊:C++0x标准比它之前的标准都要复杂(1,300⻚页,之前的只有800⻚页),并且,很多开发者对它并不熟悉。它的一些特性对代码可读性和可维护性的长期影响还不得而知。我们并不能预测什么时候它的众多特性才能被众多工具统一地实现,但这将很有趣(gcc,icc,clang,Eclipse等)。

        与Boost一样,一些C++0x扩展鼓励降低代码可读性的编程实践-比如移除检测到的冗余(比如类型名),而这些冗余对读者很有帮助,或者鼓励模板元编程。另外一些扩展提供与现在机制复杂的功能,这有可能导致混乱并带来转换开销。

结论:

        与Boost一样,使用那些被认可的语⾔言扩展和库。虽然目前还没有被认可的库,但随着标准的确立,新特征会被逐渐认识的。