Elements of Modern C++ Style
译自:http://herbsutter.com/elements-of-modern-c-style
“C++11 feels like a new language.” — Bjarne Stroustrup
C++11
标准提供了很多新特性。本文特别的而且仅仅专注于那些使得C++11
相比于C++98
来说确实感觉像一门新语言的特性,因为:
- 它们改变了编写C++代码时所要使用的风格和惯用手法,通常还包括设计C++库的方式。例如,你将会看到更多的智能指针作为参数和返回值,函数也会通过值的方式返回大对象。
- 它们被如此普遍的使用,以至于你会在大多数的代码示例中看见它们。例如,几乎在每5行的现代C++代码示例中你就会看见
auto
的身影。
C++11
的特性也要好好的使用,但先使用好这些,因为它们是使得C++11
代码整洁、安全(和现代主流语言一样整洁和安全)和快速的最普遍的特性,再加上C++的传统的,如金属般的性能,使之一如既往的强大。
说明:
- 如同
Strunk & White
,本文特意专注于简洁的总结指导,而不是为了提供详尽的原理阐述和pro/con
分析;这将在其它的文章中探究。 - 这是一份保持更新的文档。你可以在最后随时间变化和添加的列表中看见。
auto
只要有可能就使用auto
。它是很有用,原因有两个。首先,很明显是它是一种便利,可以使我们避免重复一个类型的名称,即使我们已经阐明而且编译器已经知道了它的类型。
1 | // C++98 |
其次,当一个类型具有不可知或难以用语言表达的名称时,它就不仅仅是一个便利了。例如,多数的lambda函数你根本不能或不能轻易地拼写出它的类型名。
1 | // C++98 |
注意,使用auto
并没有改变代码的含义。代码仍然是静态类型的,而且每个表达式的类型都是清晰和明确的;语言再也不会迫使我们重申类型名。
一些人刚开始的时候害怕在这里使用auto
,因为如果不(重新)声明需要的类型的话,感觉好像我们有可能意外的得到一个不同的类型。如果你想要显示的强制类型转换
,声明目标类型也是可以的。然而,在绝大多数时间,你只使用auto
就可以了;很少会发生因为错误而得到一个不一样的类型的情况,而且即使在这种情况下,依靠语言的强大类型系统,编译器会让你知道:你在尝试调用一个变量的并不存在的成员方法,或者不是以正常的方式使用它。
Smart pointers: No delete
始终使用智能指针和non-owing原始指针。绝不使用owing原始指针和delete
,除非在在罕见的情况下,如你需要实现自己的底层数据结构(即使如此,也要在类边界里面保持良好的封装)。
如果你知道你是另外一个对象的唯一拥有者,就使用unique_ptr
来明确唯一所有权。”new T
“表达式应该立即初始化拥有它的另一个对象,通常使用unique_ptr
。一个典型的例子是Pimpl惯用手法
(见 GotW #100):
1 | // C++11 Pimpl idiom: header file |
使用shared_ptr
明确共享所有权。更喜欢使用make_shared
来有效的创建共享对象。
1 | // C++98 |
使用weak_ptr
打引用破循环和明确可选性(例:实现一个对象缓存)。
1 | .. C++11 |
如果你知道另一个对象比你的生存期长,而且你需要观察它,那么使用(non-owing)原始指针。
1 | // C++11 |
nullptr
始终使用nullptr
作为空指针的值,绝不使用模棱两可的字面量0
或者NULL
宏,因为它们可以是一个整形或指针。
1 | // C++98 |
Range for
使用基于范围的for
循环的方式来对一个范围内的元素进行顺序访问是非常方便的。
1 | // C++98 |
Nonmember begin and end
总是使用非成员的begin(x)
和end(x)
(而不是x.begin()
和x.end()
)。因为begin(x)
和end(x)
是可扩展的,可以和所有的容器类型(甚至是数组)一起使用,而且不仅仅是容器,还包括那些遵循STL
风格,实现了x.begin()
和x.end()
成员函数的类型。
如果你使用了非STL
的集合类型,它们提供了迭代,但不是STL
风格的x.begin()
和x.end()
,那么你可以编写你自己的非成员的begin(x)
和end(x)
来重载该类型。这样你就可以使用如上所示的STL
容器的相同编码风格来遍历该类型。C++11
的标准作出了榜样:C数组就是这种类型,C++11
标准给它提供了begin
和end
。
1 | vector<int> v; |
Lambda Functions and Algorithms
Lambda表达式是一个可以改变游戏规则的改变,它会经常会改变你写代码的方式,使之更优雅和更快速。Lambda表达式使现有的STL
算法的可用性提升了100倍。新的C++库的设计越来越多的假设Lambda表达式是可用的(例:PPL
),而且某些甚至要求你编写Lambda表达式来使用库(例:C++ AMP
)。
这是一个简单的例子:在v
中找到第一个大于x
并小于y
的元素。在C++11
中,最简单的和最整洁的代码是使用标准算法。
1 | // C++98: write a naked loop (using std::find_if is impractically difficult) |
你是否想要一个循环或类似的语言特性(但语言并没有提供)?并不需要付出多大的努力,只要把它写成一个模板函数(算法库),利用lambda表达式你总是可以如同使用语言特性一样便利的使用它。而且具有更大的灵活性,因为它真的只是一个库,而不是一个天生的语言特性。
1 | // C# |
要熟悉lambda表达式,你将会大量的使用它们,不仅仅是在C++中—它们已经在几种流行的主要语言中被支持和广泛使用。一个很好的起点是我在PDC 2010
上的演讲:Lambdas, Lambdas Everywhere。
Move / &&
移动是作为拷贝优化的一个最好的思想,虽然它也可以做其他的事情,如:完美转发。
移动语义使我们设计API
的方式发生了改变。我们将会设计出更多的返回值对象的接口。
1 | // C++98: alternatives to avoid copying |
当你要做一些比拷贝更有效率的事情的时候,为你的类型启用移动语义。
Uniform Initialization and Initializer Lists
没有改变的是:当初始化一个局部变量时,且它的类型是非POD
或auto
时,继续使用熟悉的 =
语法,而不用额外的{ }
。
1 | // C++98 or C++11 |
在其它情况下,特别是你以前在构造对象时经常使用( )
时,应该使用{ }
代替。使用大括号可以避免几个潜在的问题:你不会意外的获得变窄类型转换(例:flot
到int
);你不会偶尔不小心拥有未初始化的POD
成员变量或数组;你会避免C++98
给你带来的偶然惊喜:由于在C++语法中声明某凌两可(Scott Meyers 的一句名言:”C++’s most vexing
parse.”),你实际上声明了一个函数而不是一个变量,但还是能通过编译。在新的风格中没有这样的烦恼。
1 | // C++98 |
新的{ }
语法在每个地方都工作的很好:
1 | // C++98 |
最后,有些时候只是为了方便,在给函数传递参数时可以不需要临时的类型名。
1 | void draw_rect(rectangle); |
我不使用大括号的唯一地方是在对非POD
变量进行简单初始化的时候,如:auto x = begin(v);
,不然它会使代码将变得不必要的丑陋。因为我知道它是一个类类型,所以我知道我不需要担心偶然的变窄类型转换,而且现代的编译器已经例行的执行优化以删去额外的拷贝(或额外的移动,如果类型启用了移动语义)。
And More
现代C++还有更多特性。在将来,我打算写一些更深入的文章,关于C++11
的这些和其它的我们要去了解和喜欢的特性。
但是现在,这里是我们必须要知道的特性列表。这些特性形成的核心,定义了现代C++的风格,这使得C++代码看起来和执行起来确实如此;你会发现它们在你所将要看到或编写的每一段现代C++代码中会普遍的被使用;使得现代C++是一门整洁、安全和快速的语言,在未来的许多年里我们的行业将会继续严重依赖于它。