C++11 新特性

右值引用

在 C++中,右值是一个临时的值,在使用结束后就会被销毁。基础类型的右值是不允许被修改的,但是用户自定义类型的右值是可以通过成员函数来修改的。但右值作为一个临时存在的对象,生命周期依旧很短。为了对即将被销毁的右值进行有效的利用,C++11 引入了右值引用。

右值引用可以延长右值的生命周期,在右值被废弃之前移走其资源,从而避免无意义的复制,从而提高效率。右值引用的符号是&&,以下是右值引用的基本使用示例。

int&& a = 1;    // 将不具名变量(临时内存区域)取了个别名
int b = 1;
int&& c = b;    // 这里将会出现错误,不能将一个左值赋予右值引用

看起来和左值引用没有什么区别,以下给出一个简单的示例。

class A {
    public:
        int a;
};
// 函数 getTemp() 返回的是一个右值,即临时变量
A getTemp() {
    return A();
}

A& a = getTemp();   // 这里将会报错,因为getTemp()返回的临时变量已被销毁
const A& a = getTemp(); // 不会报错,常量左值引用是万能引用,但只能读
A&& a = getTemp();  // 延长了getTemp()返回的右值的生命周期

在编写类的时候,经常会用到复制构造函数和复制赋值,在这个过程中被复制的内容通常要经过两次复制,并且是深复制。例如以下示例。

MyClass(const MyClass& instance) {
    // 对 instance 内容进行复制
    // C++ 会首先将其复制到临时区域再复制给相应的属性
}

借助右值引用带来的移动语义,就可以完美的对这个过程进行优化。要实现移动语义,只需要将上例中函数参数中的引用改为右值引用即可。此外标准库中提供了std::move()函数可以将左值转换为右值。

在右值引用与模板结合时,T&&不一定表示右值引用,既可能是左值引用,也可能是右值引用。其在使用时需要被初始化以决定其是左值引用还是右值引用。这里有一个比较简单的原则:

  1. 任何右值引用叠加到右值引用上,依旧是一个右值引用。
  2. 其他所有的引用类型之间的叠加都是一个左值引用。

统一初始化

C++11 扩大了用大括号括起的列表的使用范围。在 C++03 中,对于变量和属性的初始化有以下四种方法:

// 小括号初始化
int a = int(5);

// 赋值初始化
int a = 3;

// POD聚合初始化
int arr[2] = {0, 1};

// 构造函数初始化
class Example {
    Example(int v1, int v2): a(v1), b(v2) {}
private:
    int a;
    int b;
}

在 C++11 中,无论是类的变量、数组、STL 容器、类的构造函数,都统一使用大括号{}。以下给出一个示例。

class Example {
    int a, b, c[4];
    int * d = new int[4]{1, 2, 3, 4};
    vector<int> vec = {1, 2, 4};
    map<string, int> maps = {{"foo", 1}, {"bar", 2}};
public:
    Example(int i, int j): a(i), b(j), c{2, 3, 4, 5} {};
}

int main(int argc, const char * argv[]) {
    Example e{1, 2};
    return 0;
}

自动类型推导

C++11 的编译器增加了在变量声明时推断其类型的功能,这允许使用关键字auto来作为其类型,可以省去冗长的类型声明。例如以下示例。

// 例如有以下集合
vector<int> collection;
// 在C++11之前需要这样获得其迭代器
vector<int>::iterator it = collection.iterator();
// 在C++11以后可以这样书写
auto it = collection.iterator();

自动类型推断还可以用在泛型上,例如常见的 Builder 方法。在 C++11 之前是以下形式。

template <typename Built, typename Builder>
void makeObject(const Builder& builder) {
    Built val = builder.build();
}

在使用自动类型推断以后,可以省去一个类型参数。

template <typename Builder>
void makeObject(const Builder& builder) {
    auto val = builder.build();
}

为了通过使用auto变量的类型,C++11 还相应的引入了decltype关键字。decltypeauto是相对应的,可以在编译的时候判断一个变量或者一个表达式的类型。例如使用一个变量来定义另一个变量。

