Skip to content

C++整理-C++11重要特性

约 2365 字大约 8 分钟

C++

2025-08-28

我们按照cppreference给出的顺序整理一下C++11的重要特性:

auto

auto在C中本来是界定变量存储方式的关键字,在C++11之后,该关键字行为被改变为进行类型推导,最简单的方法是通过auto声明变量:

auto bar = foo();

编译器会通过foo()自动推导类型,我们也可以添加类型标记:

const auto bar = foo();
auto& bar = foo();
auto* bar = foo();

为什么auto要有类型标记?这是因为某些情况下我们需要对类型进行进一步的解释,假如foo()返回的是一个referenceauto的行为就会产生歧义,标记可以消除这种歧义:

auto bar = foo();   // 拷贝
auto& bar = foo();  // 引用

C++11的auto也可以用来声明函数:

auto 
foo() -> int 
{
  // ...
}

这是我们常说的trailing-return-type,某些时候我们需要这个特性来引用参数类型,比如下面这个函数返回int类型变量:

auto 
foo(int a) -> decltype(a) 
{
  // ...
}

C++11的auto关键字尚且十分稚嫩,我们会在后续章节介绍更新标准的auto

decltype

decltype也是编译期类型推导的手段之一,它是编译器扩展typeof的标准化,简单来说,我们可以用他来推导已经存在变量的类型:

decltype(1) bar; // int

同样,在C++11时它也没有什么特殊之处,不过值得一提的是,对于括号表达式,decltype将其处理为左值引用,这个例子来自cppreference

struct A { double x; };
const A* a;
 
decltype(a->x) y;       // type of y is double (declared type)
decltype((a->x)) z = y; // type of z is const double& (lvalue expression)

defaultdelete

以前,我们都知道编译器会帮助我们默认实现一些函数(析构、构造、拷贝等),现在我们可以显化这种默认行为:

class Foo {
public:
  Foo() = default;  // 默认实现构造函数
  ~Foo() = delete;  // 删除析构函数实现
};

如果某个函数被标记为delete,那么编译时,如果有人引用这个符号,编译将会失败。

finaloverride

它们是面向对象工具的扩展,final用来标记虚函数不应被子类重写、或者类不应被继承:

class Foo final : public Base {
public:
  void bar() final;
};

override用来标记函数由另一个虚函数覆盖而来,它的目的是保证函数是虚函数,且由父类继承而来

右值引用

左值和右值是一个相当庞大的话题,一般来说:左值是被绑定到具体符号上的变量,他们的生命周期和符号作用域绑定,右值是那些表达式结束之后就不会再存在的临时对象(比如函数的返回值、表达式的结果、非字符字面量)。

我们非常了解左值引用,它就是为已经存在的左值创建一个alias,但右值引用是什么呢?在语义上,它代表将值的所有权进行转移,举个例子:

void 
foo(std::vector<int>&& rval) 
{
  // ...
}

这往往代表着经过这个函数之后,rval所指向的变量便走向了它的终结,所以,foo这个函数获得了处分这个变量的权利,那么,明确地知道变量即将消失有什么好处吗?答案是我们可以通过不拷贝的方式转移对象,为了达成这个目的,我们有了移动构造函数:

class Foo {
public:
  Foo(Foo&& other) 
  {
    // 不必执行深拷贝
  }
};

当然,我们也同时有了移动赋值函数,其二者大致相同,为了调用这样的函数,我们需要为其传入右值:

Foo a;

Foo(Foo());
Foo(std::move(a));

std::move是一个工具函数,其将左值的引用转换为右值引用,含义是:我们不会再使用该左值,因此将其移交(给其他函数或者移动构造)

但是需要注意,在进入函数之后,右值引用参数就变成了左值:

void 
foo(std::vector<int>&& rval) 
{
  // 从此开始,`rval`是左值
}

为什么是左值呢?因为现在的rval在各种意义上都符合左值的定义:和符号绑定、生命周期和作用域相当、所有权位于当前作用域。

如何重新将其变为右值引用?要么我们再std::move一次,要么我们可以使用std::forward,该函数将类型完美转发给其他函数:

void 
foo(std::vector<int>&& rval) 
{
  bar(std::forward<std::vector<int>>(rval));
}

std::forward利用了引用折叠机制,简单来说,我们的引用标记会被折叠,本例来源于cppreference

