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

Posted 草上爬

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了每个C++开发者都应该使用的十个C++11特性相关的知识,希望对你有一定的参考价值。

这篇文章讨论了一系列所有开发者都应该学习和使用的C++11特性,在新的C++标准中,语言和标准库都加入了很多新属性,这篇文章只会介绍一些皮毛,然而,我相信有一些特征用法应该会成为C++开发者的日常用法之一。你也许已经找到很多类似介绍C++11标准特征的文章,这篇文章可以看成是那些常用特征描述的一个集合。

目录:

  • auto关键字
  • nullptr关键字
  • 基于区间的循环
  • Override和final
  • 强类型枚举
  • 智能指针
  • Lambdas表达式
  • 非成员begin()和end()
  • static_assert宏和类型萃取器
  • 移动语义

auto关键字

在C++11标准之前,auto关键字就被用来标识临时变量语义,在新的标准中,它的目的变成了另外两种用途。auto现在是一种类型占位符,它会告诉编译器,应该从初始化式中推断出变量的实际类型。当你想在不同的作用域中(例如,命名空间、函数内、for循环中中的初始化式)声明变量的时候,auto可以在这些场合使用。

auto i = 42;        // i is an int
auto l = 42LL;      // l is an long long
auto p = new foo(); // p is a foo*

使用auto经常意味着较少的代码量(除非你需要的类型是int这种只有一个单词的)。当你想要遍历STL容器中元素的时候,想一想你会怎么写迭代器代码,老式的方法是用很多typedef来做,而auto则会大大简化这个过程。

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

你应该注意到,auto并不能作为函数的返回类型,但是你能用auto去代替函数的返回类型,当然,在这种情况下,函数必须有返回值才可以。auto不会告诉编译器去推断返回值的实际类型,它会通知编译器在函数的末段去寻找返回值类型。在下面的那个例子中,函数返回值的构成是由T1类型和T2类型的值,经过+操作符之后决定的。

template <typename T1, typename T2>
auto compose(T1 t1, T2 t2) -> decltype(t1 + t2)
{
   return t1+t2;
}
auto v = compose(2, 3.14); // v‘s type is double

nullptr关键字

0曾经是空指针的值,这种方式有一些弊端,因为它可以被隐式转换成整型变量。nullptr关键字代表值类型std::nullptr_t,在语义上可以被理解为空指针。nullptr可被隐式转换成任何类型的空指针,以及成员函数指针和成员变量指针,而且也可以转换为bool(值为false),但是隐式转换到整型变量的情况不再存在了。

void foo(int* p) {}

void bar(std::shared_ptr<int> p) {}

int* p1 = NULL;
int* p2 = nullptr;   
if(p1 == p2)
{
}

foo(nullptr);
bar(nullptr);

bool f = nullptr;
int i = nullptr; // error: A native nullptr can only be converted to bool or, using reinterpret_cast, to an integral type

为了向下兼容,0仍可作为空指针的值来使用。

基于区间的循环

C++11加强了for语句的功能,以更好的支持用于遍历集合的“foreach”范式。在新的形式中,用户可以使用for去迭代遍历C风格的数组、初始化列表,以及所有非成员begin()和end被重载的容器。

当你仅仅想获取集合/数组中的元素来做一些事情,而不关注索引值、迭代器或者元素本身的时候,这种for的形式非常有用。

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(const auto& kvp : map) 
{
  std::cout << kvp.first << std::endl;

  for(auto v : kvp.second)
  {
     std::cout << v << std::endl;
  }
}

int arr[] = {1,2,3,4,5};
for(int& e : arr) 
{
  e = e*e;
}

Override和final

我经常会发现虚函数在C++中会引起很多问题,因为没有一个强制的机制来标识虚函数在派生类中被重写了。virtual关键字并不是强制性的,这给代码的阅读增加了一些困难,因为你可能不得不去看继承关系的最顶层以确认这个方法是不是虚方法。我自己经常鼓励开发者在派生类中使用virtual关键字,我自己也是这么做的,这可以让代码更易读。然而,有一些不明显的错误仍然会出现,下面这段代码就是个例子。

class B 
{
public:
   virtual void f(short) {std::cout << "B::f" << std::endl;}
};

class D : public B
{
public:
   virtual void f(int) {std::cout << "D::f" << std::endl;}
};

D::f本应该重写B::f,但是这两个函数的签名并不相同,一个参数是short,另一个则是int,因此,B::f仅仅是另外一个和D::f命名相同的函数,是重载而不是重写。你有可能会通过B类型的指针调用f(),并且期盼输出D::f的结果,但是打印出来的结果却是B::f。