auto num = 1;
decltype(num) num2 = num;

或者使用decltype来定义函数返回值,这种函数的定义格式与 C++之前的函数不同,返回值类型是放置在参数列表之后的,因为放置在函数列表之前,可能函数中所用到的类型都尚未出现,所以 C++11 改变了语法规则。例如以下函数形式。

// 最简单的后置返回值类型的示例
auto swap(int a, int b) -> int;
// 搭配前面的示例,可以构建一个适配器语法的构建函数
template <typename Builder>
auto adapt(const Builder& builder) -> decltype(builder.build()) {
    auto val = builder.build();
    // 做一些适配操作
    return val;
}

decltype可以利用变量、表达式等来取得类型,并完成类型声明。

Tip

需要注意的是,自动类型推导不能应用于函数参数、非静态成员变量、数组以及模板参数。

常量表达式

常量表达式允许一些常量的值的计算在编译阶段确定,而不是在运行阶段确定。常量表达式是一个函数,通过constexpr关键字定义,其定义和使用遵循以下规则:

  • 函数中只能存在一条return语句。
  • 函数必须有返回值。
  • 在使用前必须定义。
  • return语句中的表达式不能使用非常量的函数、全局数据。
  • 函数所接收的参数只能是常量。

以下是一个最简单的常量表达式及其用法。

constexpr int m() {
    return 999;
}

const int a = m();

自定义数据类型也可以成为常量表达式的值,但这时需要自定义常量构造函数,但是需要遵循以下要求:

  • 函数体必须为空。
  • 所有成员变量必须要初始化。
  • 初始化列表只能由常量或者常量表达式赋值。

以下是一个自定义数据类型常量构造函数的示例。

class Date {
    constexpr Date(int y, int m, int d): year(y), month(m), day(d) {}

    constexpr int get_year() { return year; }
    constexpr int get_month() { return month; }
    constexpr int get_day() { return day; }
private:
    int year;
    int month;
    int day;
}

// 使用方式如下
constexpr Date mem{1970, 1, 1};
constexpr int month = mem.get_month();

自定义字面量

C++11 允许通过自定义后缀,使用整数、浮点数、字符以及字符串字面量来建立自定义类型的对象。自定义后缀表达式都以_开始,只有来自标准库的后缀不以下划线开始。例如:12_km0.5_Pa123._a0xFF540A_RGB"abc"_L等。

自定义字面量后缀可以通过字面量运算符来定义,格式为return_type operator "" _suffix(params)

注意,在 C++11 中,operator ""_suffix之间有一个空格。在 C++14 里可以没有空格。

如果字面量运算符是一个模板,则必须有空形参列表,并且只能有一个类型参数。格式为:

template <typename>
return_type operator "" _suffix();

形参列表用于接收字面量部分,可用的形参列表形式有以下这些。

  • (const char*)
  • (unsigned long long int)
  • (long double)
  • (char)
  • (wchar_t)
  • (char16_t)
  • (char32_t)
  • (const char*, std::size_t)
  • (const wchar_t, std::size_t)
  • (const char16_t, std::size_t)
  • (const char32_t, std::size_t)

集合遍历

在 C++11 之前,要对一个集合进行遍历,通常都是采用索引或者是迭代器,例如。

int arr[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) { cout << arr[i]; }
vector<int> vec{1, 2, 3, 4, 5};
for (vector<int>::iterator it = vec.begin(); it != vec.end(); it++) {
    cout << *it;
}

C++11 引入了基于范围遍历容器的方法并不需要明确给出容器的开始和结束条件。

vector<int> vec{1, 2, 3, 4, 5};
for (auto it : vec) {
    cout << it;
}

默认情况下,for遍历取出的容器元素是不可修改的,如果需要修改,可以使用for (auto& it : vec),但这种修改受到容器本身的约束。容器本身在被遍历时并不能被修改,尤其是对其中元素的增减。

空指针