typedef 
typedef int&  lref;
typedef int&& rref;
int n;
 
lref&  r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref&  r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&

了解了这个,我们再来观察最后一个现象,那就是万能引用:

template <typename T>
void foo(T&& val) {
  // ...
}

为什么T既能引用左值,也能引用右值?那是因为模板在展开时进行了引用折叠,如果推导为右值引用,仍然折叠为右值引用,如果推导为左值引用,仍然折叠为左值引用。

有命名空间的枚举

我们可以声明带有命名空间的枚举来隔离枚举符号:

enum class Color {
  RED,
  GREEN,
  BLUE,
};

使用时,我们不能像C枚举一样直接使用RED,而是需要使用Color::RED,这样有效地隔离了枚举符号对命名空间的污染。

也可以使用enum struct

constexpr和字面类型

简单来说,constexpr是对字面量的扩展,其表示变量和函数可以在编译期展开,利用这个关键字,部分操作可以直接在编译期完成,对于变量我们可以这么声明:

constexpr Foo value{};

在C++11时,能被声明为constexpr的类型,其必须是字面量类型,任何函数想要声明为constexpr,其参数和返回值类型必须是字面量类型。

关于字面量类型,请参考cppreference

有关constexpr的特性无比庞大,且每个版本都有一定程度的修订,不过如果读者围绕C++11或者C++17进行工作,并且不需要元编程特性,则constexpr没有什么太大用处(除了给我们定义一些常量之外)

列表初始化

过去,我们自定义的列表类型不能直接兼容c-style的数组初始化方式,C++11为了统一类型初始化,创造了列表初始化和std::initializer_list

列表初始化允许我们使用大括号调用类型的构造函数:

Foo obj{ arg1, arg2 };    // 直接初始化
Foo obj = { arg1, arg2 }; // 拷贝初始化

基于这个特性,配合std::initializer_list,我们可以支持自定义列表类型的初始化:

std::vector<int> arr{ 1, 2, 3, 4 };

nullptr

c-style的空指针NULL一般被定义为:

#define NULL ((void*)0)

我们如果将void*看作any的话,实际上使用这个定义是极其危险的,因为NULL实际上不仅可以是指针类型,也可以转换为任何类型,在拥有函数重载功能的C++中,这种性质很容易造成多义性。

nullptrstd::nullptr_t的字面量实例,它只能被隐式转换成任何指针类型,避免了我们对函数重载中指针和整形变量的误判。

using

using是对typedef的升级,其作用是对类型进行重命名:

using num_t = int;
using num_vec_t = std::vector<int>;

我们可以在重命名时保留模板:

template <typename T>
using vec_t = std::vector<T>;

可变参数模板

我们可以使用...声明可变参数模板:

template <typename... Args>
void 
foo(Args... args) 
{
  bar(&args...);
}

在作用域内部,我们可以使用...解包可变参数,&args...将参数包解包为指针,再传递给bar,我们也可以使用[]访问具体参数。

fold-expression于C++17引入

lambda表达式

lambda表达式是源于数学的概念,在编程语言中一般代指嵌套函数声明,或者更为本质的函数对象。

在C++11后,我们可以通过如下方式声明lambda表达式:

auto f = [](int a) { return a + 1; };

f(1);  // 2

中括号标记的部分被称为捕获列表,可以将当前作用域中的符号以拷贝或引用的方式传递给lambda表达式:

int base = 10;
auto f = [base](int a) { return a + base; };

f(1);   // 11

一般来说,编译器实现lambda表达式的方法是构造一个匿名类,并重写其operator(),捕获列表则作为参数传递给类,这也是为什么我们不能写出具体的lambda类型。

如果想要引用lambda作为参数,需要使用模板参数,或者std::function

静态断言

static_assert用来进行编译期断言,可以作为一种模板类型检查手段,例子来源于cppreference

template<class T>
void swap(T& a, T& b) noexcept
{
    static_assert(std::is_copy_constructible_v<T>,
                  "Swap requires copying");
    static_assert(std::is_nothrow_copy_constructible_v<T> &&
                  std::is_nothrow_copy_assignable_v<T>,
                  "Swap requires nothrow copy/assign");
    auto c = b;
    b = a;
    a = c;
}

更新日志

2025/9/1 05:34
查看所有更新日志
  • 4bba0-C++11