C++开发者都应该使用的10个C++11特性

来源:互联网 发布:联想电脑mac地址查询 编辑:程序博客网 时间:2024/06/02 00:53

在C++11新标准中,语言本身和标准库都增加了很多新内容,本文只涉及了一些皮毛。不过我相信这些新特性当中有一些,应该成为所有C++开发者的常规装备。你也许看到过许多类似介绍各种C++11特性的文章。下面是我总结的,C++开发者都需要学习和使用的C++11新特性。

auto

在C++11之前,auto关键字用来指定存储期。在新标准中,它的功能变为类型推断。auto现在成了一个类型的占位符,通知编译器去根据初始化代码推断所声明变量的真实类型。各种作用域内声明变量都可以用到它。例如,名空间中,程序块中,或是for循环的初始化语句中。

auto i = 42; // i is an intauto l = 42LL; // l is an long longauto p = newfoo();// p is a foo*

使用auto通常意味着更短的代码(除非你所用类型是int,它会比auto少一个字母)。试想一下当你遍历STL容器时需要声明的那些迭代器(iterator)。现在不需要去声明那些typedef就可以得到简洁的代码了。

std::map<std::string, std::vector<int>> map;for(auto it = begin(map); it != end(map); ++it) {}

需要注意的是,auto不能用来声明函数的返回值。但如果函数有一个尾随的返回类型时,auto是可以出现在函数声明中返回值位置。这种情况下,auto并不是告诉编译器去推断返回类型,而是指引编译器去函数的末端寻找返回值类型。在下面这个例子中,函数的返回值类型就是operator+操作符作用在T1、T2类型变量上的返回值类型。

template<typenameT1,typenameT2>auto compose(T1 t1, T2 t2) -> decltype(t1 + t2){   returnt1+t2;}auto v = compose(2, 3.14); // v's type is double

 在那些情况下要申明类型
定义变量类型
函数返回值,函数参数
表达式返回变量类型

auto关键字原理
     在定义变量的时候必须申明类型,c++是强语言类型,在编译阶段需要知道类型,这样的好处是程序效率更高,而动态语言不需要类型申明的需要自推导变量类型。使用了auto是不是c++效率会变慢?完全不是,因为在编译阶段编译器已经帮程序员推导好了变量的类型。前提条件是编译器可以根据当前的程序的状态推导出变量类型。只是编译器更加智能,我猜想编译时间岂不更长了。

auto关键字使用场景

场景一,申明普通变量

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. auto num = 10;   

场景二,函数返回值,如果没有auto,总是要巧那么长的返回值,有了auto关键字后,是不是心里感觉很爽。

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. vector<string> str_vec;  
  2. vector<string>::iterator it_old = str_vec.begin();  
  3. auto it_new  = str_vec.begin();  

