通常每一个.cpp文件都有一个与之对应的.h文件。不过也有一些例外。比如单元测试代码和只包含main()函数的.cpp文件。
正确使用头文件可令代码在可读性、文件大小和性能上大为改观。下面的规则将引导你规避使用头文件时的各种陷阱。
头文件应该能够自给自足(self-contained,也就是可以作为第一个头文件被引入)以.h结尾。至于用来插入文本的文件,说到底它们并不是头文件,所以应以.inc结尾。不允许分离出-inl.h头文件的做法
不过有一个例外,即一个文件并不是self-contained的,而是作为文本插入到代码某处。或者,文件内容实际上是其它头文件的特定平台(platform-specific)扩展部分。这些文件就要用.inc文件扩展名。
如果.h文件声明了一个模板或内联函数,同时也在该文件加以定义。凡是有用到这些的.cc文件,就得统统包含该头文件,否则程序可能会在构建中链接失败。不要把这些定义放到分离的-inl.h文件里。
有个例外:如果某函数模板为所有相关模板参数显式实例化,或本身就是某类的一个私有成员,那么它就只能定义在实例化该模板的.cc文件里。
所有头文件都应该使用#define来防止头文件被多重包含, 命名格式当是:
<PROJECT>_<PATH>_<FILE>_H_
为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径.例如,项目foo中的头文件foo/src/bar/baz.h可按如下方式保护:
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_
尽可能地避免使用前置声明。使用#include包含需要的头文件即可
定义
所谓「前置声明」是类、函数和模板的纯粹声明,没伴随着其定义.
优点
缺点
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_
// b.h:
struct B {}; struct D : B {}
// good_user.cc:
#include "b.h" void f(B*); void f(void*);
void test(D* x) { f(x); } // calls f(B*)
如果#include被B和D的前置声明替代,test()就会调用f(void*)。
结论
至于什么时候包含头文件,参见 1.5 #include 的路径及顺序
只有当函数只有10行甚至更少时才将其定义为内联函数
定义
当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用。
优点
只要内联的函数体较小, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联。
缺点
滥用内联将导致程序变得更慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。
结论
有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数(注: 递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数
使用标准的头文件包含顺序可增强可读性,避免隐藏依赖:相关头文件,C库,C++库, 其他库的.h,本项目内的.h
项目内头文件应按照项目源代码目录树结构排列, 避免使用 UNIX 特殊的快捷目录"."(当前目录)或“…”(上级目录)。例如google-awesome-project/src/base/logging.h应该按如下方式包含:
#include "base/logging.h"
又如dir/foo.cc或者dir/foo_test.cc的主要作用是实现或测试dir2/foo2.h的供能,foo.cc中包含头文件的次序如下:
这种优先的顺序排序保证当dir2/foo2.h遗漏某些必要的库时,dir/foo.cc或dir/foo_test.cc的构建会立刻中止。因此这一条规则保证维护这些文件的人们首先看到构建中止的消息而不是维护其他包的人们
dir/foo.cc和dir2/foo2.h通常在同一目录下(如base/basictypes_unittest.cc和base/basictypes.h),但也可以放在不同目录下。
按字母顺序分别对每种类型的头文件进行二次排序是不错的主意。注意较老的代码可不符合这条规则,要在方便的时候改正它们。
&emsp 您所依赖的符号 (symbols) 被哪些头文件所定义,您就应该包含(include)哪些头文件,前置声明 (forward declarations) 情况除外。比如您要用到bar.h中的某个符号, 哪怕您所包含的foo.h已经包含了bar.h,也照样得包含bar.h,除非foo.h有明确说明它会自动向您提供bar.h中得symbol。不过凡是cc文件所对应得「相关头文件」已经包含的,就不用再重复包含进其cc文件里面了,就像foo.cc只包含foo.h就够了。不用再管后者所包含得其他内容。举例来说google-awesome-project/src/foo/internal/fooserver.cc得包含次序如下:
#include "foo/public/fooserver.h" // 优先位置
#include
#include
#include
#include
#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"
例外:
有时,平台特定(system-specific)代码需要条件编译(conditional includes),这些代码可以放到其它 includes 之后。当然,您的平台特定代码也要够简练且独立,比如:
#include "foo/public/fooserver.h"
#include "base/port.h" // For LANG_CXX11. #ifdef LANG_CXX11
#include #endif // LANG_CXX11
结论:
鼓励在.cc文件内使用匿名命名空间或static声明使用具名的命名空间时, 其名称可基于项目名或相对路径禁止使用using指示。禁止使用内联命名空间
定义:
命名空间将全局作用域细分为独立的, 具名的作用域, 可有效防止全局作用域的命名冲突.
优点:
虽然类已经提供了(可嵌套的)命名轴线(注:将命名分割在不同类的作用域内),命名空间在这基础上又封装了一层。
举例来说,两个不同项目的全局作用域都有一个类Foo,这样在编译或运行时造成冲突,如果每个项目将代码置于不同命名空间中,project1::Foo和project2::Foo作为不同符号自然不会冲突。
内联命名空间会自动把内部的标识符放到外层作用域,比如:
namespace X {
inline namespace Y {
void foo();
} // namespace Y
} // namespace X
X::Y::foo()与X::foo()彼此可代替。内联命名空间主要用来保持跨版本的 ABI 兼容性。
缺点:
命名空间具有迷惑性, 因为它们使得区分两个相同命名所指代的定义更加困难。
内联命名空间很容易令人迷惑,毕竟其内部的成员不再受其声明所在命名空间的限制。内联命名空间只在大型版本控制里有用
有时候不得不多次引用某个定义在许多嵌套命名空间里的实体,使用完整的命名空间会导致代码的冗长
在头文件中使用匿名空间导致违背 C++ 的唯一定义原则(One Definition Rule(ODR))。
结论:
根据下文将要提到的策略合理使用命名空间
// .h 文 件
namespace mynamespace {
// 所有声明都置于命名空间中
// 注意不要使用缩进
class MyClass {
public:
...
void Foo();
};
} // namespace mynamespace
// .cc 文 件
namespace mynamespace {
// 函数定义都置于命名空间中
void MyClass::Foo() {
...
}
} // namespace mynamespace
更复杂的.cc文件包含更多, 更复杂的细节, 比如gflags或using声明。
#include "a.h"
DEFINE_FLAG(bool, someflag, false, "dummy flag");
namespace a {
...code for a...
} // namespace a
using namespace foo;
// 在 .cc 中使用别名缩短常用的命名空间
namespace baz = ::foo::bar::baz;
// 在 .h 中使用别名缩短常用的命名空间
namespace librarian {
namespace impl { // 仅限内部使用
namespace sidetable = ::pipeline_diagnostics::sidetable;
} // namespace impl
inline void my_inline_function() {
// 限制在一个函数中的命名空间别名
namespace baz = ::foo::bar::baz;
...
}
} // namespace librarian
在.cc文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为static。但是不要在.h文件中这么做。
定义:
所有置于匿名命名空间的声明都具有内部链接性,函数和变量可以经由声明为static拥有内部链接性,这意味着你在这个文件中声明的这些标识符都不能在另一个文件中被访问。即使两个文件声明了完全一样名字的标识符,它们所指向的实体实际上是完全不同的。
结论:
推荐、鼓励在.cc中对于不需要在其他地方引用的标识符使用内部链接性声明,但是不要在.h中使用。
匿名命名空间的声明和具名的格式相同,在最后注释上namespace:
namespace {
...
} // namespace
使用静态成员函数或命名空间内的非成员函数, 尽量不要用裸的全局函数. 将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关
优点:
某些情况下, 非成员函数和静态成员函数是非常有用的, 将非成员函数放在命名空间内可避免污染全局作用域
缺点:
将非成员函数和静态成员函数作为新类的成员或许更有意义, 当它们需要访问外部资源或具有重要的依赖关系时更是如此
结论:
有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个命名空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用 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
定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的命名空间内
如果你必须定义非成员函数, 又只是在.cc文件中使用它, 可使用匿名2.1命名空间或static链接关键字(如static int Foo(){…})限定其作用域。
将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化
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;
有一个例外, 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低。
//低效的实现
for(int i = 0; i < 1000000; i++)
{
Foo f; //构造函数和析构函数分别调用 1000000 次!
f.DoSomething(i);
}
在循环作用域外面声明这类变量要高效的多:
//构造函数和析构函数只调用1次
Foo f;
for(int i = 0; i < 1000000; i++)
{
f.DoSomething(i);
}
禁止定义静态储存周期非POD变量,禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致代码的不可移植
禁止使用类的 静态储存周期 变量:由于构造和析构函数调用顺序的不确定性,它们会导致难以发现的 bug 。不过constexpr变量除外,毕竟它们又不涉及动态初始化或析构。
静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型(POD:Planin Old Data):即int,char和float以及POD类型的指针、数组和结构体。
静态变量的构造函数、析构函数和初始化的顺序在 C++ 中是只有部分明确的,甚至随着构建变化而变化,导致难以发现的 bug. 所以除了禁用类类型的全局变量,我们也不允许用函数返回值来初始化 POD 变量,除非该函数(比如getenv()或者getpid())不涉及任何全局变量。函数作用域里的静态变量除外,毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。
需要注意的是:同一个编译单元内是明确的,静态初始化优先于动态初始化,初始化顺序按照声明顺序进行,销毁则逆序。不同的编译单元之间初始化和销毁顺序属于未明确行为 (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 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。
说明:上文提及的静态变量泛指静态生存周期的对象, 包括: 全局变量, 静态变量, 静态类成员变量,以及函数静态变量
结论:
类是 C++ 中代码的基本单元. 显然, 它们被广泛使用. 下面列举了在写一个类时的主要注意事项。
不要在构造函数中调用虚函数, 也不要在无法报出错误时进行可能失败的初始化
定义
在构造函数中可以进行各种初始化操作
优点
缺点
结论
不要定义隐式类型转换. 对于转换运算符和单参数构造函数, 请使用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 }); // 错误的
这一代码从技术上说并非隐式类型转换, 但是语言标准认为这是explicit应当限制的行为。
优点
缺点
结论
如果你的类型需要, 就让它们支持拷贝 / 移动. 否则, 就把隐式产生的拷贝和移动函数禁用
定义
可拷贝类型允许对象在初始化时得到来自相同类型的另一对象的值, 或在赋值时被赋予相同类型的另一对象的值, 同时不改变源对象的值. 对于用户定义的类型, 拷贝操作一般通过拷贝构造函数与拷贝赋值操作符定义。string类型就是一个可拷贝类型的例子。
可移动类型允许对象在初始化时得到来自相同类型的临时对象的值, 或在赋值时被赋予相同类型的临时对象的值 (因此所有可拷贝对象也是可移动的)。std::unique_ptr就是一个可移动但不可复制的对象的例子。对于用户定义的类型, 移动操作一般是通过移动构造函数和移动赋值操作符实现的。
拷贝 / 移动构造函数在某些情况下会被编译器隐式调用. 例如, 通过传值的方式传递对象
优点
缺点
结论
class Foo {
public:
Foo(Foo&& other) : field_(other.field) {}
// 差, 只定义了移动构造函数, 而没有定义对应的赋值运算符.
private: Field field_;
// MyClass is neither copyable nor movable. MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;
};
仅当只有数据成员时使用struct,其他一概使用class
说明
在C++中struct与class关键字几乎含义一样. 我们为这两个关键字添加我们自己的语义理解, 以便为定义的数据类型选择合适的关键字。
struct用来定义包含数据的被动式对象, 也可以包含相关的常量, 但除了存取数据成员之外, 没有别的函数功能. 并且存取功能是通过直接访问位域, 而非函数调用。除了构造函数, 析构函数,Initialize(),Reset(),Validate()等类似的用于设定数据成员的函数外, 不能提供其它功能的函数。如果需要更多的函数功能,class更适合. 如果拿不准, 就用class。
为了和 STL 保持一致, 对于仿函数等特性可以不用class,而用struct。
注意: 类和结构体的成员变量使用不同的 命名规则。
使用组合 (这一点也是 GoF 在 <<Design Patterns>> 里反复强调的) 常常比使用继承更合理. 如果使用继承的话, 定义为public继承
定义
当子类继承基类时, 子类包含了父基类所有数据及操作的定义. C++ 实践中, 继承主要用于两种场合: 实现继承, 子类继承父类的实现代码; 接口继承, 子类仅继承父类的方法名称
优点
实现继承通过原封不动的复用基类代码减少了代码量. 由于继承是在编译时声明, 程序员和编译器都可以理解相应操作并发现错误。从编程角度而言, 接口继承是用来强制类输出特定的
API. 在类没有实现 API 中某个必须的方法时, 编译器同样会发现并报告错误
缺点
对于实现继承, 由于子类的实现代码散布在父类和子类间之间, 要理解其实现变得更加困难. 子类不能重写父类的非虚函数, 当然也就不能修改其实现. 基类也可能定义了一些数据成员, 因此还必须区分基类的实际布局
结论
真正需要用到多重实现继承的情况少之又少。只在以下情况我们才允许多重继承:最多只有一个基类是非抽象类;其它基类都是以Interface为后缀的纯接口类
定义
多重继承允许子类拥有多个基类. 要将作为 纯接口 的基类和具有 实现 的基类区别开来。
优点
相比单继承 (见 继承), 多重实现继承可以复用更多的代码。
缺点
真正需要用到多重 实现 继承的情况少之又少. 有时多重实现继承看上去是不错的解决方案, 但这时你通常也可以找到一个更明确, 更清晰的不同解决方案
结论
只有当所有父类除第一个外都是 纯接口类 时, 才允许使用多重继承. 为确保它们是纯接口, 这些类必须以Interface为后缀
接口是指满足特定条件的类, 这些类以Interface为后缀(不强制)
定义
当一个类满足以下要求时, 称之为纯接口:
优点
Interface后缀增加了类名长度, 为阅读和理解带来不便. 同时, 接口属性作为实现细节不应暴露给用户。
结论
只有在满足上述条件时, 类才以Interface结尾, 但反过来, 满足上述需要的类未必一定以Interface结尾。
除少数特定环境外,不要重载运算符。也不要创建用户定义字面量
定义
C++ 允许用户通过使用operator关键字 对内建运算符进行重载定义, 只要其中一个参数是用户定义的类型。operator关键字还允许用户使用operator""定义新的字面运算符, 并且定义类型转换函数, 例如operator bool()。
优点
结论
将所有数据成员声明为private,除非石static const类型成员(遵循常量命名规则)。处于技术上的原因,在使用Google Test 时我们允许测试固件类中的数据成员为protected
将相似的声明放在一起,将public部分放在最前
说明
函数的参数顺序为:输入参数在先,后跟输出参数
说明
C/C++ 中的函数参数或者是函数的输入, 或者是函数的输出, 或兼而有之。 输入参数通常是值参或const引用,输出参数或输入/输出参数则一般为非const指针。在排列参数顺序时, 将所有的输入参数置于输出参数之前. 特别要注意, 在加入新参数时不要因为它们是新参数就置于参数列表最后, 而是仍然要按照前述的规则, 即将新的输入参数也置于输出参数之前。
这并非一个硬性规定。 输入/输出参数 (通常是类或结构体) 让这个问题变得复杂。 并且, 有时候为了其他函数保持一致, 你可能不得不有所变通。
我们倾向于编写简短,凝练的函数
说明
我们承认长函数有时是合理的, 因此并不硬性限制函数的长度。如果函数超过40行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割。
即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题, 甚至导致难以发现的 bug。 使函数尽量简短, 以便于他人阅读和修改代码。
在处理代码时, 你可能会发现复杂的长函数. 不要害怕修改现有代码:如果证实这些代码使用 / 调试起来很困难, 或者你只需要使用其中的一小段代码, 考虑将其分割为更加简短并易于管理的若干函数。
所有按引用传递的参数必须加上const
定义
在 C 语言中, 如果函数需要修改变量的值, 参数必须为指针, 如int foo(int * pval)。在 C++ 中,函数还可以声明为引用参数int foo(int &pval)。
优点
定义引用参数可以防止出现(*pval)++这样丑陋的代码。引用参数对于拷贝构造函数这样的应用也是必需的。 同时也更明确地不接受空指针。
缺点
容易引起误解, 因为引用在语法上是值变量却拥有指针的语义。
结论
void Foo(const string &in, string *out);
事实上这在 Google Code 是一个硬性约定: 输入参数是值参或const引用,输出参数为指针。输入参数可以是const指针,但决不能是非const的引用参数, 除非特殊要求, 比如swap()。
总而言之, 大多时候输入形参往往是const T&,若用const T则说明输入另有处理。所以若要使用const T,则应给出相应的理由, 否则会使得读者感到迷惑。
若要使用函数重载,则必须能让读者一看调用点就胸有成竹,而不用花心思猜测调用的重载函数到底是哪一种。这一规则也适用于构造函数
定义
你可以编写一个参数类型为const string&的函数,然后用另一个参数类型为const char*的函数对其进行重载:
class MyClass {
public:
void Analyze(const string &text);
void Analyze(const char *text, size_t textlen);
};
优点
通过重载参数不同的同名函数, 可以令代码更加直观. 模板化代码需要重载, 这同时也能为使用者带来便利。
缺点
如果函数单靠不同的参数类型而重载 (注:这意味着参数数量不变), 读者就得十分熟悉 C++ 五花八门的匹配规则, 以了解匹配过程具体到底如何。另外, 如果派生类只重载了某个函数的部分变体, 继承语义就容易令人困惑。
结论
如果打算重载一个函数, 可以试试改在函数名里加上参数信息. 例如, 用AppendString()和AppendInt()等,而不是一口气重载多个Append()。如果重载函数的目的是为了支持不同数量的同一类型参数, 则优先考虑使用std::vector以便使用者可以用 列表初始化 指定参数。
只允许在非虚函数中使用缺省参数, 且必须保证缺省参数的值始终一致. 缺省参数与 函数重载遵循同样的规则. 一般情况下建议使用函数重载, 尤其是在缺省函数带来的可读性提升不能弥补下文中所提到的缺点的情况下
优点
有些函数一般情况下使用默认参数, 但有时需要又使用非默认的参数. 缺省参数为这样的情形提供了便利, 使程序员不需要为了极少的例外情况编写大量的函数. 和函数重载相比, 缺省参数的语法更简洁明了, 减少了大量的样板代码, 也更好地区别了 “必要参数” 和 “可选参数”。
缺点
结论
只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法
定义
C++ 现在允许两种不同的函数声明方式.。以往的写法是将返回类型置于函数名之前。例如:
int foo(int x);
C++11 引入了这一新的形式. 现在可以在函数名前使用auto关键字,在参数列表之后后置返回类型. 例如:
auto foo(int x) -> int;
后置返回类型为函数作用域。对于像int这样简单的类型, 两种写法没有区别. 但对于复杂的情况, 例如类域中的类型声明或者以函数参数的形式书写的类型, 写法的不同会造成区别。
优点
template <class T, class U> auto add(T t, U u) -> decltype(t + u);
对比下面的例子:
template <class T, class U> decltype(declval<T&>() + declval<U&>()) add(T t, U u);
缺点
结论
&ems;在大部分情况下, 应当继续使用以往的函数声明写法, 即将返回类型置于函数名前。只有在必需的时候 (如 Lambda 表达式) 或者使用后置语法能够简化书写并且提高易读性的时候才使用新的返回类型后置语法。但是后一种情况一般来说是很少见的, 大部分时候都出现在相当复杂的模板代码中,而多数情况下不鼓励写这样复杂的模板代码。
Google 用了很多自己实现的技巧 / 工具使 C++ 代码更加健壮, 我们使用 C++ 的方式可能和你在其它地方见到的有所不同。