这里还有另外一个不明显的错误:参数是相同的,但是在基类中的函数是const成员函数,而在派生类中则不是。

class B 
{
public:
   virtual void f(int) const {std::cout << "B::f " << std::endl;}
};

class D : public B
{
public:
   virtual void f(int) {std::cout << "D::f" << std::endl;}
};

又一次,这两个函数的关系是重载而非重写,因此,如果你想通过B类型的指针来调用f(),程序会打印出B::f,而不是D::f。

幸运的是,有一种方法可以来描述你的意图,两个新的、专门的标识符(不是关键字)添加进了C++11中:override,可以指定在基类中的虚函数应该被重写;final,可以用来指定派生类中的函数不会重写基类中的虚函数。第一个例子会变成:

class B 
{
public:
   virtual void f(short) {std::cout << "B::f" << std::endl;}
};

class D : public B
{
public:
   virtual void f(int) override {std::cout << "D::f" << std::endl;}
};

这段代码会触发一个编译错误(如果你使用override标识符尝试第二个例子,也会得到相同的错误。):

‘D::f‘: 有override标识符的函数并没有重写任何基类函数

另一方面,如果你想要一个函数永远不能被重写(顺着继承层次往下都不能被重写),你可以把该函数标识为final,在基类中和派生类中都可以这么做。如果实在派生类中,你可以同时使用override和final标识符。

class B 
{
public:
   virtual void f(int) {std::cout << "B::f" << std::endl;}
};

class D : public B
{
public:
   virtual void f(int) override final {std::cout << "D::f" << std::endl;}
};

class F : public D
{
public:
   virtual void f(int) override {std::cout << "F::f" << std::endl;}
};

用‘final‘声明的函数不能被‘F::f‘重写。

强类型枚举

“传统”的C++枚举类型有一些缺点:它会在一个代码区间中抛出枚举类型成员(如果在相同的代码域中的两个枚举类型具有相同名字的枚举成员,这会导致命名冲突),它们会被隐式转换为整型,并且不可以指定枚举的底层数据类型。

通过引入一种新的枚举类型,这些问题在C++11中被解决了,这种新的枚举类型叫做强类型枚举。这种类型用enum class关键字来标识,它永远不会在代码域中抛出枚举成员,也不会隐式的转换为整形,同时还可以具有用户指定的底层类型(这个特征也被加入了传统枚举类型中)。

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

智能指针

有大量的文章介绍过智能指针,因此,我仅仅想提一提智能指针的引用计数和内存自动释放相关的东西:

  • unique_ptr:当一块内存的所有权并不是共享的时候(它并不具有拷贝构造函数),可以使用,但是,它可以被转换为另外一个unique_ptr(具有移动构造函数)。

  • shared_ptr:当一块内存的所有权可以被共享的时候,可以使用(这就是为什么它叫这个名)。

  • weak_ptr:具有一个shared_ptr管理的指向一个实体对象的引用,但是并没有做任何引用计数的工作,它被用来打破循环引用关系(想象一个关系树,父节点拥有指向子节点的引用(shared_ptr),但是子节点也必须持有指向父节点的引用;如果第二个引用也是一个独立的引用,一个循环就产生了,这会导致任何对象都永远无法释放)。

换句话说,auto_ptr已经过时了,应该不再被使用了。

什么时候该使用unique_ptr,什么时候该使用shared_ptr,取决于程序对内存所有权的需求,我推荐你读一读这里的讨论

下面第一个例子演示了unique_ptr的用法,如果你想要把对象的控制权转交给另一个unique_ptr,请使用std::move(我将会在最后一段讨论这个函数)。在控制权交接后,让出控制权的智能指针会变成null,如果调用get(),会返回nullptr。

void foo(int* p)
{
   std::cout << *p << std::endl;
}
std::unique_ptr<int> p1(new int(42));
std::unique_ptr<int> p2 = std::move(p1); // transfer ownership

if(p1)
  foo(p1.get());

(*p2)++;

if(p2)
  foo(p2.get());

第二个例子演示了shared_ptr的用法。尽管语义不同,因为所有权是共享的,但用法都差不多。

void foo(int* p)
{
}
void bar(std::shared_ptr<int> p)
{
   ++(*p);
}
std::shared_ptr<int> p1(new int(42));
std::shared_ptr<int> p2 = p1;

bar(p1);   
foo(p2.get());

第一个声明等价于这个。

auto p3 =

以上是关于每个C++开发者都应该使用的十个C++11特性的主要内容,如果未能解决你的问题,请参考以下文章

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

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

34.JS 开发者必须知道的十个 ES6 新特性

JavaScript 开发者都应该知道的十个概念

Ruby 2.5 的十个新特性

每个工程师都应该了解的一些 C++ 特性