场景三,申明函数返回值
[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. auto add(int a, int b)->int {  
  2.      return a + b;  
  3. }  
auto使用需要注意的几个问题
1、使用auto关键字的变量必须有初始值,编译器才能推导出变量类型。

2、在传递const变量的时候,使用auto必须自己加const

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
  1. const int a =10;  
  2. auto b = a;                // b的类型是int  
  3. const auto c = a;      // c 的类型是const int  

3、关于很奇葩的函数返回值
auto关键字只能在函数的返回值使用auto关键字,并且在函数尾还需要标注返回类型
这样看起来很奇葩,莫不如不用auto关键字,但我认为委员会的人没这么闲,上google查了下,有下面两条原因:
1、他们认为把返回值放在函数后面更好看(特别是返回值很长的时候)

2、C++14将会把讨人烦得返回值去掉,编译器直接支持自推导。



nullptr

以前都是用0来表示空指针的,但由于0可以被隐式类型转换为整形,这就会存在一些问题。关键字nullptr是std::nullptr_t类型的值,用来指代空指针。nullptr和任何指针类型以及类成员指针类型的空值之间可以发生隐式类型转换,同样也可以隐式转换为bool型(取值为false)。但是不存在到整形的隐式类型转换。

 从1972年C语言刚刚诞生以来,常数0就扮演着整数(int)0和空指针( null pointer )两种角色。为了避免理解上的二义性,C语言通常使用NULL宏来表示空指针,NULL宏通常被定义为(void *)0或0, 而C++仅仅采用0来表示空指针,这样存在一个问题:比如对于重载函数 fun(char *) 和 fun(int) 的调用来说,若直接用NULL作为参数调用fun(NULL),我们可能认为NULL作为空指针的表示,应该调用 fun(char *) 函数,然而NULL实际上的值为0,就会调用  fun(int) 。 nullptr 关键字正是为了解决这一问题而产生的。
nullptr 能够转换成任何指针类型(包括成员函数指针和成员变量指针)和bool类型(这是为了兼容普通指针都能使用 if(ptr) 判断是否为空指针的形式),但是不能被转换为整数0。

#include "stdafx.h"#include <iostream>using namespace std;class MyClass{public:static void fun(int i){printf("1111");}static void fun(void *p){printf("2222");}static void fun(bool b){//默认MyClass::fun(NULL)会调用这个printf("33333");}private:};int _tmain(int argc, _TCHAR* argv[]){//MyClass::fun(NULL)可以调用三个静态函数中的任何一个,如果三者都存在,首先调用static void fun(int i)//MyClass::fun(NULL);//而MyClass::fun(nullptr)只能调用static void fun(void *p),如果不存在此函数,将编译不过MyClass::fun(nullptr);return 0;}
1. 引入nullptr的原因
引入nullptr的原因,这个要从NULL说起。对于C和C++程序员来说,一定不会对NULL感到陌生。但是C和C++中的NULL却不等价。NULL表示指针不指向任何对象,但是问题在于,NULL不是关键字,而只是一个宏定义(macro)。

1.1 NULL在C中的定义
在C中,习惯将NULL定义为void*指针值0:
[cpp]
#define NULL (void*)0 
#define NULL (void*)0但同时,也允许将NULL定义为整常数0

1.2 NULL在C++中的定义
在C++中,NULL却被明确定义为整常数0:
[cpp] 
// lmcons.h中定义NULL的源码  
#ifndef NULL  
#ifdef __cplusplus  
#define NULL    0  
#else  
#define NULL    ((void *)0)  
#endif  
#endif 

根本原因和C++的重载函数有关。C++通过搜索匹配参数的机制,试图找到最佳匹配(best-match)的函数,而如果继续支持void*的隐式类型转换,则会带来语义二义性(syntax ambiguous)的问题。
下面分析并测试了libcxx中 nullptr_t 的实现,对于转换为bool型别的调用还有些困惑。

#include <iostream>  using std::cout;using std::endl;namespace myname {struct nullptr_t{void * _;struct __nat { int __for_bool_; };nullptr_t(int __nat::*) {} // 构造函数,参数可以是0(空指针),或者&__nat::__for_bool_  operator int __nat::*() const { return 0; } // 不理解为什么转换为bool型时会调用此转换函数  // operator bool() const {return 0; /* or return false; */} // 为什么不这样实现?  template <class _Tp>operator _Tp* () const { return 0; } // 转换为非成员变量指针、非成员函数指针的普通指针  template <class _Tp, class _Up>operator _Tp _Up::* () const { return 0; } // 转换为成员变量指针或成员函数指针  friend bool operator==(nullptr_t, nullptr_t) { return true; }friend bool operator!=(nullptr_t, nullptr_t) { return false; }friend bool operator<(nullptr_t, nullptr_t) { return false; }friend bool operator<=(nullptr_t, nullptr_t) { return true; }friend bool operator>(nullptr_t, nullptr_t) { return false; }friend bool operator>=(nullptr_t, nullptr_t) { return true; }};struct Test {};}using namespace myname;inline nullptr_t __get_nullptr_t() { return nullptr_t(0); }#define nullptr __get_nullptr_t()  int main(int argc, char *argv[]){char *pch = nullptr;    // ok, __get_nullptr_t() => nullptr_t::nullptr_t(0) => template <class _Tp> nullptr_t::operator _Tp* () const  int *pint = nullptr;    // ok, __get_nullptr_t() => nullptr_t::nullptr_t(0) => template <class _Tp> nullptr_t::operator _Tp* () const  bool    b = nullptr;    // ok, __get_nullptr_t() => nullptr_t::nullptr_t(0) => nullptr_t::operator int __nat::*() const  //int     n = nullptr;  // error: cannot convert 'myname::nullptr_t' to 'int' in initialization  int Test::*p = nullptr; // ok, __get_nullptr_t() => nullptr_t::nullptr_t(0) => template <class _Tp, class _Up> operator _Tp _Up::* () const  void (Test::*pfn)(int, char) = nullptr; // ok, __get_nullptr_t() => nullptr_t::nullptr_t(0) => template <class _Tp, class _Up> operator _Tp _Up::* () const  return 0;}


Range-based for loops (基于范围的for循环)

为了在遍历容器时支持”foreach”用法,C++11扩展了for语句的语法。用这个新的写法,可以遍历C类型的数组、初始化列表以及任何重载了非成员的begin()和end()函数的类型。

如果你只是想对集合或数组的每个元素做一些操作,而不关心下标、迭代器位置或者元素个数,那么这种foreach的for循环将会非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
std::map<std::string, std::vector<int>> map;
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
map["one"] = v;
 
for(constauto& kvp : map)
{
  std::cout << kvp.first << std::endl;
 
  for(auto v : kvp.second)
  {
     std::cout << v << std::endl;
  }
}
 
intarr[] = {1,2,3,4,5};
for(int& e : arr)
{
  e = e*e;
}

C++ 11 CPP 11 features

 

Override和final

我总觉得 C++中虚函数的设计很差劲,因为时至今日仍然没有一个强制的机制来标识虚函数会在派生类里被改写。vitual关键字是可选的,这使得阅读代码变得很费劲。因为可能需要追溯到继承体系的源头才能确定某个方法是否是虚函数。为了增加可读性,我总是在派生类里也写上virtual关键字,并且也鼓励大家都这么做。即使这样,仍然会产生一些微妙的错误。看下面这个例子:

classB{public:   virtual void f(short) {std::cout << "B::f"<< std::endl;}}; classD : publicB{public:   virtual void f(int) {std::cout << "D::f"<< std::endl;}};

D::f 按理应当重写 B::f。然而二者的声明是不同的,一个参数是short,另一个是int。因此D::f(原文为B::f,可能是作者笔误——译者注)只是拥有同样名字的另一个函数(重载)而不是重写。当你通过B类型的指针调用f()可能会期望打印出D::f,但实际上则会打出 B::f 。

另一个很微妙的错误情况:参数相同,但是基类的函数是const的,派生类的函数却不是。

classB{public:   virtual void f(int) const{std::cout << "B::f " << std::endl;}}; classD : publicB{public:   virtual void f(int) {std::cout << "D::f"<< std::endl;}};

同样,这两个函数是重载而不是重写,所以你通过B类型指针调用f()将打印B::f,而不是D::f。

幸运的是,现在有一种方式能描述你的意图。新标准加入了两个新的标识符(不是关键字)::

  1. override,表示函数应当重写基类中的虚函数。
  2. final,表示派生类不应当重写这个虚函数。

第一个的例子如下:

classB{public:   virtual void f(short) {std::cout << "B::f"<< std::endl;}}; classD : publicB{public:   virtual void f(int) override {std::cout << "D::f"<< std::endl;}};

现在这将触发一个编译错误(后面那个例子,如果也写上override标识,会得到相同的错误提示):

'D::f' : method with override specifier 'override' did not override any base class methods

另一方面,如果你希望函数不要再被派生类进一步重写,你可以把它标识为final。可以在基类或任何派生类中使用final。在派生类中,可以同时使用override和final标识。

classB{public:   virtual void f(int) {std::cout << "B::f"<< std::endl;}}; classD : publicB{public:   virtual void f(int) override final {std::cout << "D::f"<< std::endl;}}; classF : publicD{public:   virtual void f(int) override {std::cout << "F::f"<< std::endl;}};

被标记成final的函数将不能再被F::f重写。

 

Strongly-typed enums 强类型枚举

传统的C++枚举类型存在一些缺陷:它们会将枚举常量暴露在外层作用域中(这可能导致名字冲突,如果同一个作用域中存在两个不同的枚举类型,但是具有相同的枚举常量就会冲突),而且它们会被隐式转换为整形,无法拥有特定的用户定义类型。

在C++11中通过引入了一个称为强类型枚举的新类型,修正了这种情况。强类型枚举由关键字enum class标识。它不会将枚举常量暴露到外层作用域中,也不会隐式转换为整形,并且拥有用户指定的特定类型(传统枚举也增加了这个性质)。

enum class Options {None, One, All};Options o = Options::All;

Smart Pointers 智能指针

已经有成千上万的文章讨论这个问题了,所以我只想说:现在能使用的,带引用计数,并且能自动释放内存的智能指针包括以下几种:

  • unique_ptr: 如果内存资源的所有权不需要共享,就应当使用这个(它没有拷贝构造函数),但是它可以转让给另一个unique_ptr(存在move构造函数)。
  • shared_ptr:  如果内存资源需要共享,那么使用这个(所以叫这个名字)。
  • weak_ptr: 持有被shared_ptr所管理对象的引用,但是不会改变引用计数值。它被用来打破依赖循环(想象在一个tree结构中,父节点通过一个共享所有权的引用(chared_ptr)引用子节点,同时子节点又必须持有父节点的引用。如果这第二个引用也共享所有权,就会导致一个循环,最终两个节点内存都无法释放)。

 

另一方面,auto_ptr已经被废弃,不会再使用了。

什么时候使用unique_ptr,什么时候使用shared_ptr取决于对所有权的需求,我建议阅读以下的讨论:http://stackoverflow.com/questions/15648844/using-smart-pointers-for-class-members

以下第一个例子使用了unique_ptr。如果你想把对象所有权转移给另一个unique_ptr,需要使用std::move(我会在最后几段讨论这个函数)。在所有权转移后,交出所有权的智能指针将为空,get()函数将返回nullptr。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
voidfoo(int* p)
{
std::cout << *p << std::endl;
}
std::unique_ptr<int> p1(newint(42));
std::unique_ptr<int> p2 = std::move(p1); // transfer ownership
 
if(p1)
foo(p1.get());
 
(*p2)++;
 
if(p2)
foo(p2.get());

第二个例子展示了shared_ptr。用法相似,但语义不同,此时所有权是共享的。

1
2
3
4
5
6
7
8
9
10
11
12
voidfoo(int* p)
{
}
voidbar(std::shared_ptr<int> p)
{
++(*p);
}
std::shared_ptr<int> p1(newint(42));
std::shared_ptr<int> p2 = p1;
 
bar(p1);
foo(p2.get());

第一个声明和以下这行是等价的:

1
auto p3 = std::make_shared<int>(42);

make_shared<T>是一个非成员函数,使用它的好处是可以一次性分配共享对象和智能指针自身的内存。而显示地使用shared_ptr构造函数来构造则至少需要两次内存分配。除了会产生额外的开销,还可能会导致内存泄漏。在下面这个例子中,如果seed()抛出一个错误就会产生内存泄漏。

1
2
3
4
5
voidfoo(std::shared_ptr<int> p, intinit)
{
*p = init;
}
foo(std::shared_ptr<int>(newint(42)), seed());

如果使用make_shared就不会有这个问题了。第三个例子展示了weak_ptr。注意,你必须调用lock()来获得被引用对象的shared_ptr,通过它才能访问这个对象。

1
2
3
4
5
6
7
8
9
10
11
12
auto p = std::make_shared<int>(42);
std::weak_ptr<int> wp = p;
 
{
auto sp = wp.lock();
std::cout << *sp << std::endl;
}
 
p.reset();
 
if(wp.expired())
std::cout << "expired"<< std::endl;

如果你试图锁定(lock)一个过期(指被弱引用对象已经被释放)的weak_ptr,那你将获得一个空的shared_ptr.

 

Lambdas

匿名函数(也叫lambda)已经加入到C++中,并很快异军突起。这个从函数式编程中借来的强大特性,使很多其他特性以及类库得以实现。你可以在任何使用函数对象或者函子(functor)或std::function的地方使用lambda。你可以从这里(http://msdn.microsoft.com/en-us/library/dd293603.aspx)找到语法说明。

1
2
3
4
5
6
7
8
9
10
11
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
 
std::for_each(std::begin(v), std::end(v), [](intn) {std::cout << n << std::endl;});
 
auto is_odd = [](intn) {returnn%2==1;};
auto pos = std::find_if(std::begin(v), std::end(v), is_odd);
if(pos != std::end(v))
std::cout << *pos << std::endl;

更复杂的是递归lambda。考虑一个实现Fibonacci函数的lambda。如果你试图用auto来声明,就会得到一个编译错误。

1
auto fib = [&fib](intn) {returnn < 2 ? 1 : fib(n-1) + fib(n-2);};
1
2
3
4
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

问题出在auto意味着对象类型由初始表达式决定,然而初始表达式又包含了对其自身的引用,因此要求先知道它的类型,这就导致了无穷递归。解决问题的关键就是打破这种循环依赖,用std::function显式的指定函数类型:

1
std::function<int(int)> lfib = [&lfib](intn) {returnn < 2 ? 1 : lfib(n-1) + lfib(n-2);};

 

非成员begin()和end()

也许你注意到了,我在前面的例子中已经用到了非成员begin()和end()函数。他们是新加入标准库的,除了能提高了代码一致性,还有助于更多地使用泛型编程。它们和所有的STL容器兼容。更重要的是,他们是可重载的。所以它们可以被扩展到支持任何类型。对C类型数组的重载已经包含在标准库中了。

我们还用上一个例子中的代码来说明,在这个例子中我打印了一个数组然后查找它的第一个偶数元素。如果std::vector被替换成C类型数组。代码可能看起来是这样的:

1
2
3
4
5
6
7
8
9
intarr[] = {1,2,3};
std::for_each(&arr[0], &arr[0]+sizeof(arr)/sizeof(arr[0]), [](intn) {std::cout << n << std::endl;});
 
auto is_odd = [](intn) {returnn%2==1;};
auto begin = &arr[0];
auto end = &arr[0]+sizeof(arr)/sizeof(arr[0]);
auto pos = std::find_if(begin, end, is_odd);
if(pos != end)
std::cout << *pos << std::endl;

如果使用非成员的begin()和end()来实现,就会是以下这样的:

1
2
3
4
5
6
7
intarr[] = {1,2,3};
std::for_each(std::begin(arr), std::end(arr), [](intn) {std::cout << n << std::endl;});
 
auto is_odd = [](intn) {returnn%2==1;};
auto pos = std::find_if(std::begin(arr), std::end(arr), is_odd);
if(pos != std::end(arr))
std::cout << *pos << std::endl;

这基本上和使用std::vecto的代码是完全一样的。这就意味着我们可以写一个泛型函数处理所有支持begin()和end()的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
template<typenameIterator>
voidbar(Iterator begin, Iterator end)
{
std::for_each(begin, end, [](intn) {std::cout << n << std::endl;});
 
auto is_odd = [](intn) {returnn%2==1;};
auto pos = std::find_if(begin, end, is_odd);
if(pos != end)
std::cout << *pos << std::endl;
}
 
template<typenameC>
voidfoo(C c)
{
bar(std::begin(c), std::end(c));
}
 
template<typenameT,size_tN>
voidfoo(T(&arr)[N])
{
bar(std::begin(arr), std::end(arr));
}
 
intarr[] = {1,2,3};
foo(arr);
 
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
foo(v);

 

static_assert和 type traits

static_assert提供一个编译时的断言检查。如果断言为真,什么也不会发生。如果断言为假,编译器会打印一个特殊的错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typenameT,size_tSize>
classVector
{
   static_assert(Size < 3, "Size is too small");
   T _points[Size];
};
 
intmain()
{
   Vector<int, 16> a1;
   Vector<double, 2> a2;
   return0;
}
1
2
3
4
5
6
7
error C2338: Size is too small
see reference to class template instantiation 'Vector<T,Size>' being compiled
   with
   [
      T=double,
      Size=2
   ]

static_assert和type traits一起使用能发挥更大的威力。type traits是一些class,在编译时提供关于类型的信息。在头文件<type_traits>中可以找到它们。这个头文件中有好几种class: helper class,用来产生编译时常量。type traits class,用来在编译时获取类型信息,还有就是type transformation class,他们可以将已存在的类型变换为新的类型。

 

下面这段代码原本期望只做用于整数类型。

1
2
3
4
5
template<typenameT1,typenameT2>
auto add(T1 t1, T2 t2) -> decltype(t1 + t2)
{
returnt1 + t2;
}

但是如果有人写出如下代码,编译器并不会报错

1
2
std::cout << add(1, 3.14) << std::endl;
std::cout << add("one", 2) << std::endl;

程序会打印出4.14和”e”。但是如果我们加上编译时断言,那么以上两行将产生编译错误。

1
2
3
4
5
6
7
8
template<typenameT1,typenameT2>
auto add(T1 t1, T2 t2) -> decltype(t1 + t2)
{
   static_assert(std::is_integral<T1>::value,"Type T1 must be integral");
   static_assert(std::is_integral<T2>::value,"Type T2 must be integral");
 
   returnt1 + t2;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
   ]

 

Move semantics (Move语义)

这是C++11中所涵盖的另一个重要话题。就这个话题可以写出一系列文章,仅用一个段落来说明显然是不够的。因此在这里我不会过多的深入细节,如果你还不是很熟悉这个话题,我鼓励你去阅读更多地资料。

C++11加入了右值引用(rvalue reference)的概念(用&&标识),用来区分对左值和右值的引用。左值就是一个有名字的对象,而右值则是一个无名对象(临时对象)。move语义允许修改右值(以前右值被看作是不可修改的,等同于const T&类型)。

C++的class或者struct以前都有一些隐含的成员函数:默认构造函数(仅当没有显示定义任何其他构造函数时才存在),拷贝构造函数,析构函数还有拷贝赋值操作符。拷贝构造函数和拷贝赋值操作符提供bit-wise的拷贝(浅拷贝),也就是逐个bit拷贝对象。也就是说,如果你有一个类包含指向其他对象的指针,拷贝时只会拷贝指针的值而不会管指向的对象。在某些情况下这种做法是没问题的,但在很多情况下,实际上你需要的是深拷贝,也就是说你希望拷贝指针所指向的对象。而不是拷贝指针的值。这种情况下,你需要显示地提供拷贝构造函数与拷贝赋值操作符来进行深拷贝。

如果你用来初始化或拷贝的源对象是个右值(临时对象)会怎么样呢?你仍然需要拷贝它的值,但随后很快右值就会被释放。这意味着产生了额外的操作开销,包括原本并不需要的空间分配以及内存拷贝。

现在说说move constructor和move assignment operator。这两个函数接收T&&类型的参数,也就是一个右值。在这种情况下,它们可以修改右值对象,例如“偷走”它们内部指针所指向的对象。举个例子,一个容器的实现(例如vector或者queue)可能包含一个指向元素数组的指针。当用一个临时对象初始化一个对象时,我们不需要分配另一个数组,从临时对象中把值复制过来,然后在临时对象析构时释放它的内存。我们只需要将指向数组内存的指针值复制过来,由此节约了一次内存分配,一次元数组的复制以及后来的内存释放。

以下代码实现了一个简易的buffer。这个buffer有一个成员记录buffer名称(为了便于以下的说明),一个指针(封装在unique_ptr中)指向元素为T类型的数组,还有一个记录数组长度的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
template<typenameT>
classBuffer
{
   std::string          _name;
   size_t              _size;
   std::unique_ptr<T[]> _buffer;
 
public:
   // default constructor
   Buffer():
      _size(16),
      _buffer(newT[16])
   {}
 
   // constructor
   Buffer(conststd::string& name, size_tsize):
      _name(name),
      _size(size),
      _buffer(newT[size])
   {}
 
   // copy constructor
   Buffer(constBuffer& copy):
      _name(copy._name),
      _size(copy._size),
      _buffer(newT[copy._size])
   {
      T* source = copy._buffer.get();
      T* dest = _buffer.get();
      std::copy(source, source + copy._size, dest);
   }
 
   // copy assignment operator
   Buffer& operator=(constBuffer& copy)
   {
      if(this!= ©)
      {
         _name = copy._name;
 
         if(_size != copy._size)
         {
            _buffer = nullptr;
            _size = copy._size;
            _buffer = _size > 0 > newT[_size] : nullptr;
         }
 
         T* source = copy._buffer.get();
         T* dest = _buffer.get();
         std::copy(source, source + copy._size, dest);
      }
 
      return*this;
   }
 
   // move constructor
   Buffer(Buffer&& temp):
      _name(std::move(temp._name)),
      _size(temp._size),
      _buffer(std::move(temp._buffer))
   {
      temp._buffer = nullptr;
      temp._size = 0;
   }
 
   // move assignment operator
   Buffer& operator=(Buffer&& temp)
   {
      assert(this!= &temp); // assert if this is not a temporary
 
      _buffer = nullptr;
      _size = temp._size;
      _buffer = std::move(temp._buffer);
 
      _name = std::move(temp._name);
 
      temp._buffer = nullptr;
      temp._size = 0;
 
      return*this;
   }
};
 
template<typenameT>
Buffer<T> getBuffer(conststd::string& name)
{
   Buffer<T> b(name, 128);
   returnb;
}
intmain()
{
   Buffer<int> b1;
   Buffer<int> b2("buf2", 64);
   Buffer<int> b3 = b2;
   Buffer<int> b4 = getBuffer<int>("buf4");
   b1 = getBuffer<int>("buf5");
   return0;
}

默认的copy constructor以及copy assignment operator大家应该很熟悉了。C++11中新增的是move constructor以及move assignment operator,这两个函数根据上文所描述的move语义实现。如果你运行这段代码,你就会发现b4构造时,move constructor会被调用。同样,对b1赋值时,move assignment operator会被调用。原因就在于getBuffer()的返回值是一个临时对象——也就是右值。

你也许注意到了,move constuctor中当我们初始化变量name和指向buffer的指针时,我们使用了std::move。name实际上是一个string,std::string实现了move语义。std::unique_ptr也一样。但是如果我们写_name(temp._name),那么copy constructor将会被调用。不过对于_buffer来说不能这么写,因为std::unique_ptr没有copy constructor。但为什么std::string的move constructor此时没有被调到呢?这是因为虽然我们使用一个右值调用了Buffer的move constructor,但在这个构造函数内,它实际上是个左值。为什么?因为它是有名字的——“temp”。一个有名字的对象就是左值。为了再把它变为右值(以便调用move constructor)必须使用std::move。这个函数仅仅是把一个左值引用变为一个右值引用。

更新:虽然这个例子是为了说明如何实现move constructor以及move assignment operator,但具体的实现方式并不是唯一的。在本文的回复中Member 7805758同学提供了另一种可能的实现。为了方便查看,我把它也列在下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
template<typenameT>
classBuffer
{
   std::string          _name;
   size_t              _size;
   std::unique_ptr<T[]> _buffer;
 
public:
   // constructor
   Buffer(conststd::string& name = "",size_tsize = 16):
      _name(name),
      _size(size),
      _buffer(size?newT[size] : nullptr)
   {}
 
   // copy constructor
   Buffer(constBuffer& copy):
      _name(copy._name),
      _size(copy._size),
      _buffer(copy._size?newT[copy._size] : nullptr)
   {
      T* source = copy._buffer.get();
      T* dest = _buffer.get();
      std::copy(source, source + copy._size, dest);
   }
 
   // copy assignment operator
   Buffer& operator=(Buffer copy)
   {
       swap(*this, copy);
       return*this;
   }
 
   // move constructor
   Buffer(Buffer&& temp):Buffer()
   {
      swap(*this, temp);
   }
 
   friendvoidswap(Buffer& first, Buffer& second) noexcept
   {
       usingstd::swap;
       swap(first._name  , second._name);
       swap(first._size  , second._size);
       swap(first._buffer, second._buffer);
   }
};

 

结论

关于C++11还有很多要说的。本文只是各种入门介绍中的一个。本文展示了一系列C++开发者应当使用的核心语言特性与标准库函数。然而我建议你能更加深入地学习,至少也要再看看本文所介绍的特性中的部分。

0 0
原创粉丝点击