本篇博客主要总结一些常用的C++11的新特性。

学习C++11的新特性

auto

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

1
2
3
4
auto x = 1; //int 类型
auto y = "hello"; //字符串类型
auto z = y; //字符串类型
auto k; //编译出错,使用auto时必须进行初始化

使用auto关键字来声明,但是不立即对其进行定义,编译器则会报错。auto声明的变量必须被初始化,以使编译器能够从其初始化表达式中推导出其类型。这个意义上,auto并非一种类型声明,而是一个类型声明时的“占位符”,编译器在编译时会将suto替代为变量实际的类型。

优势:

  1. 代码更简洁

    1
    2
    std::vector<std::string>::iterator i=vs.begin();
    auto i = vs.begin();
  2. 避免类型声明错误, 减少隐式转换过程中发生的错误

  3. 更好地支持泛型编程, 改动类的定义比如函数的返回值类型,不会影响到其他的代码

注意事项:

  1. 使用auto必须进行初始化,否则编译出错。
  2. 可以使用 const, volatile, *, **修饰
  3. 函数或模板参数不能为 auto 类型
  4. 定义在堆上的变量,必须初始化,auto x = new auto(9);
  5. 因为auto为一个占位符,不能使用sizeof,typeid
  6. 定义一个auto序列时,类型需要保持一直。auto x =1,y=0.0,c='c'; //error
  7. auto 会自动退化为指针,除非声明为引用。
    1
    2
    3
    int a[9];
    auto i = a; // i = int*
    auto& k = a; // k = int[9]

nullptr

NULL定义一般如下:

1
2
3
4
5
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif

即在 C++ 环境中, NULL被定义为0, 多数情况下,不会出现问题,但是当有重载或者模板推导时,编译器无法给出正确结果
如:

1
2
void call(void* data);
call(NULL); //传入的是0,被隐式转为int,无法正确编译通过

使用规范:
如果编译器支持 nullptr, 直接使用 nullptr 替代 NULL。
如果不支持,可以模拟实现:

1
2
3
4
5
6
7
8
9
10
const class nullptr_t_t
{
public:
template<class T> operator T*() const {return 0;}
template<class C,class T> operator T C::*() const {return 0;}
private:
void operator& () const;
} nullptr_t = {};
#undef NULL
#define NULL nullptr_t

nullptr可以和任何指针类型和类成员指针类型的空值之间进行隐式类型转换,也可以转为bool(false).

foreach

为了在遍历容器时支持”foreach”用法,C++11扩展了for语句的语法。

1
2
3
4
5
6
map<string, vector<int> > test;
for(const auto& k: test){
cout << k.first << endl;
for(auto v: k.second)
cout << v << endl;
}

如果你只是想对集合或数组的每个元素做一些操作,而不关心下标、迭代器位置或者元素个数,那么这种foreach的for循环将会非常有用。
可以遍历C类型的数组、初始化列表以及任何重载了非成员的begin()和end()函数的类型。

override and final

在C++中,使用 virtual 关键字用于实现多态机制,但是virtual关键字时可选的,无法指定虚函数会在派生类中重写。其实为了增加可读性,建议在派生类中加入 virtual 关键字。但是这样做依旧会产生一些错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;

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

class B: public A{
public:
virtual void f(double) {cout << "B::f()" << endl;} //参数类型不一致, 重载,而不是重写
//virtual void f(int) const {cout << "B::f()" << endl;} 也会产生相同的效果
};

int main() {
A *p;
B b;
p = &b;
p->f(1.1);
return 0;
}
/*
* 输出为 A::f()
*/

使用 overridefinal 标示符(非关键字),可以解决上述问题。
override: 标示函数应当重写基类的虚函数
final: 标示派生类不应重写几类的虚函数

1
2
3
4
5
6
7
8
9
class A{
public:
virtual void f(int) final {cout << "A::f()" << endl;}
};

class B: public A{
public:
virtual void f(int) override {cout << "B::f()" << endl;}//标示重写,如果参数不一致编译错误, 重写 final 标示的函数也会编译错误。
};

strongly-typed enums

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

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

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

smart pointer

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

80%的情况下,使用 shared_ptr 是正确的,剩下的20%情况使用的时 unique_ptr 和 weak_ptr, 如果某个对象只能被一个对象所拥有,随着该对象的destroy 也会destroy,那么可以使用 unique_ptr, weak_ptr 主要用于打破引用循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <memory>

class Device {
};

class Settings {
std::unique_ptr<Device> device;
public:
Settings(std::unique_ptr<Device> d) {
device = std::move(d);
}

Device* getDevice() {
return device.get();
}
};

int main() {
std::unique_ptr<Device> device(new Device());
Settings settings(std::move(device));
// ...
Device *myDevice = settings.getDevice();
// do something with myDevice...
}

lambda

编程中提到的 lambda 表达式,通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数。
编写代码时,尤其是使用 STL 算法时,可能会使用函数指针和函数对象来解决问题和执行计算。 函数指针和函数对象各有利弊。例如,函数指针具有最低的语法开销,但不保持范围内的状态,函数对象可保持状态,但需要类定义的语法开销。
lambda 结合了函数指针和函数对象的优点并避免其缺点。 lambda 与函数对象相似的是灵活并且可以保持状态,但不同的是其简洁的语法不需要显式类定义。 使用 lambda,你可以编写出比等效的函数对象代码更简洁、更不容易出错的代码。

