原文标题:Ten C++11 Features Every C++ Developer Should Use
原文作者:Marius Bancila
原文地址:codeproject
备注:非直译,带个人感情色彩,有疑惑参看原文。
This article discusses a series of features new to C++11 that all developers should learn and use. There are lots of new additions to the language and the standard library, and this article barely scratches the surface. However, I believe some of these new features should become routine for all C++ developers. You could probably find many similar articles evangelizing different C++11 features. This is my attempt to assemble a list of C++ features that should be a norm nowadays.
本文讨论了10个C++11中新增的特性,所有的C++开发者都应学习和使用。C++11无论是语言本身还是标准库都新增了许多新的东西,而本文仅仅是介绍了皮毛。但我认为其中的部分新特性应该被应用到开发者的日常开发中。已有许多文章介绍了C++11与之前版本的不同之处,下面我将尝试列出当前应作为准则的那些C++的特性。
内容索引:
- 自动类型(auto)
- 空指针(nullptr)
- 集合循环(Range-based for loops)
- override和final标识(override and final)
- 强类型的枚举类型(Strongly-typed enums)
- 智能指针(Smart pointers)
- Lambdas表达式(Lambdas)
- 非成员的begin()和end()函数(non-member begin() and end())
- 静态断言和类型特性(static_assert and type traits)
- 移动语义(Move semantics)
自动类型
Before C++11 the auto keyword was used for storage duration specification. In the new standard its purpose was changed towards type inference. auto
is now a sort of placeholder for a type, telling the compiler it has to deduce the actual type of a variable that is being declared from its initializer. It can be used when declaring variables in different scopes such as namespaces, blocks or initialization statement of for loops.
在C++11之前auto关键字用于标识自动存储的临时变量,而在新标准中auto用于类型推断。auto现在是一种类型占位符,告诉编译器需要从变量的初始定义中推断出该变量的实际类型。它可用于不同域中变量的定义,如命名空间、块、for循环中的初始化语句等。
1 auto i = 42; // i is an int 2 auto l = 42LL; // l is an long long 3 auto p = new foo(); // p is a foo*
Using auto
usually means less code (unless your type is int which is one letter shorter). Think of iterators in STL that you always had to write while iterating over containers. It makes obsolete creating typedefs just for the sake of simplicity.
使用auto可以减少代码长度(int除外,它比auto还少一个字符),想一想STL中每次需要遍历容器时你不得不写一遍的迭代器就知道auto有多好用了。它使得创建typedef来精简代码成为了过时的技术。
1 std::map<std::string, std::vector<int>> map; 2 for(auto it = begin(map); it != end(map); ++it) 3 { 4 }
You should note that auto cannot be used as the return type of a function. However, you can use auto in place of the return type of function, but in this case the function must have a trailing return type. In this case auto does not tell the compiler it has to infer the type, it only instructs it to look for the return type at the end of the function. In the example below the return type of function compose is the return type of operator+ that sums values of types T1 and T2.
或许你已注意到了auto不能作为函数的返回类型。但是在函数尾标注了函数的返回类型时,可以在写函数返回值类型的地方使用auto(但这样用没什么意义,应该是作为到C++14的过渡吧)。这种情况下auto并没有让编译器去推断返回类型,而只是告诉它到函数尾去查看返回类型。在下面的例子中,函数compose的返回类型是表达式(t1+t2)的返回类型。
1 template <typename T1, typename T2> 2 auto compose(T1 t1, T2 t2) -> decltype(t1 + t2) 3 { 4 return t1+t2; 5 } 6 auto v = compose(2, 3.14); // v's type is double
空指针
Zero used to be the value of null pointers, and that has drawbacks due to the implicit conversion to integral types. The keyword nullptr
denotes a value of type std::nullptr_t
that represents the null pointer literal. Implicit conversions exists from nullptr to null pointer value of any pointer type and any pointer-to-member types, but also to bool (as false). But no implicit conversion to integral types exist.
C98中空指针的值为0,由于隐式转换为整型带来了很多弊端。而关键字nullptr提供了一个std::nullptr_tr类型的值专门来表示空指针的含义。隐式转换可以发生在从nullptr到任何指针类型的空指针值或任何成员指针类型的空指针值,也可以转换为bool类型(作为false),但不会发生隐式转换为整型的情况。
1 void foo(int* p) {} 2 3 void bar(std::shared_ptr<int> p) {} 4 5 int* p1 = NULL; 6 int* p2 = nullptr; 7 if(p1 == p2) 8 { 9 } 10 11 foo(nullptr); 12 bar(nullptr); 13 14 bool f = nullptr; 15 int i = nullptr; // error: A native nullptr can only converted to bool or, using reinterpret_cast, to an integral type
For backward compatibility 0 is still a valid null pointer value.
为了向后兼容,0仍然可以作为空指针的值。
集合循环
C++11 augmented the for statement to support the "foreach" paradigm of iterating over collections. In the new form, it is possible to iterate over C-like arrays, initializer lists and anything for which the non-member begin()
and end()
functions are overloaded.
C++11扩展了for表达式以支持遍历集合时类似“foreach”样式的功能。新的形式可以遍历C风格的数组、初始化列表以及任何重载了非成员函数begin()和end()的数据集合。
This for each for is useful when you just want to get and do something with the elements of a collection/array and don't care about indexes, iterators or number of elements.
如果你仅仅是想对集合中的元素做点什么,而并不关心集合中的索引、迭代器、元素个数等,那么逐个访问的for语句是个不错的选择。
1 std::map<std::string, std::vector<int> > map; 2 std::vector<int> v; 3 v.push_back(1); 4 v.push_back(2); 5 v.push_back(3); 6 map["one"] = v; 7 8 for(const auto& kvp : map) 9 { 10 std::cout << kvp.first << std::endl; 11 12 for(auto v : kvp.second) 13 { 14 std::cout << v << std::endl; 15 } 16 17 } 18 19 int arr[] = {1,2,3,4,5}; 20 for(int& e : arr) 21 { 22 e = e*e; 23 }
override和final标识
I always founded the virtual methods badly designed in C++ because there wasn't (and still isn't) a mandatory mechanism to mark virtual methods as overridden in derived classes. The virtual keyword is optional and that makes reading code a bit harder, because you may have to look through the top of the hierarchy to check if the method is virtual. I have always used, and encouraged people to use the virtual keyword on derived classes also, to make the code easier to read. However, there are subtle errors that can still arise. Take for instance the following example:
我一向认为C++中虚拟方法的设计很差,它没有(现在也没有)一个强制机制来标记虚拟方法在派生类中的重写情况。virtual关键字是可选的,有时你必须沿着继承链找到最顶端的父类来判断这个方法是否为虚拟方法,降低了代码可读性。因此我提倡在派生类中使用virtual关键字以让代码更容易阅读。但还是可能出现一些微妙的错误,看看下面的例子:
1 class B 2 { 3 public: 4 virtual void f(short) {std::cout << "B::f" << std::endl;} 5 }; 6 7 class D : public B 8 { 9 public: 10 virtual void f(int){std::cout << "D::f" << std::endl;} 11 };
D::f
is supposed to override B::f
. However, the signature differ, one takes a short
, one takes an int
, therefor B::f
is just another method with the same name (and overload) and not an override. You may call f()
through a pointer to B
and expect to print D::f
, but it's printing B::f
.
D::f原本要重写B::f的,但由于参数的类型一个是short而另一个是int,造成函数签名不同,因此B::f只是一个拥有相同名字(而且被重载了)的另一个方法,并没有被重写。你可能希望通过指向B的指针访问f()函数输出D::f,但它输出的却是B::f。
Here is another subtle error: the parameters are the same, but the method in the base class is marked const, while method in the derived is not.
还有一个微妙的错误:函数参数相同,但基类中该方法被标记为const,而派生类中没有标记。
1 class B 2 { 3 public: 4 virtual void f(int) const {std::cout << "B::f" << std::endl;} 5 }; 6 7 class D : public B 8 { 9 public: 10 virtual void f(int) {std::cout << "D::f" << std::endl;} 11 };
Again, these two are overloads and not overrides, so if you call f()
through a pointer to B
it will print B::f
and not D::f
.
这两个方法仍然是重载的关系而不是重写。如果你通过指向B的指针访问f()输出的是B::f而不是D::f。
Fortunately there is now a way to describe your intentions. Two new special identifiers (not keywords) have been added: override
, to indicate that a method is supposed to be an override of a virtual method in a base class, and final
, to indicate that a derived class shall not override a virtual method. The first example would become:
现在这个问题得到了解决。C++11新增了两个标记(注意不是关键字):override和final。override表明一个方法是对基类中虚拟方法的重写,final表明派生类中的方法不能重写虚拟方法。第一个例子如下:
1 class B 2 { 3 public: 4 virtual void f(short) {std::cout << "B::f" << std::endl;} 5 }; 6 7 class D : public B 8 { 9 public: 10 virtual void f(int) override {std::cout << "D::f" << std::endl;} 11 };
This now triggers a compiler error (the same error you'd get for the second example too, if using the override
specifier):
'D::f' : method with override specifier 'override' did not override any base class methods
例子中的写法会触发一个编译错误(上文使用const的例子中如果使用override说明符的话也会触发这个错误):
“ 'D::f': 带有重写说明符'override'的方法没有重写任何基类方法 ”
On the other hand if you intend to make a method impossible to override any more (down the hierarchy) mark it as final
. That can be in the base class, or any derived class. If it's in a derived classes you can use both the override
and final
specifiers.
而如果需要把一个方法标记为不能再被重写(这个方法在所在类的派生类中都不能被重写),使用final说明符。可以在基类中使用final,也可以在任何派生类中使用final。派生类中即可以使用override说明符,也可以使用final说明符。
1 class B 2 { 3 public: 4 virtual void f(int) {std::cout << "B::f" << std::endl;} 5 }; 6 7 class D : public B 8 { 9 public: 10 virtual void f(int) override final {std::cout << "D::f" << std::endl;} 11 }; 12 13 class F : public D 14 { 15 public: 16 virtual void f(int) override {std::cout << "F::f" << std::endl;} 17 };
function declared as 'final' cannot be overridden by 'F::f'
例子中声明为'final'的方法不能被F::f重写。
强类型的枚举类型
"Traditional" enums in C++ have some drawbacks: they export their enumerators in the surrounding scope (which can lead to name collisions, if two different enums in the same have scope define enumerators with the same name), they are implicitly converted to integral types and cannot have a user-specified underlying type.
C++中传统的枚举类型有一些缺点:会把枚举数导出到外围域中(当两个不同的枚举类型同时拥有定义了相同名字的枚举数的域时,会导致命名冲突);会隐式地转换为整型,并不能使用用户指定的基础类型。
These issues have been fixed in C++ 11 with the introduction of a new category of enums, called strongly-typed enums. They are specified with the enum class
keywords. They no longer export their enumerators in the surrounding scope, are no longer implicitly converted to integral types and can have a user-specified underlying type (a feature also added for traditional enums).
这些问题在C++11中得到了解决。C++11引进了新的一种枚举类型,叫强类型枚举类型,通过关键字“enum class”指定。它们不再将枚举数导出到外围域中,也不会隐式转换为整型,可以使用用户自定义的基础类型(这种特性也加入了传统枚举类型中)。
1 enum class Options {None, One, All}; 2 Options o = Options::All;
智能指针
There have been tons of articles written on this subject, therefore I just want to mention the smart pointers with reference counting and auto releasing of owned memory that are available:
- unique_ptr: should be used when ownership of a memory resource does not have to be shared(it doesn't have a copy constructor), but it can be transferred to another unique_ptr(move constructor exists).
- shared_ptr: should be used when ownership of a memory resource should be shared(hence the name).
- weak_ptr: holds a reference to an object managed by a shared_ptr, but does not contribute to the reference count; it is used to break dependency cycles(think of a tree where the parent holds an owning reference (shared_ptr) to its children, but the children also must hold a reference to the parent; if this second reference was also an owning one, a cycle would be created and no object would ever be released).
已有很多文章讨论了这个主题,因此这里仅说一下智能指针的引用计数和自动释放自身的可用内存:
- unique_ptr:该指针所指内存资源的所有权不能被共享(没有复制构造函数),但该指针可以转换成另一个unique_ptr指针(存在移动构造函数)。
- shared_ptr:该指针所指内存资源的所有权可被共享(如其名)。
- weak_ptr:该指针是对由shared_ptr管理的一个对象的引用,但并不会增加引用计数;该指针用来打破循环依赖(设想一下一棵树的情景,父结点持有对子结点的引用(shared_ptr类型),同时子结点也持有对父结点的引用;如果子对父的引用也是shared_ptr类型的话,就导致了循环引用,父与子结点都不能被释放)。
On the other hand the auto_ptr is obsolete and should no longer be used.
When you should unique_ptr and when you should use shared_ptr depends on the ownership requirements and I recommend reading this discussion.
auto_ptr类型已经过时不再使用了。如果你需要使用unique_ptr,或者需要使用有所有权需求的shared_ptr指针时,推荐你读一下这篇文章。
The first example below shows unique_ptr. If you want to transfer ownership of an object to another unique_ptr use std::move (I'll discuss this function in the last paragraph). After the ownership transfer, the smart pointer that ceded the ownership becomes null and get() returns nullptr.
下面的例子演示了unique_ptr的用法。如果你想把一个对象的所有权转交给另一个unique_ptr指针,可以用std::move函数(在下一段讨论这个函数)。所有权转交后,放弃所有权的智能指针变为空指针,通过get()方法返回的值为nullptr。
1 void foo(int* p) 2 { 3 std::cout << *p << std::endl; 4 } 5 std::unique_ptr<int> p1(new int(42)); 6 std::unique_ptr<int> p2 = std::move(p1); // transfer ownership 7 8 if(p1) 9 foo(p1.get()); 10 11 (*p2)++; 12 13 if(p2) 14 foo(p2.get());
The second example shows shared_ptr. Usage is similar, though the semantics are different since ownership is shared.
下面的例子演示了shared_ptr的用法。和unique_ptr的用法类似,不同之处在于语义:shared_ptr的所有权是共享的。
1 void foo(int* p) 2 { 3 } 4 void bar(std::shared_ptr<int> p) 5 { 6 ++(*p); 7 } 8 std::shared_ptr<int> p1(new int(42)); 9 std::shared_ptr<int> p2 = p1; 10 11 bar(p1); 12 foo(p2.get());
The first declaration is equivalent to this one:
第一条声明(指p1的声明)等价于下面这条:
auto p3 = std::make_shared<int>(42);
make_shared<T> is a non-member function and has the advantage of allocating memory for the shared object and the smart pointer with a single allocation, as opposed to the explicit construction of a shared_ptr via the contructor, that requires at least two allocations. In addition to possible overhead, there can be situations where memory leaks can occur because of that. In the next example memory leaks could occur if send() throws an error.
非成员函数make_shared<T>可以方便地通过单独一次内存分配过程为共享对象和智能指针分配内存,而通过shared_ptr的构造函数进行隐式构造需要至少两次分配过程。多次分配需要额外的开销,有些情况下会造成内存泄漏,比如下面的例子,如果seed()抛出了一个错误那么将引起内存泄漏。
1 void foo(std::shared_ptr<int> p, int init) 2 { 3 *p = init; 4 } 5 foo(std::shared_ptr<int>(new int(42)), seed());
No such problem exists if using make_shared. The third sample shows usage of weak_ptr. Notice that you always must get a shared_ptr to the referred object by calling lock(), in order to access the object.
如果使用make_shared则不存在这样的问题。下面的是使用weak_ptr的例子。注意,如果需要访问weak_ptr引用的对象,则必须调用weak_ptr的lock()方法并将返回值传给shared_ptr类型的指针。
1 auto p = std::make_shared<int>(42); 2 std::weak_ptr<int> wp = p; 3 4 { 5 auto sp = wp.lock(); 6 std::cout << *sp << std::endl; 7 } 8 9 p.reset(); 10 11 if(wp.expired()) 12 std::cout << "expired" << std::endl;
If you try to lock on an expired weak_ptr (the object is weakly reference has been released) you get an empty shared_ptr.
如果尝试锁定一个过期的weak_ptr(弱引用的对象已经被释放了),将返回一个空的shared_ptr。
Lambdas表达式
Anonymous functions, called lambda, have been added to C++ and quickly rose to prominence. It is a powerful feature borrowed from functional programming, that in turned enabled other features or powered libraries. You can use lambdas wherever a function object or a functor or a std::function is expected. You can read about the syntax here.
C++11中引进了用于定义匿名函数的lambda表达式,并很快成为了非常重要的特性。它是从函数式编程中借鉴来的强大特性,反过来它带来了其它的特性(或者说引进了其它的函数库)。任何可以用函数对象、函子或std::function的地方都可以使用lambda表达式。如果想详细了解它的语法请戳这里。
1 std::vector<int> v; 2 v.push_back(1); 3 v.push_back(2); 4 v.push_back(3); 5 6 std::for_each(std::begin(v), std::end(v), [](int n) {std::cout << n << std::endl;}); 7 8 auto is_odd = [](int n) {return n%2==1;}; 9 auto pos = std::find_if(std::begin(v), std::end(v), is_odd); 10 if(pos != std::end(v)) 11 std::cout << *pos << std::endl;
A bit trickier are recursive lambdas. Imagine a lambda that represents a Fibonacci function. If you attempt to write it using auto you get compilation error:
需要注意的是递归lambda表达式。比如用lambda表达式来表示Fibonacci方程,如果尝试像下面这样用auto来实现,将导致编译错误。
auto fib = [&fib](int n) {return n < 2 ? 1 : fib(n-1) + fib(n-2);};
error C3533: 'auto &': a parameter cannot have a type that contains 'auto' error C3531: 'fib': a symbol whose type contains 'auto' must have an initializer error C3536: 'fib': cannot be used before it is initialized error C2064: term does not evaluate to a function taking 1 arguments
The problem is auto means the type of the object is inferred from its initializer, yet the initializer contains a reference to it, therefore needs to know its type. This is a cyclic problem. The key is to break this dependency cycle and explicitly specify the function's type using std::function.
问题在于auto类型的对象需要通过它的初始语句来推断出具体的类型,而初始语句包含了对它的引用,又需要知道它的具体类型。这造成了循环依赖问题。解决的关键在于打断依赖循环,可通过std::function来明确地指明函数的具体类型。
std::function<int(int)> lfib = [&lfib](int n) {return n < 2 ? 1 : lfib(n-1) + lfib(n-2);};
非成员的begin()和end()函数
You probably noticed I have used in the samples above non-member begin() and end() functions. These are a new addition to the standard library, promoting uniformity, consistency and enabling more generic programming. They work with all STL containers, but more than that they are overloadable, so they can be extended to work with any type. Overloads for C-like arrays are also provided.
你或许已经注意到了上面例子中频繁出现的非成员函数begin()和end()。这是新加进标准库的函数,提升了标准库的统一性和一致性,并让工程师可以编写更通用的程序。这两个函数适用于所有的STL容器,而且还是可重载的,可以扩展应用到任何的类型,同时提供了应用于C风格数组的重载。
Let's take for instance the previous example where I was printing a vector and then looking for its first odd element. If the std::vector was instead a C-like array, the code might have looked like this:
让我们再看一下前面输出vector数组元素并查找第一个奇数的例子。如果将std::vector数组换成C风格数组,代码可能看起来像这样:
1 int arr[] = {1,2,3}; 2 std::for_each(&arr[0], &arr[0]+sizeof(arr)/sizeof(arr[0]), [](int n) {std::cout << n << std::endl;}); 3 4 auto is_odd = [](int n) {return n%2==1;}; 5 auto begin = &arr[0]; 6 auto end = &arr[0]+sizeof(arr)/sizeof(arr[0]); 7 auto pos = std::find_if(begin, end, is_odd); 8 if(pos != end) 9 std::cout << *pos << std::endl;
With non-member begin() and end() it could be put as this:
使用了非成员函数begin()和end()之后它是这个样子的:
1 int arr[] = {1,2,3}; 2 std::for_each(std::begin(arr), std::end(arr), [](int n) {std::cout << n << std::endl;}); 3 4 auto is_odd = [](int n) {return n%2==1;}; 5 auto pos = std::find_if(std::begin(arr), std::end(arr), is_odd); 6 if(pos != std::end(arr)) 7 std::cout << *pos << std::endl;
This is basically identical code to the std::vector version. That means we can write a single generic method for all types supported by begin() and end().
这段代码和std::vector版本的基本相同,也就是说我们可以为所有支持begin()和end()方法的类型写一个通用的方法。
1 template <typename Iterator> 2 void bar(Iterator begin, Iterator end) 3 { 4 std::for_each(begin, end, [](int n) {std::cout << n << std::endl;}); 5 6 auto is_odd = [](int n) {return n%2==1;}; 7 auto pos = std::find_if(begin, end, is_odd); 8 if(pos != end) 9 std::cout << *pos << std::endl; 10 } 11 12 template <typename C> 13 void foo(C c) 14 { 15 bar(std::begin(c), std::end(c)); 16 } 17 18 template <typename T, size_t N> 19 void foo(T(&arr)[N]) 20 { 21 bar(std::begin(arr), std::end(arr)); 22 } 23 24 int arr[] = {1,2,3}; 25 foo(arr); 26 27 std::vector<int> v; 28 v.push_back(1); 29 v.push_back(2); 30 v.push_back(3); 31 foo(v);
静态断言和类型特性
static_assert performs an assertion check at compile-time. If the assertion is true, nothing happens. If the assertion is false, the compile displays the specified error message.
静态断言在编译时进行一次断言检查。如果断言为真,什么也不做;如果断言为假,编译器会显示特定的错误消息。
1 template <typename T, size_t Size> 2 class Vector 3 { 4 static_assert(Size < 3, "Size is too small"); 5 T _points[Size]; 6 }; 7 8 int main() 9 { 10 Vector<int, 16> a1; 11 vector<double, 2> a2; 12 return 0; 13 }
error C2338: Size is too small see reference to class template instantiation 'Vector<T,Size>' being compiled with [ T=double, Size=2 ]
static_assert becomes more useful when used together with type traits. These are a series of classes that provide information about types at compile time. They are available in the <type_traits> header. There are several categories of classes in this header: helper classes, for creating compile-time constants, type traits classes, to get type information at compile time, and type transformation classes, for getting new types by applying transformation on existing types.
静态断言配合类型特性一起使用将更加强大。所谓类型特性就是在编译阶段提供类型信息的一系列类,相关定义位于头文件<type_traits>中。这个头文件中定义了好几种类:辅助类,用于创建编译时的常量;类型特性类,用于获取编译时关于类型的信息;类型转换类,对已存在的类型进行转换以生成新的类型。
In the following example function add is supposed to work only with ingegral types.
在下面的例子中,add函数原本被设定为只能处理整型。
1 template <typename T1, typename T2> 2 auto add(T1 t1, T2 t2) -> decltype(t1 + t2) 3 { 4 return t1 + t2; 5 }
However, there are no compiler error if one writes
然而如果下面这样写的话也不会报编译错误:
1 std::cout << add(1, 3.14) << std::endl; 2 std::cout << add("one", 2) << std::endl;
The program actually prints 4.14 and"e". But if we add some compile-time asserts, both these lines would generate compiler errors.
事实上程序输出的结果为4.14和"e"。但如果增加了编译时断言,上面的两行代码都会导致编译时错误。
1 template <typename T1, typename T2> 2 auto add(T1 t1, T2 t2) -> decltype(t1 + t2) 3 { 4 static_assert(std::is_integral<T1>::value, "Type T1 must be integral"); 5 static_assert(std::is_integral<T2>::value, "Type T2 must be integral"); 6 7 return t1 + t2; 8 }
error C2338: Type T2 must be integral see reference to function template instantiation 'T2 add<int,double>(T1,T2)' being compiled with [ T2=double, T1=int ] error C2338: Type T1 must be integral see reference to function template instantiation 'T1 add<const char*,int>(T1,T2)' being compiled with [ T1=const char *, T2=int ]
移动语义
This is yet another important and well covered topic from C++11, that one could write a series of articles, not just a paragraph. Therefore I will not get into too many details, but encourage you to find additional readings, if you're not already familiar with the topic.
移动语义是另一个C++11中重要而且广泛讨论的话题,据此可以写好几篇文章而不仅仅是一个段落。因此我不会过多地讨论细节,但如果你还不熟悉这个话题,建议找几篇相关的文章来读一读。
C++11 has introduced the concept of rvalue references (specified with &&) to differentiate a reference to an lvalue or an rvalue. An lvalue is an object that has a name, while an rvalue is an object that does not have a name (a temporary object). The move semantics allow modifying rvalues (previously considered immutable and indistinguishable from const T& types).
C++11通过引进右值引用(通过&&指定)的概念来区别一个引用是左值的还是右值的。左值指有具体名字的对象,右值指没有具体名字的对象(临时对象)。移动语义则允许修改右值(之前认为右值是不变的,和const T&类型没有区别)。
A C++ class/struct used to have some implicit member functions: default constructor (only if another constructor is not explicitly defined) and copy constructor, a destructor and a copy assignment operator. The copy constructor and the copy assignment operator perform a bit-wise (or shallow) copy, i.e. copying the variables bitwise. That means if you have a class that contains pointers to some objects, they just copy the value of the pointers and not the objects they point to. This might be OK in some cases, but for many cases you actually want a deep-copy, meaning that you want to copy the objects pointers refer to, and not the values of the pointers. In this case you have to explicitly write copy constructor and copy assignment operator to perform a deep-copy.
C++的类或结构体通常有一些隐式成员函数:默认构造函数(只有当没有其它显示定义的构造函数时才存在),复制构造函数,析构函数,以及复制赋值运算符。复制构造函数和复制赋值运算符进行按位复制(也叫浅复制),即按位复制变量。当定义的类包含一些指向对象的指针时,它们仅仅复制了指针的值而不复制指针所指的对象。在一些情况下这样做OK,但更多时候我们真正想做的是进行深复制,即需要复制指针所指的对象而不是指针的值。这种情况下需要显式地写出复制构造函数以及复制赋值运算符以进行深复制。
What if the object you initialize or copy from is an rvalue (a temporary). You still have to copy its value, but soon after the rvalue goes away. That means an overhead of operations, including allocations and memory copying that after all, should not be necessary.
如果你需要进行初始化或复制的对象是一个右值(临时的对象)则应该怎么办?你依然需要复制它的值,但很快右值对象就会消失。这意味着包括内存分配和内存拷贝在内的所有操作的开销都是没有必要的。
Enter the move constructor and move assignment operator. These two special functions take a T&& argument, which is an rvalue. Knowing that fact, they can modify the object, such as "stealing" the objects their pointers refer to. For instance, a container implementation (such as a vector or a queue) may have a pointer to an array of elements. When an object is instantiating from a temporary, instead of allocating another array, copying the values from the temporary, and then deleting the memory from the temporary when that is destroyed, we just copy the value of the pointer that refers to the allocated array, thus saving an allocation, copying a sequence of elements, and a later de-allocation.
现在来看看移动构造函数和移动赋值操作符。这两个特殊的函数都有一个T&&类型的参数,这个参数是一个右值。记住,它们都可以修改对象,比如“盗取”它们的指针所指向的对象等。例如,一个容器的实现(vector或者queue等)可能包含一个指向元素数组的指针。当从一个临时量中实例化一个对象时,我们不需要:分配另一个数组,从临时量中拷贝值出来,然后在删除它时释放临时量所占的内存。我们只需要复制指向已分配数组的指针的值即可,这样就节省了分配内存、复制数组元素和随后的释放内存的开销。
The following example shows a dummy buffer implementation. The buffer is identified by a name (just for the sake of showing a point revealed below), has a pointer (wrapper in an std::unique_ptr
) to an array of elements of type T and variable that tells the size of the array.
下面的例子演示了虚拟缓存的实现。缓存通过名字进行识别,有一个指向元素类型为T的数组的指针(封装在一个std::unique_ptr中),还有一个存储数组长度的变量。
1 template <typename T> 2 class Buffer 3 { 4 std::string _name; 5 size_t _size; 6 std::unique_ptr<T[]> _buffer; 7 8 public: 9 // default constructor 10 Buffer(): 11 _size(16), 12 _buffer(new T[16]) 13 {} 14 15 // constructor 16 Buffer(const std::string& name, size_t size): 17 _name(name), 18 _size(size), 19 _buffer(new T[size]) 20 {} 21 22 // copy constructor 23 Buffer(const Buffer& copy): 24 _name(copy._name), 25 _size(copy._size), 26 _buffer(new T[copy._size]) 27 { 28 T* source = copy._buffer.get(); 29 T* dest = _buffer.get(); 30 std::copy(source, source + copy._size, dest); 31 } 32 33 // copy assignment operator 34 Buffer& operator=(const Buffer& copy) 35 { 36 if(this != ©) 37 { 38 _name = copy._name; 39 40 if(_size != copy) 41 { 42 _buffer = nullptr; 43 _size = copy._size; 44 _buffer = _size > 0 ? new T[_size] : nullptr; 45 } 46 47 T* source = copy._buffer.get(); 48 T* dest = _buffer.get(); 49 std::copy(source, source + copy._size, dest); 50 } 51 52 return *this; 53 } 54 55 // move constructor 56 Buffer(Buffer&& temp): 57 _name(std::move(temp._name)), 58 _size(temp._size), 59 _buffer(std::move(temp._buffer)) 60 { 61 temp._buffer = nullptr; 62 temp._size = 0; 63 } 64 65 // move assignment operator 66 Buffer& operator=(Buffer&& temp) 67 { 68 assert(this != &temp); // assert if this is not a temporary 69 70 _buffer = nullptr; 71 _size = temp.size; 72 _buffer = std::move(temp._buffer); 73 74 _name = std::move(temp._name); 75 76 temp._buffer = nullptr; 77 temp._size = 0; 78 79 return *this; 80 } 81 }; 82 83 template <typename T> 84 Buffer<T> getBuffer(const std::string& name) 85 { 86 Buffer<T> b(name, 128); 87 return b; 88 } 89 90 int main() 91 { 92 Buffer<int> b1; 93 Buffer<int> b2("buf2", 64); 94 Buffer<int> b3 = b2; 95 Buffer<int> b4 = getBuffer<int>("buf4"); 96 b1 = getBuffer<int>("buf5"); 97 return 0; 98 }
The default copy constructor and copy assignment operator should look familiar. What's new to C++11 is the move constructor and move assignment operator, implemented in the spirit of the aforementioned move semantics. If you run this code you'll see that when b4 is constructed, the move constructor is called. Also, when b1 is assigned a value, the move assignment operator is called. The reason is the value returned by getBuffer()
is a temporary, i.e. an rvalue.
默认复制构造函数和复制赋值操作符和以前的类似,C++中新的的东西是基于上述移动语义的精神实现的移动构造函数和移动赋值操作符。运行上面这段程序时会发现,b4在构造时调用了移动构造函数,b1被赋值时调用了移动赋值操作符。原因是getBuffer()函数的返回值是一个临时量,即是一个右值。
You probably noticed the use of std::move in the move constructor, when initializing the name variable and the pointer to the buffer. The name is actually a string, and std::string
also implements move semantics. Same for the std::unique_ptr
. However, if we just said _name(temp._name)
the copy constructor would have been called. For _buffer that would not have been even possible because std::unique_ptr
does not have a copy constructor. But why wasn't the move constructor for std::string
called in this case? Because even if the object the move constructor for Buffer
is called with is an rvalue, inside the constructor it is actually an lvalue. Why? Because it has a name, "temp
" and a named object is an lvalue. To make it again an rvalue (and be able to invoke the appropriate move constructor) one must use std::move
. This function just turns an lvalue reference into an rvalue reference.
你或许已经注意到了移动构造函数中初始化名字变量和指向缓存的指针时使用到的std::move。名字变量实际上是string类型,而std::string和std::unique_ptr一样也实现了移动语义。如果我们说通过_name(temp._name)语句复制构造函数将被调用,但对于_buffer来说这不可能,因为std::unique_ptr连复制构造函数都没有。然而这种情况下std::string的移动构造函数为什么没有被调用呢?因为尽管移动构造函数为Buffer调用的对象是一个右值,但在构造函数内部实际上它是一个左值。为什么?因为它有一个叫作“temp”的名字,而有名字的对象是一个左值。使用std::move的目的就是让它再次成为一个右值(以便调用适当的移动构造函数)。std::move函数的作用就是将一个左值引用转换为一个右值引用。
UPDATE: Though the purpose of this example was to show how move constructor and move assignment operator should be implemented, the exact details of an implementation may vary. An alternative implementation was provided by Member 7805758 in the comments. To be easier to see it I will show it here:
更新:尽管这个例子的目的是演示移动构造函数和移动赋值操作符的实现方式,但具体的实现细节却大有不同。评论中的Member 7805758提供了一种实现方式,为了阅读方便写到了正文中:
1 template <typename T> 2 class Buffer 3 { 4 std::string _name; 5 size_t _size; 6 std::unique_ptr<T[]> _buffer; 7 8 public: 9 // constructor 10 Buffer(const std::string& name = "", size_t size = 16): 11 _name(name), 12 _size(size), 13 _buffer(size ? new T[size] : nullptr) 14 {} 15 16 // copy constructor 17 Buffer(const Buffer& copy): 18 _name(copy._name), 19 _size(copy._size), 20 _buffer(copy._size ? new T[copy._size] : nullptr) 21 { 22 T* source = copy._buffer.get(); 23 T* dest = _buffer.get(); 24 std::copy(source, source + copy._size, dest); 25 } 26 27 // copy assignment operator 28 Buffer& operator=(Buffer copy) 29 { 30 swap(*this. copy); 31 return *this; 32 } 33 34 // move constructor 35 Buffer(Buffer&& temp):Buffer() 36 { 37 swap(*this, temp); 38 } 39 40 friend void swap(Buffer& first, Buffer& second) noexcept 41 { 42 using std::swap; 43 swap(first._name , second._name); 44 swap(first._size , second._size); 45 swap(first._buffer , second._buffer); 46 } 47 };
总结
There are many more things to say about C++11; this was just one of many possible beginnings. This article presented a series of core language and standard library features that every C++ developer should use. However, I recommend you additional readings, at least for some of these features.
还有许多关于C++11的东西可以讨论,这仅仅只是一个开始。本文展示了C++开发者都会用到的一些核心语言和标准库中的特性,我建议至少对于其中的一些特性再找些其它文章做进一步的学习。
许可
This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)
本文及相关源代码和文件遵循CPOL许可。