Elements of Modern C++ Style

目录
  1. auto
  2. Smart pointers: No delete
  3. nullptr
  4. Range for
  5. Nonmember begin and end
  6. Lambda Functions and Algorithms
  7. Move / &&
  8. Uniform Initialization and Initializer Lists
  9. And More

译自: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
2
3
4
5
6
7
8
9
// C++98
map<int, string>::iterator i = m.begin();
double const xlimit = config["xlimit"];
singleton& s = singleton::instance();

// C++11
auto i = begin(m);
auto const xlimit = config["xlimit"];
auto& s = singleton::instance();

其次,当一个类型具有不可知或难以用语言表达的名称时,它就不仅仅是一个便利了。例如,多数的lambda函数你根本不能或不能轻易地拼写出它的类型名。

1
2
3
4
5
// C++98
binder2nd<greater> x = bind2nd(greater(), 42);

// C++11
auto x = [](int i) { return i > 42; };

注意,使用auto并没有改变代码的含义。代码仍然是静态类型的,而且每个表达式的类型都是清晰和明确的;语言再也不会迫使我们重申类型名。

一些人刚开始的时候害怕在这里使用auto,因为如果不(重新)声明需要的类型的话,感觉好像我们有可能意外的得到一个不同的类型。如果你想要显示的强制类型转换,声明目标类型也是可以的。然而,在绝大多数时间,你只使用auto就可以了;很少会发生因为错误而得到一个不一样的类型的情况,而且即使在这种情况下,依靠语言的强大类型系统,编译器会让你知道:你在尝试调用一个变量的并不存在的成员方法,或者不是以正常的方式使用它。

Smart pointers: No delete

始终使用智能指针和non-owing原始指针。绝不使用owing原始指针和delete,除非在在罕见的情况下,如你需要实现自己的底层数据结构(即使如此,也要在类边界里面保持良好的封装)。