继承自 C,C++也使用NULL来初始化空指针,这个值通常建议用 0 来代替。但是这时空指针实际上是一个整型值。所以为了避免与整型值混淆,C++11 加入了一个新的关键字nullptr来表示空指针。

所以现在所有对于指针的初始化都可以使用nullptr来完成,并且可以与NULL完全区分开。

强类型枚举类

C/C++的对于枚举的传统处理都是将其转化为整型。例如以下两个位于同一作用域的枚举类型就不能同时使用。

enum Side { Left, Right };
enum Judgement { Right, Wrong };

C++11 引入了强类型枚举来解决这个问题,枚举值不会被默认转换为整型,也不能与整型值比较;而其中的枚举值也不能脱离枚举类型使用,只能使用枚举类型::枚举值的格式。

强类型枚举使用enum class来定义,格式可参考以下示例。

enum class Enumerate {
    Value1,
    Value2 = 9, \\ 可指定值
    Value3
};

强类型枚举默认底层类型为int,可以通过指定其底层类型来改变,但是需要注意底层类型必须为除wchar_t以外的整型。例如。

enum class Gender:char {
    Female,
    Male
};

静态断言

C++11 引入了关键字static_assert来完成编译期间的断言,也就是静态断言。其使用格式为static_assert(常量表达式, 提示文字)。静态断言会在编译期执行,对于指定表达式进行计算,如果表达式值为假,那么静态断言就会产生一条编译错误,中断编译。

默认构造函数

在定义一个类或结构体时,如果没有提供一个构造函数,C++会默认提供一个构造函数,其中会对成员变量进行默认初始化。例如以下示例。

struct Point {
    int x;
    int y;
}

当提供一个构造函数之后,默认提供的构造函数就没有了,但是 C++11 提供了一种方法可以要求 C++重新生成一个默认构造函数。具体使用可参考下例。

struct Point {
    Point()=default;
    Point(int _x, int _y): x(_x), y(_y) {}
    int x = 0;
    int y = 0;
}

委托构造函数

在 C++11 之前的版本中,构造函数之间是不能相互调用的,如果几个构造函数之间共享相同的构造逻辑,就必须编写额外的方法。例如下例。

class Example {
    string keep;
    init(string k);
public:
    Example(string s) { init(s); }
    Example() { init("default"); }
}

从 C++11 开始,利用委托构造函数,实现了构造函数的相互调用,所以可以在定义一个构造函数时调动另一个构造函数了。例如。

class Example {
    string keep;
public:
    Example(string s): keep(s) {}
    Example(): Example{"default"} {}
}

相似的,在类继承中,派生类也可以使用这种语法来调用基类的构造函数。

重写限制

对于类之间的继承,C++11 添加了overridefinal两个关键字来完成继承控制。其中final可以阻止类的进一步派生和重写,override则确保派生类中声明的函数与基类中有相同的签名。具体使用可参考下例。

// 类A完全不能被继承
class A final {}
// 类B为一个基类
class B {
    virtual void foo() const;
    virtual void bar();
}
// 类D继承类B
class D: public B {
    virtual void foo() const override;  // 重写基类的函数
    virtual void bar() override final;  // 重写基类函数并不能再重写
}

成员初始值定义

在 C++11 之前的版本中,只有静态成员变量是可以使用=来初始化的。这个特性在 C++11 中得到了扩展,现在成员变量可以通过={}来完成初始化,具体语法可参考变量初始化。

Lambda 表达式

Lambda 表达式已经在前面介绍过了,这是 C++11 才引入的功能。

模板别名

typedef关键字是不能为模板定义别名的,以下示例是无法通过编译的。

template <typename T>
typedef std::map<std::string, T> map_alias;

map_alias<int> map_int;

必须要通过一个类似于下例包装类来完成。

template <typename T>
struct map_alias {
    typedef std::map<std::string, T> map;
};

map_alias<int>::map map_int;

但是 C++11 通过关键字using提供了这个功能,允许为模板定义一个别名,简化变量的定义。

template <typename T>
using map_alias = std::map<std::string, T>;

alias_map<int> map_int;