语法:

  1. Capture 子句(在 C++ 规范中也称为 lambda 引导。)
    它指定要捕获的变量以及是通过值还是引用进行捕获。 有与号 (&) 前缀的变量通过引用访问,没有该前缀的变量通过值访问。可以使用默认捕获模式(标准语法中的 capture-default)来指示如何捕获 lambda 中引用的任何外部变量:[&] 表示通过引用捕获引用的所有变量,而 [=] 表示通过值捕获它们,使用 capture-default 时,只有 lambda 中提及的变量才会被捕获。可以使用默认捕获模式,然后为特定变量显式指定相反的模式。 例如,如果 lambda 体通过引用访问外部变量 total 并通过值访问外部变量 factor,则以下 capture 子句等效:

    1
    2
    3
    4
    5
    6
    [&total, factor]
    [factor, &total]
    [&, factor]
    [factor, &]
    [=, &total]
    [&total, =]
  2. 参数列表(可选)。 (也称为 lambda 声明符)

  3. 可变规范(可选)。
  4. 异常规范(可选)。
  5. 尾随返回类型(可选)。
    lambda 自动推导返回类型。 无需使用 auto 关键字,除非指定尾随返回类型。 trailing-return-type 类似于普通方法或函数的返回类型部分。 但是,返回类型必须跟在参数列表的后面,你必须在返回类型前面包含 trailing-return-type 关键字 ->。
    如果 lambda 体仅包含一个返回语句或其表达式不返回值,则可以省略 lambda 表达式的返回类型部分。 如果 lambda 体包含单个返回语句,编译器将从返回表达式的类型推导返回类型。 否则,编译器会将返回类型推导为 void。 下面的代码示例片段说明了这一原则。

    1
    2
    3
    auto x1 = [](int i){ return i; }; // OK: return type is int
    auto x2 = []{ return{ 1, 2 }; }; // ERROR: return type is void, deducing
    // return type from braced-init-list is not valid
  6. “lambda 体”
    lambda 表达式的 lambda 体(标准语法中的 compound-statement)可包含普通方法或函数的主体可包含的任何内容。普通函数和 lambda 表达式的主体均可访问以下变量类型:

  • 从封闭范围捕获变量,如前所述。
  • 参数
  • 本地声明变量
  • 类数据成员(在类内部声明并且捕获 this 时)
  • 具有静态存储持续时间的任何变量(例如,全局变量)
1
2
3
4
5
6
7
8
9
for_each(v.begin(), v.end(), [&evenCount] (int n) {
cout << n;
if (n % 2 == 0) {
cout << " is even " << endl;
++evenCount;
} else {
cout << " is odd " << endl;
}
});

begin, end

非成员begin() 和 end() 和所有的STL容器兼容。更重要的是,他们是可重载的。所以它们可以被扩展到支持任何类型。对C类型数组的重载已经包含在标准库中了。

1
2
3
4
5
std::for_each(std::begin(array), std::end(array), [](int n){std::cout << n << std::endl;})

auto is_odd = [](int n){return n%2 == 1;};
auto begin = std::begin(array);
auto end = std::end(array);

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

static_assert and type traits

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

1
2
3
4
5
static_assert(constant-expression, string-literal);
/*
constant-expression: 可以转换为布尔值的整型常量表达式。如果计算出的表达式为零 (false),则显示 string-literal 参数,并且编译因出错而失败。 如果表达式不为零 (true),则 static_assert 声明无效。
string-literal: 当constant-expression 参数为false时显示的消息。 该消息是编译器的基本字符集中的一个字符串;即,不是多字节或宽字符。
*/

示例:

1
2
3
const int size=6;
static_assert(size < 5, "Size is big than 5");
//编译报错,并输出信息。

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

1
2
3
4
5
6
7
8
9
10
11
template <typename T1, typename T2>
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");
return t1+t2;
};
int main() {
cout << add(1,2) << endl;
cout << add("hello",1) << endl;
return 0;
}

move semantics

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

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

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

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

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
98
99
100
101
102
103
104
105
#include <iostream>
#include <bits/unique_ptr.h>
#include <assert.h>

template <typename T>
class Buffer
{
std::string _name;
size_t _size;
std::unique_ptr<T[]> _buffer;

public:
// default constructor
Buffer():
_size(16),
_buffer(new T[16])
{}

// constructor
Buffer(const std::string& name, size_t size):
_name(name),
_size(size),
_buffer(new T[size])
{}

// copy constructor
Buffer(const Buffer& copy):
_name(copy._name),
_size(copy._size),
_buffer(new T[copy._size])
{
std::cout << "copy constructor called" << std::endl;
T* source = copy._buffer.get();
T* dest = _buffer.get();
std::copy(source, source + copy._size, dest);
}

// copy assignment operator
Buffer& operator=(const Buffer& copy)
{
std::cout << "copy assignment operator called" << std::endl;
if(this != NULL)
{
_name = copy._name;

if(_size != copy._size)
{
_buffer = nullptr;
_size = copy._size;
_buffer = _size > 0 ? new T[_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))
{
std::cout << "move constructor called" << std::endl;
temp._buffer = nullptr;
temp._size = 0;
}

// move assignment operator
Buffer& operator=(Buffer&& temp)
{
std::cout << "move assignment called" << std::endl;
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 <typename T>
Buffer<T> getBuffer(const std::string& name)
{
Buffer<T> b(name, 128);
return b;
}
int main()
{
Buffer<int> b1;
Buffer<int> b2("buf2", 64);
Buffer<int> b3 = b2;
Buffer<int> b4 = Buffer<int>("buf4",10);
b1 = Buffer<int>("buf5",13);
return 0;
}