如果你知道你是另外一个对象的唯一拥有者,就使用unique_ptr来明确唯一所有权。”new T“表达式应该立即初始化拥有它的另一个对象,通常使用unique_ptr。一个典型的例子是Pimpl惯用手法(见 GotW #100):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// C++11 Pimpl idiom: header file
class widget {
public:
widget();
// ... (see GotW #100) ...
private:
class impl;
unique_ptr<impl> pimpl;
};

// implementation file
class widget::impl { /*...*/ };

widget::widget(): pimpl{ new impl{ /*...*/} } { }
// ...

使用shared_ptr明确共享所有权。更喜欢使用make_shared来有效的创建共享对象。

1
2
3
4
5
6
7
// C++98
widget* pw = new widget();
...
delete pw;

// C++11
auto pw = make_shared<widget>();

使用weak_ptr打引用破循环和明确可选性(例:实现一个对象缓存)。

1
2
3
4
5
6
7
8
9
10
11
12
.. C++11
class gadget;

class widget {
private:
shared_ptr<gadget> g; // if shared ownership
};

class gadget {
private:
weak_ptr<widget> w;
};

如果你知道另一个对象比你的生存期长,而且你需要观察它,那么使用(non-owing)原始指针。

1
2
3
4
5
6
7
// C++11
class node {
vector<unique_ptr<node> > children;
node* parent;
public:
...
};

nullptr

始终使用nullptr作为空指针的值,绝不使用模棱两可的字面量0或者NULL宏,因为它们可以是一个整形或指针。

1
2
3
4
5
// C++98
int* p = 0;

// C++11
int* p = nullptr;

Range for

使用基于范围的for循环的方式来对一个范围内的元素进行顺序访问是非常方便的。

1
2
3
4
5
6
7
8
9
// C++98
for(vector<int>::iterator i = v.begin(); i != v.end(); ++i) {
total += *i;
}

// C++11
for(auto d: v) {
total += d;
}

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标准给它提供了beginend

1
2
3
4
5
6
7
8
9
10
vector<int> v;
int a[100];

// C++98
sort(v.begin(), v.end());
sort(&a[0], &a[0] + sizeof(a)/sizeof(a[0]));

// C++11
sort(begin(v), end(v));
sort(begin(a), end(a));

Lambda Functions and Algorithms

Lambda表达式是一个可以改变游戏规则的改变,它会经常会改变你写代码的方式,使之更优雅和更快速。Lambda表达式使现有的STL算法的可用性提升了100倍。新的C++库的设计越来越多的假设Lambda表达式是可用的(例:PPL),而且某些甚至要求你编写Lambda表达式来使用库(例:C++ AMP)。

这是一个简单的例子:在v中找到第一个大于x并小于y的元素。在C++11中,最简单的和最整洁的代码是使用标准算法。

1
2
3
4
5
6
7
// C++98: write a naked loop (using std::find_if is impractically difficult)
for(; i != v.end(); ++i) {
if(*i > x && *i < y) break;
}

// C++11: use std::find_if
auto i = find_if(begin(v), end(v), [=](int i) {return i > x && i < y;});

你是否想要一个循环或类似的语言特性(但语言并没有提供)?并不需要付出多大的努力,只要把它写成一个模板函数(算法库),利用lambda表达式你总是可以如同使用语言特性一样便利的使用它。而且具有更大的灵活性,因为它真的只是一个库,而不是一个天生的语言特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// C#
lock(mut_x) {
... use x ...
}

// C++11 without lambdas: already nice, and more flexible (e.g., can use timeouts, other options)
{
lock_guard<mutex> hold {mut_x};
... use x ...
}

// C++11 with lambdas, and a helper algorithm: C# syntax in C++
// Algorithm: template<typename T> void lock(T& t, F f) {lock_guard hold(t); f();}
lock(mut_x, [&]{
... use x ...
});

要熟悉lambda表达式,你将会大量的使用它们,不仅仅是在C++中—它们已经在几种流行的主要语言中被支持和广泛使用。一个很好的起点是我在PDC 2010上的演讲:Lambdas, Lambdas Everywhere

Move / &&

移动是作为拷贝优化的一个最好的思想,虽然它也可以做其他的事情,如:完美转发。

移动语义使我们设计API的方式发生了改变。我们将会设计出更多的返回值对象的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// C++98: alternatives to avoid copying
vector<int>* make_big_vector(); // option 1: return by pointer: no copy, but don't forget to delete
...
vector<int>* result = make_big_vector();

void make_big_vector(vector<int>& out); // option2: pass out by reference: no copy, but caller needs a named object
...
vector<int> result;
make_big_vector(result);


// C++11: move
vector<int> make_big_vector(); // usuall sufficient for 'caller-allocated out' situations
...
auto result = make_big_vector(); // guaranteed not to copy the vector

当你要做一些比拷贝更有效率的事情的时候,为你的类型启用移动语义。

Uniform Initialization and Initializer Lists

没有改变的是:当初始化一个局部变量时,且它的类型是非PODauto时,继续使用熟悉的 = 语法,而不用额外的{ }

1
2
3
4
5
6
// C++98 or C++11
int a = 42; // still fine, as always


// C++11
auto x = begin(v); // no narrowing or non-initialization is possible

在其它情况下,特别是你以前在构造对象时经常使用( )时,应该使用{ }代替。使用大括号可以避免几个潜在的问题:你不会意外的获得变窄类型转换(例:flotint);你不会偶尔不小心拥有未初始化的POD成员变量或数组;你会避免C++98给你带来的偶然惊喜:由于在C++语法中声明某凌两可(Scott Meyers 的一句名言:”C++’s most vexing
parse.”),你实际上声明了一个函数而不是一个变量,但还是能通过编译。在新的风格中没有这样的烦恼。

1
2
3
4
5
6
7
8
9
10
11
12
// C++98
rectangle w(origin(), extents()); // oops, declares a function, if origin and extents are types
complex<double> c(2.71828, 3.14159);
int a[] = {1,2,3,4};
vector<int> v;
for(int i = 1; i <= 4; ++i) v.push_back(i);

// C++11
rectangle w {origin(), extents()};
complex<double> c {2.71828, 3.14159};
int a[] {1,2,3,4};
vector<int> v {1,2,3,4};

新的{ }语法在每个地方都工作的很好:

1
2
3
4
5
6
// C++98
X::X(/*...*/): mem1(init1), mem2(init2, init3) {/*...*/}


// C++11
X::X(/*...*/): mem1{init1}, mem2{init2, init3} {/*...*/}

最后,有些时候只是为了方便,在给函数传递参数时可以不需要临时的类型名。

1
2
3
4
5
6
7
8
void draw_rect(rectangle);

// C++98
draw_rect(rectangle(myobj.origin, selection.extents));


// C++11
draw_rect({myobj.origin, selection.extents});

我不使用大括号的唯一地方是在对非POD变量进行简单初始化的时候,如:auto x = begin(v);,不然它会使代码将变得不必要的丑陋。因为我知道它是一个类类型,所以我知道我不需要担心偶然的变窄类型转换,而且现代的编译器已经例行的执行优化以删去额外的拷贝(或额外的移动,如果类型启用了移动语义)。

And More

现代C++还有更多特性。在将来,我打算写一些更深入的文章,关于C++11的这些和其它的我们要去了解和喜欢的特性。

但是现在,这里是我们必须要知道的特性列表。这些特性形成的核心,定义了现代C++的风格,这使得C++代码看起来和执行起来确实如此;你会发现它们在你所将要看到或编写的每一段现代C++代码中会普遍的被使用;使得现代C++是一门整洁、安全和快速的语言,在未来的许多年里我们的行业将会继续严重依赖于它。

评论