Google开源项目风格指南namespacef//namespace2.3.32.3.非成员函数、静态成员函数和全局函数Tip:使用静态成员函数或命名空间内的非成员函数,尽量不要用裸的全局函数。将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关优点:某些情况下,非成员函数和静态成员函数是非常有用的,将非成员函数放在命名空间内可避免污染全局作用域缺点:将非成员函数和静态成员函数作为新类的成员或许更有意义,当它们需要访问外部资源或具有重要的依赖关系时更是如此结论:有时,把函数的定义同类的实例脱钩是有益的,甚至是必要的,这样的函数可以被定义成静态成员,或是非成员函数。非成员函数不应依赖于外部变量,应尽量置于某个命名空间内。相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类,不如使用2.1:命名空间。举例而言,对于头文件myproject/foo_bar.h,应当使用namespace myprojectnamespace foo_bar fvoid Function1();void Function2();// namespacefoo_bar?// namespace myproject而非namespacemyproject{class FooBar (public:staticvoidFunctioni(;static void Function2();;// namespacemyproject定义在同一编译单元的函数,被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖;静态成员函数对此尤其敏感。可以考虑提取到新类中,或者将函数置于独立库的命名空间132.3.2.作用域
Google 开源项目风格指南 namespace { . } // namespace 2.3.3 2.3. 非成员函数、静态成员函数和全局函数 Tip: 使用静态成员函数或命名空间内的非成员函数, 尽量不要用裸的全局函数. 将一系列函数直接置于 命名空间中,不要用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧 密相关. 优点: 某些情况下, 非成员函数和静态成员函数是非常有用的, 将非成员函数放在命名空间内可避免 污染全局作用域. 缺点: 将非成员函数和静态成员函数作为新类的成员或许更有意义, 当它们需要访问外部资源或具 有重要的依赖关系时更是如此. 结论: 有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态 成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个命名空间内. 相比 单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用2.1. 命名空间 。 举例而言,对于头文件 myproject/foo_bar.h , 应当使用 namespace myproject { namespace foo_bar { void Function1(); void Function2(); } // namespace foo_bar } // namespace myproject 而非 namespace myproject { class FooBar { public: static void Function1(); static void Function2(); }; } // namespace myproject 定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依 赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的命名空间 2.3. 2. 作用域 13
Google开源项目风格指南内.如果你必须定义非成员函数,又只是在:Cc文件中使用它,可使用匿名2.1.命名空间或static链接关键字(如staticintFoo()..})限定其作用域2.3.42.4.局部变量Tip:将函数变量尽可能置于最小作用域内,并在变量声明时进行初始化C十十允许在函数的任何位置声明变量我们提倡在尽可能小的作用域中声明变量,离第一次使用越近越好,这使得代码浏览者更容易定位变量声明的位置,了解变量的类型和初始值。特别是,应使用初始化的方式替代声明再赋值,比如:int i;i=f();//坏——初始化和声明分离初始化时声明intj=gO;//好-vector<int> v;v.push_back(1);//用花括号初始化更好v.push_back(2);vector<int>v={1,2};//好—u一开始就初始化属于if,while和for语句的变量应当在这些语句中正常地声明,这样子这些变量的作用域就被限制在这些语句中了,举例而言:while(const char*p=strchr(str,/"))str=p+1;Warning:有一个例外,如果变量是一个对象,每次进入作用域都要调用其构造函数,每次退出作用域都要调用其析构函数.这会导致效率降低//低效的实现for(inti=0;i<1000000;++i){Foo f;//构造函数和析构函数分别调用1000000次!f.DoSomething(i);}在循环作用域外面声明这类变量要高效的多:Foo f;//构造函数和析构函数只调用1次for(inti=0;i<1000000;++i){(continues onnextpage)14Chapter2.C++风格指南-内容目录
Google 开源项目风格指南 内. 如果你必须定义非成员函数, 又只是在 .cc 文件中使用它, 可使用匿名2.1. 命名空间 或 static 链接关键字 (如 static int Foo() {.}) 限定其作用域. 2.3.4 2.4. 局部变量 Tip: 将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化. C++ 允许在函数的任何位置声明变量. 我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越 好. 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值. 特别是,应使用初始化的 方式替代声明再赋值, 比如: int i; i = f(); // 坏——初始化和声明分离 int j = g(); // 好——初始化时声明 vector<int> v; v.push_back(1); // 用花括号初始化更好 v.push_back(2); vector<int> v = {1, 2}; // 好——v 一开始就初始化 属于 if, while 和 for 语句的变量应当在这些语句中正常地声明,这样子这些变量的作用域就被限制在 这些语句中了,举例而言: while (const char* p = strchr(str, '/')) str = p + 1; Warning: 有一个例外, 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用 域都要调用其析构函数. 这会导致效率降低. // 低效的实现 for (int i = 0; i < 1000000; ++i) { Foo f; // 构造函数和析构函数分别调用 1000000 次! f.DoSomething(i); } 在循环作用域外面声明这类变量要高效的多: Foo f; // 构造函数和析构函数只调用 1 次 for (int i = 0; i < 1000000; ++i) { (continues on next page) 14 Chapter 2. C++ 风格指南 - 内容目录
Google开源项目风格指南(continuedfrom previous page)f.DoSomething(i);2.3.52.5.静态和全局变量Tip:禁止定义静态储存周期非POD变量,禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致代码的不可移植。禁止使用类的静态储存周期变量:由于构造和析构函数调用顺序的不确定性,它们会导致难以发现的bug。不过constexpr变量除外,毕竞它们又不涉及动态初始化或析构。静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型(POD:PlainOldData):即int,char和float,以及POD类型的指针、数组和结构体。静态变量的构造函数、析构函数和初始化的顺序在C++中是只有部分明确的,甚至随着构建变化而变化,导致难以发现的bug所以除了禁用类类型的全局变量,我们也不允许用函数返回值来初始化POD变量,除非该函数(比如getenv()或getpid))不涉及任何全局变量。函数作用域里的静态变量除外毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。Note:Xris译注:同一个编译单元内是明确的,静态初始化优先于动态初始化,初始化顺序按照声明顺序进行,销毁则逆序。不同的编译单元之间初始化和销毁顺序属于未明确行为(unspecifiedbehaviour)。同理,全局和静态变量在程序中断时会被析构,无论所谓中断是从main()返回还是对exit()的调用。析构顺序正好与构造函数调用的顺序相反。但既然构造顺序未定义,那么析构顺序当然也就不定了。比如,在程序结束时某静态变量已经被析构了,但代码还在跑一一比如其它线程一一并试图访问它且失败;再比如,一个静态string变量也许会在一个引用了前者的其它变量析构之前被析构掉。改善以上析构问题的办法之一是用quick_exit()来代替exit()并中断程序。它们的不同之处是前者不会执行任何析构,也不会执行atexitO所绑定的任何handlers.如果您想在执行guickexit()来中断时执行某handler(比如刷新log),您可以把它绑定到at_quickexit().如果您想在exit()和quick_exit()都用上该handler,都绑定上去。综上所述,我们只允许POD类型的静态变量,即完全禁用vector(使用C数组替代)和string(使用const char [)。如果您确实需要一个class类型的静态或全局变量,可以考虑在main()函数或pthread_once()内初始化一个指针且永不回收。注意只能用raw指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。Note:Yang.Y译注:152.3.2.作用域
Google 开源项目风格指南 (continued from previous page) f.DoSomething(i); } 2.3.5 2.5. 静态和全局变量 Tip: 禁止定义静态储存周期非 POD 变量,禁止使用含有副作用的函数初始化 POD 全局变量,因为 多编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致代码的不可移植。 禁止使用类的 静态储存周期 变量:由于构造和析构函数调用顺序的不确定性,它们会导致难以发现的 bug 。不过 constexpr 变量除外,毕竟它们又不涉及动态初始化或析构。 静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生 数据类型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 类型的指针、数组和结构体。 静态变量的构造函数、析构函数和初始化的顺序在 C++ 中是只有部分明确的,甚至随着构建变化而变 化,导致难以发现的 bug. 所以除了禁用类类型的全局变量,我们也不允许用函数返回值来初始化 POD 变量,除非该函数(比如 getenv() 或 getpid() )不涉及任何全局变量。函数作用域里的静态变量除外, 毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。 Note: Xris 译注: 同一个编译单元内是明确的,静态初始化优先于动态初始化,初始化顺序按照声明顺序进行,销毁则逆 序。不同的编译单元之间初始化和销毁顺序属于未明确行为 (unspecified behaviour)。 同理,全局和静态变量在程序中断时会被析构,无论所谓中断是从 main() 返回还是对 exit() 的调用。 析构顺序正好与构造函数调用的顺序相反。但既然构造顺序未定义,那么析构顺序当然也就不定了。比 如,在程序结束时某静态变量已经被析构了,但代码还在跑——比如其它线程——并试图访问它且失败; 再比如,一个静态 string 变量也许会在一个引用了前者的其它变量析构之前被析构掉。 改善以上析构问题的办法之一是用 quick_exit() 来代替 exit() 并中断程序。它们的不同之处是前者 不会执行任何析构,也不会执行 atexit() 所绑定的任何 handlers. 如果您想在执行 quick_exit() 来 中断时执行某 handler(比如刷新 log),您可以把它绑定到 _at_quick_exit(). 如果您想在 exit() 和 quick_exit() 都用上该 handler, 都绑定上去。 综上所述,我们只允许 POD 类型的静态变量,即完全禁用 vector (使用 C 数组替代) 和 string (使用 const char [])。 如果您确实需要一个 class 类型的静态或全局变量,可以考虑在 main() 函数或 pthread_once() 内初 始化一个指针且永不回收。注意只能用 raw 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出 的不定顺序问题。 Note: Yang.Y 译注: 2.3. 2. 作用域 15
Google开源项目风格指南上文提及的静态变量泛指静态生存周期的对象,包括:全局变量,静态变量,静态类成员变量,以及函数静态变量2.3.6译者(YuleFox)笔记1.cc中的匿名命名空间可避免命名冲突,限定作用域,避免直接使用using关键字污染命名空间2.嵌套类符合局部使用原则,只是不能在其他头文件中前置声明,尽量不要pub1ic;3.尽量不用全局函数和全局变量,考虑作用域和命名空间限制,尽量单独形成编译单元;4.多线程中的全局变量(含静态成员变量)不要使用class类型(含STL容器),避免不明确行为导致的bug5.作用域的使用,除了考虑名称污染,可读性之外,主要是为降低耦合,提高编译/执行效率2.3.7译者(acgtyrant)笔记1.注意「using指示(using-directive)」和「using声明(using-declaration)」的区别。2.匿名命名空间说白了就是文件作用域,就像Cstatic声明的作用域一样,后者已经被C++标准提倡弃用。3.局部变量在声明的同时进行显式值初始化,比起隐式初始化再赋值的两步过程要高效,同时也贯彻了计算机体系结构重要的概念「局部性(locality)」。4.注意别在循环犯大量构造和析构的低级错误。2.43.类类是C十十中代码的基本单元,显然,它们被广泛使用,本节列举了在写一个类时的主要注意事项2.4.13.1.构造函数的职责总述不要在构造函数中调用虚函数,也不要在无法报出错误时进行可能失败的初始化定义在构造函数中可以进行各种初始化操作优点·无需考虑类是否被初始化·经过构造函数完全初始化后的对象可以为const类型,也能更方便地被标准容器或算法使用缺点16Chapter2.C++风格指南-内容目录
Google 开源项目风格指南 上文提及的静态变量泛指静态生存周期的对象, 包括: 全局变量, 静态变量, 静态类成员变量, 以及函数静 态变量. 2.3.6 译者 (YuleFox) 笔记 1. cc 中的匿名命名空间可避免命名冲突, 限定作用域, 避免直接使用 using 关键字污染命名空间; 2. 嵌套类符合局部使用原则, 只是不能在其他头文件中前置声明, 尽量不要 public; 3. 尽量不用全局函数和全局变量, 考虑作用域和命名空间限制, 尽量单独形成编译单元; 4. 多线程中的全局变量 (含静态成员变量) 不要使用 class 类型 (含 STL 容器), 避免不明确行为导致 的 bug. 5. 作用域的使用, 除了考虑名称污染, 可读性之外, 主要是为降低耦合, 提高编译/执行效率. 2.3.7 译者(acgtyrant)笔记 1. 注意「using 指示(using-directive)」和「using 声明(using-declaration)」的区别。 2. 匿名命名空间说白了就是文件作用域,就像 C static 声明的作用域一样,后者已经被 C++ 标准提 倡弃用。 3. 局部变量在声明的同时进行显式值初始化,比起隐式初始化再赋值的两步过程要高效,同时也贯彻 了计算机体系结构重要的概念「局部性(locality)」。 4. 注意别在循环犯大量构造和析构的低级错误。 2.4 3. 类 类是 C++ 中代码的基本单元. 显然, 它们被广泛使用. 本节列举了在写一个类时的主要注意事项. 2.4.1 3.1. 构造函数的职责 总述 不要在构造函数中调用虚函数, 也不要在无法报出错误时进行可能失败的初始化. 定义 在构造函数中可以进行各种初始化操作. 优点 • 无需考虑类是否被初始化. • 经过构造函数完全初始化后的对象可以为 const 类型, 也能更方便地被标准容器或算法使用. 缺点 16 Chapter 2. C++ 风格指南 - 内容目录
Google开源项目风格指南·如果在构造函数内调用了自身的虚函数,这类调用是不会重定向到子类的虚函数实现。即使当前没有子类化实现,将来仍是隐患,:在没有使程序崩溃(因为并不是一个始终合适的方法)或者使用异常(因为已经被禁用了)等方法的条件下,构造函数很难上报错误·如果执行失败,会得到一个初始化失败的对象,这个对象有可能进人不正常的状态,必须使用boo1IsValid()或类似这样的机制才能检查出来,然而这是一个十分容易被疏忽的方法·构造函数的地址是无法被取得的,因此,举例来说,由构造函数完成的工作是无法以简单的方式交给其他线程的结论构造函数不允许调用虚函数,如果代码允许,直接终止程序是一个合适的处理错误的方式,否则,考虑用Init()方法或工厂函数.构造函数不得调用虚函数,或尝试报告一个非致命错误如果对象需要进行有意义的(non-trivial)初始化,考虑使用明确的Init()方法或使用工厂模式.AvoidInit()methodsonobjectswithnootherstatesthataffectwhichpublicmethodsmaybecalled(此类形式的半构造对象有时无法正确工作)2.4.23.2.隐式类型转换总述不要定义隐式类型转换.对于转换运算符和单参数构造函数,请使用explicit关键字定义隐式类型转换允许一个某种类型(称作源类型)的对象被用于需要另一种类型(称作目的类型)的位置例如,将一个int类型的参数传递给需要double类型的函数除了语言所定义的隐式类型转换,用户还可以通过在类定义中添加合适的成员定义自己需要的转换。在源类型中定义隐式类型转换,可以通过目的类型名的类型转换运算符实现(例如operatorbool):在目的类型中定义隐式类型转换,则通过以源类型作为其唯一参数(或唯一无默认值的参数)的构造函数实现explicit关键字可以用于构造函数或(在C++11引人)类型转换运算符,以保证只有当目的类型在调用点被显式写明时才能进行类型转换,例如使用cast.这不仅作用于隐式类型转换,还能作用于C++11的列表初始化语法:classFoo(explicit Foo(int X, double y);1;void Func(Foo f);此时下面的代码是不允许的:Func((42,3.14));//Error这一代码从技术上说并非隐式类型转换,但是语言标准认为这是explicit应当限制的行为172.4.3.类
Google 开源项目风格指南 • 如果在构造函数内调用了自身的虚函数, 这类调用是不会重定向到子类的虚函数实现. 即使当前没 有子类化实现, 将来仍是隐患. • 在没有使程序崩溃 (因为并不是一个始终合适的方法) 或者使用异常 (因为已经被禁用 了) 等方法 的条件下, 构造函数很难上报错误 • 如果执行失败, 会得到一个初始化失败的对象, 这个对象有可能进入不正常的状态, 必须使用 bool IsValid() 或类似这样的机制才能检查出来, 然而这是一个十分容易被疏忽的方法. • 构造函数的地址是无法被取得的, 因此, 举例来说, 由构造函数完成的工作是无法以简单的方式交给 其他线程的. 结论 构造函数不允许调用虚函数. 如果代码允许, 直接终止程序是一个合适的处理错误的方式. 否则, 考虑用 Init() 方法或工厂函数. 构造函数不得调用虚函数, 或尝试报告一个非致命错误. 如果对象需要进行有意义的 (non-trivial) 初始 化, 考虑使用明确的 Init() 方法或使用工厂模式. Avoid Init() methods on objects with no other states that affect which public methods may be called (此类形式的半构造对象有时无法正确工作). 2.4.2 3.2. 隐式类型转换 总述 不要定义隐式类型转换. 对于转换运算符和单参数构造函数, 请使用 explicit 关键字. 定义 隐式类型转换允许一个某种类型 (称作 源类型) 的对象被用于需要另一种类型 (称作 目的类型) 的位置, 例如, 将一个 int 类型的参数传递给需要 double 类型的函数. 除了语言所定义的隐式类型转换, 用户还可以通过在类定义中添加合适的成员定义自己需要的转换. 在源 类型中定义隐式类型转换, 可以通过目的类型名的类型转换运算符实现 (例如 operator bool()). 在目 的类型中定义隐式类型转换, 则通过以源类型作为其唯一参数 (或唯一无默认值的参数) 的构造函数实现. explicit 关键字可以用于构造函数或 (在 C++11 引入) 类型转换运算符, 以保证只有当目的类型在调用 点被显式写明时才能进行类型转换, 例如使用 cast. 这不仅作用于隐式类型转换, 还能作用于 C++11 的 列表初始化语法: class Foo { explicit Foo(int x, double y); . }; void Func(Foo f); 此时下面的代码是不允许的: Func({42, 3.14}); // Error 这一代码从技术上说并非隐式类型转换, 但是语言标准认为这是 explicit 应当限制的行为. 2.4. 3. 类 17