C++11完全指南:新特性与最佳实践 #
本文全面介绍了C++11标准引入的所有重要特性,包括移动语义、右值引用、Lambda表达式、智能指针等核心语言特性,以及标准库的重要更新。每个特性都配有详细说明和实用的代码示例,帮助开发者快速掌握并应用这些新特性。
C++11 包含以下新的语言特性:
- 移动语义
- 可变参数模板
- 右值引用
- 转发引用
- 初始化列表
- 静态断言
- auto
- lambda 表达式
- decltype
- 类型别名
- nullptr
- 强类型枚举
- 属性
- constexpr
- 委托构造函数
- 用户自定义字面量
- 显式虚函数覆盖
- final 限定符
- 默认函数
- 删除函数
- 基于范围的 for 循环
- 移动语义的特殊成员函数
- 转换构造函数
- 显式转换函数
- 内联命名空间
- 非静态数据成员初始化器
- 右尖括号
- 引用限定的成员函数
- 尾置返回类型
- noexcept 限定符
- char32_t 和 char16_t
- 原始字符串字面量
C++11 包含以下新的库特性:
- std::move
- std::forward
- std::thread
- std::to_string
- 类型特征
- 智能指针
- std::chrono
- 元组
- std::tie
- std::array
- 无序容器
- std::make_shared
- std::ref
- 内存模型
- std::async
- std::begin/end
C++11 语言特性 #
移动语义 #
移动一个对象意味着将它所管理的某些资源的所有权转移到另一个对象。
移动语义的第一个好处是性能优化。当一个对象即将结束生命周期时,无论是因为它是一个临时对象,还是通过显式调用 std::move
,移动通常是转移资源的更便宜的方式。例如,移动一个 std::vector
只是将一些指针和内部状态复制到新向量中——复制则需要复制向量中的每个元素,这是昂贵且不必要的,尤其是当旧向量即将被销毁时。
移动语义还使得像 std::unique_ptr
(智能指针)这样的不可拷贝类型能够在语言级别保证一次只有一个实例管理资源,同时能够在作用域之间转移实例。
参见:右值引用,移动语义的特殊成员函数,std::move
,std::forward
,转发引用。
右值引用 #
C++11 引入了一种新的引用,称为 右值引用。一个类型为 T
的右值引用(T
是非模板类型参数,例如 int
或用户定义类型)的语法是 T&&
。右值引用只能绑定到右值。
类型推导与左值和右值:
int x = 0; // `x` 是类型为 `int` 的左值
int& xl = x; // `xl` 是类型为 `int&` 的左值
int&& xr = x; // 编译错误 -- `x` 是左值
int&& xr2 = 0; // `xr2` 是类型为 `int&&` 的左值 -- 绑定到右值临时量 `0`
void f(int& x) {}
void f(int&& x) {}
f(x); // 调用 f(int&)
f(xl); // 调用 f(int&)
f(3); // 调用 f(int&&)
f(std::move(x)); // 调用 f(int&&)
f(xr2); // 调用 f(int&)
f(std::move(xr2)); // 调用 f(int&& x)
参见:std::move
,std::forward
,转发引用。
转发引用 #
也被称为(非官方地)通用引用。转发引用的语法是 T&&
,其中 T
是模板类型参数,或者使用 auto&&
。这使得 完美转发 成为可能:传递参数时保持它们的值类别(例如,左值仍然是左值,临时量作为右值转发)。
转发引用允许引用根据类型绑定到左值或右值。转发引用遵循 引用折叠 规则:
T& &
变为T&
T& &&
变为T&
T&& &
变为T&
T&& &&
变为T&&
auto
类型推导与左值和右值:
int x = 0; // `x` 是类型为 `int` 的左值
auto&& al = x; // `al` 是类型为 `int&` 的左值 -- 绑定到左值 `x`
auto&& ar = 0; // `ar` 是类型为 `int&&` 的左值 -- 绑定到右值临时量 `0`
模板类型参数推导与左值和右值:
// 自 C++14 或更高版本起:
void f(auto&& t) {
// ...
}
// 自 C++11 或更高版本起:
template <typename T>
void f(T&& t) {
// ...
}
int x = 0;
f(0); // T 是 int,推导为 f(int &&) => f(int&&)
f(x); // T 是 int&,推导为 f(int& &&) => f(int&)
int& y = x;
f(y); // T 是 int&,推导为 f(int& &&) => f(int&)
int&& z = 0; // 注意:`z` 是类型为 `int&&` 的左值。
f(z); // T 是 int&,推导为 f(int& &&) => f(int&)
f(std::move(z)); // T 是 int,推导为 f(int &&) => f(int&&)
参见:std::move
,std::forward
,右值引用。
可变参数模板 #
...
语法用于创建 参数包 或展开一个参数包。模板 参数包 是一个模板参数,它接受零个或多个模板参数(非类型、类型或模板)。至少有一个参数包的模板称为 可变参数模板。
template <typename... T>
struct arity {
constexpr static int value = sizeof...(T);
};
static_assert(arity<>::value == 0);
static_assert(arity<char, short, int>::value == 3);
一个有趣的用途是从 参数包 创建一个 初始化列表,以便迭代可变参数函数的参数。
template <typename First, typename... Args>
auto sum(const First first, const Args... args) -> decltype(first) {
const auto values = {first, args...};
return std::accumulate(values.begin(), values.end(), First{0});
}
sum(1, 2, 3, 4, 5); // 15
sum(1, 2, 3); // 6
sum(1.5, 2.0, 3.7); // 7.2
初始化列表 #
使用 “花括号列表” 语法创建的轻量级数组类容器。例如,{ 1, 2, 3 }
创建了一个整数序列,其类型为 std::initializer_list<int>
。作为传递对象向量给函数的替代方法很有用。
int sum(const std::initializer_list<int>& list) {
int total = 0;
for (auto& e : list) {
total += e;
}
return total;
}
auto list = {1, 2, 3};
sum(list); // == 6
sum({1, 2, 3}); // == 6
sum({}); // == 0
静态断言 #
在编译时评估的断言。
constexpr int x = 0;
constexpr int y = 1;
static_assert(x == y, "x != y");
auto #
auto
类型的变量由编译器根据其初始化器的类型进行推导。
auto a = 3.14; // double
auto b = 1; // int
auto& c = b; // int&
auto d = { 0 }; // std::initializer_list<int>
auto&& e = 1; // int&&
auto&& f = b; // int&
auto g = new auto(123); // int*
const auto h = 1; // const int
auto i = 1, j = 2, k = 3; // int, int, int
auto l = 1, m = true, n = 1.61; // 错误 -- `l` 被推导为 int,`m` 是 bool
auto o; // 错误 -- `o` 需要初始化器
对于复杂类型特别有用,可以提高可读性:
std::vector<int> v = ...;
std::vector<int>::const_iterator cit = v.cbegin();
// 与以下代码相比:
auto cit = v.cbegin();
函数也可以使用 auto
推导返回类型。在 C++11 中,必须显式指定返回类型,或者使用 decltype
,如下所示:
template <typename X, typename Y>
auto add(X x, Y y) -> decltype(x + y) {
return x + y;
}
add(1, 2); // == 3
add(1, 2.0); // == 3.0
add(1.5, 1.5); // == 3.0
上面示例中的尾置返回类型是表达式 x + y
的 声明类型(参见 decltype 部分)。例如,如果 x
是整数,y
是双精度浮点数,则 decltype(x + y)
是双精度浮点数。因此,上述函数将根据表达式 x + y
的类型推导出返回类型。注意,尾置返回类型可以访问其参数和 this
(如果适用)。
Lambda 表达式 #
lambda
是一个匿名函数对象,能够在作用域内捕获变量。它包含:一个 捕获列表;一组可选的参数(带可选尾置返回类型);以及一个函数体。捕获列表的示例:
[]
- 不捕获任何内容。[=]
- 按值捕获作用域内的局部对象(局部变量、参数)。[&]
- 按引用捕获作用域内的局部对象(局部变量、参数)。[this]
- 按引用捕获this
。[a, &b]
- 按值捕获对象a
,按引用捕获对象b
。
int x = 1;
auto getX = [=] { return x; };
getX(); // == 1
auto addX = [=](int y) { return x + y; };
addX(1); // == 2
auto getXRef = [&]() -> int& { return x; };
getXRef(); // 返回 `x` 的 int&
默认情况下,值捕获在 lambda 中不能被修改,因为编译器生成的方法被标记为 const
。mutable
关键字允许修改捕获的变量。该关键字放在参数列表之后(即使参数列表为空也必须存在)。
int x = 1;
auto f1 = [&x] { x = 2; }; // OK:`x` 是引用,修改原始变量
auto f2 = [x] { x = 2; }; // 错误:lambda 只能在捕获的值上执行 const 操作
// 与以下代码相比:
auto f3 = [x]() mutable { x = 2; }; // OK:lambda 可以对捕获的值执行任何操作
decltype #
decltype
是一个运算符,返回传递给它的表达式的 声明类型。如果表达式包含 cv 限定符和引用,则会保留这些属性。decltype
的示例:
int a = 1; // `a` 的声明类型为 `int`
decltype(a) b = a; // `decltype(a)` 是 `int`
const int& c = a; // `c` 的声明类型为 `const int&`
decltype(c) d = a; // `decltype(c)` 是 `const int&`
decltype(123) e = 123; // `decltype(123)` 是 `int`
int&& f = 1; // `f` 的声明类型为 `int&&`
decltype(f) g = 1; // `decltype(f)` 是 `int&&`
decltype((a)) h = g; // `decltype((a))` 是 int&
template <typename X, typename Y>
auto add(X x, Y y) -> decltype(x + y) {
return x + y;
}
add(1, 2.0); // `decltype(x + y)` => `decltype(3.0)` => `double`
类型别名 #
与使用 typedef
在语义上类似,但使用 using
的类型别名更易于阅读,并且与模板兼容。
template <typename T>
using Vec = std::vector<T>;
Vec<int> v; // std::vector<int>
using String = std::string;
String s {"foo"};
nullptr #
C++11 引入了一种新的空指针类型,旨在取代 C 的 NULL
宏。nullptr
本身是类型 std::nullptr_t
,可以隐式转换为指针类型,与 NULL
不同的是,它不能转换为除 bool
之外的整数类型。
void foo(int);
void foo(char*);
foo(NULL); // 错误 -- 模糊
foo(nullptr); // 调用 foo(char*)
强类型枚举 #
解决了 C 风格枚举的多种问题,包括:隐式转换、无法指定底层类型、作用域污染等。
// 指定底层类型为 `unsigned int`
enum class Color : unsigned int { Red = 0xff0000, Green = 0xff00, Blue = 0xff };
// `Red`/`Green` 在 `Alert` 中不会与 `Color` 冲突
enum class Alert : bool { Red, Green };
Color c = Color::Red;
属性 #
提供了一种通用语法,替代了 __attribute__(...)
、__declspec
等。
// `noreturn` 属性表示 `f` 不会返回。
[[ noreturn ]] void f() {
throw "error";
}
constexpr #
常量表达式是 可能 在编译时由编译器评估的表达式。只有非复杂计算可以在常量表达式中执行(这些规则在后续版本中逐渐放宽)。使用 constexpr
限定符表示变量、函数等是常量表达式。
constexpr int square(int x) {
return x * x;
}
int square2(int x) {
return x * x;
}
int a = square(2); // mov DWORD PTR [rbp-4], 4
int b = square2(2); // mov edi, 2
// call square2(int)
// mov DWORD PTR [rbp-8], eax
在前面的代码片段中,注意调用 square
时的计算是在编译时完成的,并且结果嵌入到代码生成中,而 square2
是在运行时调用的。
constexpr
值是可以由编译器评估的值,但并非保证在编译时评估:
const int x = 123;
constexpr const int& y = x; // 错误 -- 常量表达式变量 `y` 必须由常量表达式初始化
类中的常量表达式:
struct Complex {
constexpr Complex(double r, double i) : re{r}, im{i} { }
constexpr double real() { return re; }
constexpr double imag() { return im; }
private:
double re;
double im;
};
constexpr Complex I(0, 1);
委托构造函数 #
构造函数现在可以调用同一类中的其他构造函数,使用初始化列表。
struct Foo {
int foo;
Foo(int foo) : foo{foo} {}
Foo() : Foo(0) {}
};
Foo foo;
foo.foo; // == 0
用户自定义字面量 #
用户自定义字面量允许你扩展语言并添加自己的语法。要创建字面量,定义一个 T operator "" X(...) { ... }
函数,返回类型为 T
,名称为 X
。注意,该函数的名称定义了字面量的名称。任何不以下划线开头的字面量名称都是保留的,不会被调用。根据字面量调用的类型,用户自定义字面量函数应接受的参数有特定规则。
将摄氏温度转换为华氏温度:
// 需要 `unsigned long long` 参数作为整数字面量。
long long operator "" _celsius(unsigned long long tempCelsius) {
return std::llround(tempCelsius * 1.8 + 32);
}
24_celsius; // == 75
字符串到整数的转换:
// 需要 `const char*` 和 `std::size_t` 作为参数。
int operator "" _int(const char* str, std::size_t) {
return std::stoi(str);
}
"123"_int; // == 123,类型为 `int`
显式虚函数覆盖 #
指定虚函数覆盖了另一个虚函数。如果虚函数没有覆盖父类的虚函数,则会抛出编译器错误。
struct A {
virtual void foo();
void bar();
};
struct B : A {
void foo() override; // 正确 -- B::foo 覆盖了 A::foo
void bar() override; // 错误 -- A::bar 不是虚函数
void baz() override; // 错误 -- B::baz 没有覆盖 A::baz
};
final 限定符 #
指定虚函数不能在派生类中被覆盖,或者类不能被继承。
struct A {
virtual void foo();
};
struct B : A {
virtual void foo() final;
};
struct C : B {
virtual void foo(); // 错误 -- 声明的 `foo` 覆盖了一个 `final` 函数
};
类不能被继承:
struct A final {};
struct B : A {}; // 错误 -- 基类 `A` 被标记为 `final`
默认函数 #
提供函数的默认实现(例如构造函数)的更优雅、高效的方式。
struct A {
A() = default;
A(int x) : x{x} {}
int x {1};
};
A a; // a.x == 1
A a2 {123}; // a.x == 123
在继承中:
struct B {
B() : x{1} {}
int x;
};
struct C : B {
// 调用 B::B
C() = default;
};
C c; // c.x == 1
删除函数 #
提供函数的删除实现的更优雅、高效的方式。用于防止对象被拷贝。
class A {
int x;
public:
A(int x) : x{x} {};
A(const A&) = delete;
A& operator=(const A&) = delete;
};
A x {123};
A y = x; // 错误 -- 调用了被删除的拷贝构造函数
y = x; // 错误 -- 赋值运算符被删除
基于范围的 for 循环 #
用于迭代容器元素的语法糖。
std::array<int, 5> a {1, 2, 3, 4, 5};
for (int& x : a) x *= 2;
// a == { 2, 4, 6, 8, 10 }
注意使用 int
和 int&
的区别:
std::array<int, 5> a {1, 2, 3, 4, 5};
for (int x : a) x *= 2;
// a == { 1, 2, 3, 4, 5 }
移动语义的特殊成员函数 #
拷贝构造函数和拷贝赋值运算符在拷贝时被调用,随着 C++11 引入移动语义,现在还有移动构造函数和移动赋值运算符用于移动。
struct A {
std::string s;
A() : s{"test"} {}
A(const A& o) : s{o.s} {}
A(A&& o) : s{std::move(o.s)} {}
A& operator=(A&& o) {
s = std::move(o.s);
return *this;
}
};
A f(A a) {
return a;
}
A a1 = f(A{}); // 从右值临时量移动构造
A a2 = std::move(a1); // 使用 std::move 移动构造
A a3 = A{};
a2 = std::move(a3); // 使用 std::move 移动赋值
a1 = f(A{}); // 从右值临时量移动赋值
转换构造函数 #
转换构造函数会将花括号列表语法中的值转换为构造函数参数。
struct A {
A(int) {}
A(int, int) {}
A(int, int, int) {}
};
A a {0, 0}; // 调用 A::A(int, int)
A b(0, 0); // 调用 A::A(int, int)
A c = {0, 0}; // 调用 A::A(int, int)
A d {0, 0, 0}; // 调用 A::A(int, int, int)
注意,花括号列表语法不允许缩小范围:
struct A {
A(int) {}
};
A a(1.1); // OK
A b {1.1}; // 错误:从 double 到 int 的缩小转换
注意,如果构造函数接受一个 std::initializer_list
,它将被优先调用:
struct A {
A(int) {}
A(int, int) {}
A(int, int, int) {}
A(std::initializer_list<int>) {}
};
A a {0, 0}; // 调用 A::A(std::initializer_list<int>)
A b(0, 0); // 调用 A::A(int, int)
A c = {0, 0}; // 调用 A::A(std::initializer_list<int>)
A d {0, 0, 0}; // 调用 A::A(std::initializer_list<int>)
显式转换函数 #
现在可以使用 explicit
限定符将转换函数声明为显式。
struct A {
operator bool() const { return true; }
};
struct B {
explicit operator bool() const { return true; }
};
A a;
if (a); // OK,调用 A::operator bool()
bool ba = a; // OK,复制初始化选择 A::operator bool()
B b;
if (b); // OK,调用 B::operator bool()
bool bb = b; // 错误:复制初始化不会考虑 B::operator bool()
内联命名空间 #
内联命名空间的所有成员被视为其父命名空间的一部分,允许函数的特化并简化版本控制过程。这是一个传递性属性,如果 A 包含 B,B 又包含 C,且 B 和 C 都是内联命名空间,则 C 的成员可以像在 A 中一样使用。
namespace Program {
namespace Version1 {
int getVersion() { return 1; }
bool isFirstVersion() { return true; }
}
inline namespace Version2 {
int getVersion() { return 2; }
}
}
int version {Program::getVersion()}; // 使用 Version2 中的 getVersion()
int oldVersion {Program::Version1::getVersion()}; // 使用 Version1 中的 getVersion()
bool firstVersion {Program::isFirstVersion()}; // 添加 Version2 后无法编译
非静态数据成员初始化器 #
允许在声明非静态数据成员时直接初始化,从而清理构造函数中的默认初始化。
// C++11 之前的默认初始化
class Human {
Human() : age{0} {}
private:
unsigned age;
};
// C++11 中的默认初始化
class Human {
private:
unsigned age {0};
};
右尖括号 #
C++11 现在能够推断一系列右尖括号是作为运算符使用,还是作为类型定义的结束,而无需添加空格。
typedef std::map<int, std::map <int, std::map <int, int> > > cpp98LongTypedef;
typedef std::map<int, std::map <int, std::map <int, int>>> cpp11LongTypedef;
引用限定的成员函数 #
成员函数现在可以根据 *this
是左值引用还是右值引用进行限定。
struct Bar {
// ...
};
struct Foo {
Bar& getBar() & { return bar; }
const Bar& getBar() const& { return bar; }
Bar&& getBar() && { return std::move(bar); }
const Bar&& getBar() const&& { return std::move(bar); }
private:
Bar bar;
};
Foo foo{};
Bar bar = foo.getBar(); // 调用 `Bar& getBar() &`
const Foo foo2{};
Bar bar2 = foo2.getBar(); // 调用 `Bar& Foo::getBar() const&`
Foo{}.getBar(); // 调用 `Bar&& Foo::getBar() &&`
std::move(foo).getBar(); // 调用 `Bar&& Foo::getBar() &&`
std::move(foo2).getBar(); // 调用 `const Bar&& Foo::getBar() const&`
尾置返回类型 #
C++11 允许函数和 lambda 使用替代语法指定返回类型。
int f() {
return 123;
}
// 与以下代码相比:
auto f() -> int {
return 123;
}
auto g = []() -> int {
return 123;
};
当某些返回类型无法解析时,此特性特别有用:
// 注意:此代码无法编译!
template <typename T, typename U>
decltype(a + b) add(T a, U b) {
return a + b;
}
// 尾置返回类型允许如下实现:
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
在 C++14 中,可以使用 decltype(auto) (C++14)
。
noexcept 限定符 #
noexcept
限定符指定函数是否可能抛出异常。它是 throw()
的改进版本。
void func1() noexcept; // 不会抛出异常
void func2() noexcept(true); // 不会抛出异常
void func3() throw(); // 不会抛出异常
void func4() noexcept(false); // 可能抛出异常
非抛出异常的函数可以调用可能抛出异常的函数。每当异常被抛出且异常处理程序的搜索遇到非抛出异常函数的最外层代码块时,将调用 std::terminate
函数。
extern void f(); // 可能抛出异常
void g() noexcept {
f(); // 有效,即使 f 抛出异常
throw 42; // 有效,实际上相当于调用 std::terminate
}
char32_t 和 char16_t #
提供标准类型以表示 UTF-8 字符串。
char32_t utf8_str[] = U"\u0123";
char16_t utf8_str[] = u"\u0123";
原始字符串字面量 #
C++11 引入了一种新的声明字符串字面量的方式,称为 “原始字符串字面量”。来自转义序列的字符(制表符、换行符、反斜杠等)可以以原始形式输入,同时保留格式。这在编写包含大量引号或特殊格式的文学文本时特别有用,可以使字符串字面量更易于阅读和维护。
原始字符串字面量的声明语法如下:
R"delimiter(raw_characters)delimiter"
其中:
delimiter
是一个可选的字符序列,由除括号、反斜杠和空格之外的任何源字符组成。raw_characters
是任何原始字符序列;必须不包含关闭序列")delimiter"
。
示例:
// msg1 和 msg2 是等效的。
const char* msg1 = "\nHello,\n\tworld!\n";
const char* msg2 = R"(
Hello,
world!
)";
C++11 库特性 #
std::move #
std::move
表示可以转移传递给它的对象的资源。使用被移动的对象时需要小心,因为它们可能处于未定义状态(参见:我可以对被移动的对象做什么?)。
std::move
的定义(执行移动实际上只是将对象强制转换为右值引用):
template <typename T>
typename remove_reference<T>::type&& move(T&& arg) {
return static_cast<typename remove_reference<T>::type&&>(arg);
}
转移 std::unique_ptr
:
std::unique_ptr<int> p1 {new int{0}}; // 在实践中,使用 std::make_unique
std::unique_ptr<int> p2 = p1; // 错误 -- 不能拷贝 unique_ptr
std::unique_ptr<int> p3 = std::move(p1); // 将 `p1` 移动到 `p3`
// 现在使用 `p1` 持有的对象是不安全的
std::forward #
返回传递给它的参数,同时保持它们的值类别和 cv 限定符。对于泛型代码和工厂函数非常有用。与 转发引用 配合使用。
std::forward
的定义:
template <typename T>
T&& forward(typename remove_reference<T>::type& arg) {
return static_cast<T&&>(arg);
}
一个 wrapper
函数的示例,它只是将其他 A
对象转发到新的 A
对象的拷贝构造函数或移动构造函数:
struct A {
A() = default;
A(const A& o) { std::cout << "copied" << std::endl; }
A(A&& o) { std::cout << "moved" << std::endl; }
};
template <typename T>
A wrapper(T&& arg) {
return A{std::forward<T>(arg)};
}
wrapper(A{}); // moved
A a;
wrapper(a); // copied
wrapper(std::move(a)); // moved
std::thread #
std::thread
库提供了一种标准方式来控制线程,例如创建和销毁线程。在以下示例中,创建了多个线程来执行不同的计算,然后程序等待它们全部完成。
void foo(bool clause) { /* 执行某些操作... */ }
std::vector<std::thread> threadsVector;
threadsVector.emplace_back([]() {
// 将被调用的 lambda 函数
});
threadsVector.emplace_back(foo, true); // 线程将运行 foo(true)
for (auto& thread : threadsVector) {
thread.join(); // 等待线程完成
}
std::to_string #
将数值参数转换为 std::string
。
std::to_string(1.2); // == "1.2"
std::to_string(123); // == "123"
类型特征 #
类型特征定义了一个基于模板的接口,用于在编译时查询或修改类型的属性。
static_assert(std::is_integral<int>::value);
static_assert(std::is_same<int, int>::value);
static_assert(std::is_same<std::conditional<true, int, double>::type, int>::value);
智能指针 #
C++11 引入了新的智能指针:std::unique_ptr
、std::shared_ptr
、std::weak_ptr
。std::auto_ptr
现在已废弃,并在 C++17 中被移除。
std::unique_ptr
是一个不可拷贝但可移动的指针,管理自己的堆分配内存。注意:优先使用 std::make_X
辅助函数,而不是使用构造函数。参见 std::make_unique 和 std::make_shared 的部分。
std::unique_ptr<Foo> p1 { new Foo{} }; // `p1` 拥有 `Foo`
if (p1) {
p1->bar();
}
{
std::unique_ptr<Foo> p2 {std::move(p1)}; // 现在 `p2` 拥有 `Foo`
f(*p2);
p1 = std::move(p2); // 所有权返回到 `p1` -- `p2` 被销毁
}
if (p1) {
p1->bar();
}
// 当 `p1` 超出作用域时,`Foo` 实例被销毁
std::shared_ptr
是一个管理共享资源的智能指针。共享指针持有一个 控制块,其中包含几个组件,如托管对象和引用计数器。所有对控制块的访问都是线程安全的,然而,对托管对象本身的操纵 不是 线程安全的。
void foo(std::shared_ptr<T> t) {
// 对 `t` 执行某些操作...
}
void bar(std::shared_ptr<T> t) {
// 对 `t` 执行某些操作...
}
void baz(std::shared_ptr<T> t) {
// 对 `t` 执行某些操作...
}
std::shared_ptr<T> p1 {new T{}};
// 这些操作可能在其他线程中发生?
foo(p1);
bar(p1);
baz(p1);
std::chrono #
chrono
库包含一组处理 持续时间、时钟 和 时间点 的工具函数和类型。该库的一个用例是代码基准测试:
std::chrono::time_point<std::chrono::steady_clock> start, end;
start = std::chrono::steady_clock::now();
// 执行某些计算...
end = std::chrono::steady_clock::now();
std::chrono::duration<double> elapsed_seconds = end - start;
double t = elapsed_seconds.count(); // `t` 表示秒数,类型为 `double`
元组 #
元组是一个固定大小的异构值集合。可以通过使用 std::tie
解包,或者使用 std::get
访问 std::tuple
的元素。
// `playerProfile` 的类型为 `std::tuple<int, const char*, const char*>`。
auto playerProfile = std::make_tuple(51, "Frans Nielsen", "NYI");
std::get<0>(playerProfile); // 51
std::get<1>(playerProfile); // "Frans Nielsen"
std::get<2>(playerProfile); // "NYI"
std::tie #
创建一个左值引用的元组。用于解包 std::pair
和 std::tuple
对象。使用 std::ignore
作为忽略值的占位符。在 C++17 中,应使用结构化绑定。
// 使用元组...
std::string playerName;
std::tie(std::ignore, playerName, std::ignore) = std::make_tuple(91, "John Tavares", "NYI");
// 使用对...
std::string yes, no;
std::tie(yes, no) = std::make_pair("yes", "no");
std::array #
std::array
是基于 C 风格数组的容器。支持常见的容器操作,如排序。
std::array<int, 3> a = {2, 1, 3};
std::sort(a.begin(), a.end()); // a == { 1, 2, 3 }
for (int& x : a) x *= 2; // a == { 2, 4, 6 }
无序容器 #
这些容器保持平均常数时间复杂度的搜索、插入和删除操作。为了实现常数时间复杂度,牺牲了顺序以换取速度,通过将元素哈希到桶中实现。有四种无序容器:
unordered_set
unordered_multiset
unordered_map
unordered_multimap
std::make_shared #
std::make_shared
是创建 std::shared_ptr
实例的推荐方式,原因如下:
- 避免使用
new
运算符。 - 避免在指定指针持有的底层类型时重复代码。
- 提供异常安全性。假设我们像这样调用一个函数
foo
:
foo(std::shared_ptr<T>{new T{}}, function_that_throws(), std::shared_ptr<T>{new T{}});
编译器可以自由地先调用 new T{}
,然后调用 function_that_throws()
,等等… 由于我们在共享指针的第一次构造中分配了堆内存,因此引入了内存泄漏。使用 std::make_shared
可以提供异常安全性:
foo(std::make_shared<T>(), function_that_throws(), std::make_shared<T>());
- 避免两次分配。当我们调用
std::shared_ptr{ new T{} }
时,我们需要为T
分配内存,然后在共享指针中为控制块分配内存。
参见 智能指针 部分,了解有关 std::unique_ptr
和 std::shared_ptr
的更多信息。
std::ref #
std::ref(val)
用于创建类型为 std::reference_wrapper
的对象,它持有 val
的引用。在通常的引用传递使用 &
无法编译,或者由于类型推导导致 &
被丢弃的情况下使用。std::cref
与之类似,但创建的引用包装器持有对 val
的常量引用。
// 创建一个容器来存储对象的引用。
auto val = 99;
auto _ref = std::ref(val);
_ref++;
auto _cref = std::cref(val);
//_cref++; // 无法编译
std::vector<std::reference_wrapper<int>>vec; // vector<int&>vec 无法编译
vec.push_back(_ref); // vec.push_back(&i) 无法编译
cout << val << endl; // 输出 100
cout << vec[0] << endl; // 输出 100
cout << _cref; // 输出 100
内存模型 #
C++11 为 C++ 引入了内存模型,这意味着库支持线程和原子操作。这些操作包括但不限于原子加载/存储、比较交换、原子标志、承诺、未来、锁和条件变量。
参见:std::thread
std::async #
std::async
以异步或延迟计算的方式运行给定函数,然后返回一个 std::future
,它持有该函数调用的结果。
第一个参数是策略,可以是:
std::launch::async | std::launch::deferred
由实现决定是执行异步操作还是延迟计算。std::launch::async
在新线程上运行可调用对象。std::launch::deferred
在当前线程上执行延迟计算。
int foo() {
/* 在这里执行某些操作,然后返回结果。 */
return 1000;
}
auto handle = std::async(std::launch::async, foo); // 创建一个异步任务
auto result = handle.get(); // 等待结果
std::begin/end #
std::begin
和 std::end
自由函数被添加到以通用方式返回容器的起始和结束迭代器。这些函数也适用于没有 begin
和 end
成员函数的原始数组。
template <typename T>
int CountTwos(const T& container) {
return std::count_if(std::begin(container), std::end(container), [](int item) {
return item == 2;
});
}
std::vector<int> vec = {2, 2, 43, 435, 4543, 534};
int arr[8] = {2, 43, 45, 435, 32, 32, 32, 32};
auto a = CountTwos(vec); // 2
auto b = CountTwos(arr); // 1
致谢 #
- cppreference - 特别适用于查找新库特性的示例和文档。
- C++ 右值引用详解 - 一篇很好的介绍,帮助我理解了右值引用、完美转发和移动语义。
- clang 和 gcc 的标准支持页面。还包括了语言/库特性的提案,我用它们来查找描述、它们要解决的问题以及一些示例。
- 编译器探索器
- Scott Meyers 的《Effective Modern C++》 - 高度推荐的书籍!
- Jason Turner 的 C++ Weekly - 一个很棒的 C++ 相关视频集合。
- 我可以对被移动的对象做什么?
- decltype(auto) 的一些用途是什么?
- 以及我忘记的许多其他 Stack Overflow 帖子…
- 本文翻译自github