现代cpp学习笔记
发布于 2022 年 5 月 1 日,更新于 2022 年 5 月 1 日

尾返回类型

template <typename T, typename U> auto add(T t, U u) -> decltype(t + u) {
  return t + u;
}

初始化列表

class Foo {
public:
  int value_a;
  int value_b;
  Foo(int a, int b) : value_a(a), value_b(b){};
};
 
Foo foo1{1, 2};
Foo foo2(1, 2);
Foo foo3 = {1, 2};
auto foo4 = new Foo(1, 2);
 
 
class MagicFoo {
public:
  std::vector<int> vec;
  MagicFoo(std::initializer_list<int> list) {
    for (auto it = list.begin(); it != list.end(); it++) {
      vec.push_back(*it);
    }
  }
};
 
MagicFoo foo{1, 2, 3, 4, 5};

结构化绑定

// c++17 同时返回多个值
std::tuple<int, double, std::string> f() {
  return std::make_tuple(1, 2.3, "456");
}
 
auto [x, y, z] = f();

委托构造

class Base {
public:
  int value1;
  int value2;
  Base() { value1 = 1; }
  // 可以在一个构造函数中调用另一个
  Base(int value) : Base() { // 委托 Base() 构造函数
    value2 = value;
  }
};

继承构造

class Base {
public:
  int value1;
  int value2;
  Base() { value1 = 1; }
  Base(int value) : Base() { value2 = value; }
};
 
class Subclass : public Base {
public:
  // 使用父类的构造方法
  using Base::Base; // 继承构造
};

显式函数重载

class Base {
  virtual void foo(int);
  virtual void foo2(int) final; // 阻止函数重载
};
 
class SubClass : Base {
  virtual void foo(int) override; // 重载
};
 
// 阻止类SubClass2被继承
class SubClass2 final {};

显式禁用默认函数

class Magic {
public:
  Magic() = default; // 显式声明使用编译器生成的构造
  Magic &operator=(const Magic &) = delete; // 显式声明拒绝编译器生成构造
  Magic(int magic_number);
};

强类型枚举

enum class new_enum : unsigned int { // 默认为int
  value1,
  value2,
  value3 = 100,
  value4 = 100
};
 
// 获得枚举的值
template <typename T>
std::ostream &operator<<(
    typename std::enable_if<std::is_enum<T>::value, std::ostream>::type &stream,
    const T &e) {
  return stream << static_cast<typename std::underlying_type<T>::type>(e);
}

Lambda 表达式

[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
// 函数体
}

值捕获

被捕获的变量在 Lambda 表达式被创建时拷贝, 而非调用时才拷贝

void lambda_value_capture() {
    int value = 1;
    auto copy_value = [value] {
        return value;
    };
    value = 100;
    auto stored_value = copy_value();
    std::cout << "stored_value = " << stored_value << std::endl;
    // 这时, stored_value == 1, 而 value == 100.
    // 因为 copy_value 在创建时就保存了一份 value 的拷贝
}

引用捕获

与引用传参类似,引用捕获保存的是引用,值会发生变化。

void lambda_reference_capture() {
    int value = 1;
    auto copy_value = [&value] {
        return value;
    };
    value = 100;
    auto stored_value = copy_value();
    std::cout << "stored_value = " << stored_value << std::endl;
    // 这时, stored_value == 100, value == 100.
    // 因为 copy_value 保存的是引用
}

隐式捕获

可以在捕获列表中写一个 &\= 向编译器声明采用引用捕获或者值捕获

[] 空捕获列表
[name1, name2, ...] 捕获一系列变量
[&] 引用捕获, 从函数体内的使用确定引用捕获列表
[=] 值捕获, 从函数体内的使用确定值捕获列表

表达式捕获

#include <iostream>
#include <memory>  // std::make_unique
#include <utility> // std::move
 
void lambda_expression_capture() {
    auto important = std::make_unique<int>(1);
    auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
        return x+y+v1+(*v2);
    };
    std::cout << add(3,4) << std::endl;
}

在上面的代码中,important 是一个独占指针,是不能够被 ”=” 值捕获到,这时候我们可以将其转移为右值,在表达式中初始化。

泛型Lambda

从 C++14 开始,Lambda 函数的形式参数可以使用 auto 关键字来产生意义上的泛型:

auto add = [](auto x, auto y) {
    return x+y;
};
 
add(1, 2);
add(1.1, 2.2);

右值引用

左值 (lvalue, left value),顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。

右值 (rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。

纯右值 (prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如 10true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。

字面量除了字符串字面量以外,均为纯右值。而字符串字面量是一个左值,类型为 const char 数组。 数组可以被隐式转换成相对应的指针类型,而转换表达式的结果(如果不是左值引用)则一定是个右值(右值引用为将亡值,否则为纯右值)。

// 正确,"01234" 类型为 const char [6],因此是左值
const char (&left)[6] = "01234";
// 错误,"01234" 是左值,不可被右值引用
// const char (&&right)[6] = "01234";
 
const char*   p   = "01234";  // 正确,"01234" 被隐式转换为 const char*
const char*&& pr  = "01234";  // 正确,"01234" 被隐式转换为 const char*,该转换的结果是纯右值
// const char*& pl = "01234"; // 错误,此处不存在 const char* 类型的左值

将亡值 (xvalue, expiring value),是 C++11 为了引入右值引用而提出的概念(因此在传统 C++ 中, 纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。

右值引用和左值引用

要拿到一个将亡值,就需要用到右值引用:T &&,其中 T 是类型。 右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活。

C++11 提供了 std::move 这个方法将左值参数无条件的转换为右值

std::string lv1 = "string,"; // lv1 是一个左值
// std::string&& r1 = lv1; // 非法, 右值引用不能引用左值
std::string&& rv1 = std::move(lv1); // 合法, std::move可以将左值转移为右值
std::cout << rv1 << std::endl; // string,
 
const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的生命周期
// lv2 += "Test"; // 非法, 常量引用无法被修改
std::cout << lv2 << std::endl; // string,string,
 
std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象生命周期
rv2 += "Test"; // 合法, 非常量引用能够修改临时变量
std::cout << rv2 << std::endl; // string,string,string,Test
 
const int &b = std::move(1); // 合法, 常量左引用允许引用右值

移动语义

class A {
public:
  int *pointer;
  A() : pointer(new int(1)) { std::cout << "构造" << pointer << std::endl; }
  A(A &a) : pointer(new int(*a.pointer)) {
    std::cout << "拷贝" << pointer << std::endl;
  } // 无意义的对象拷贝
  A(A &&a) : pointer(a.pointer) {
    a.pointer = nullptr;
    std::cout << "移动" << pointer << std::endl;
  }
  ~A() {
    std::cout << "析构" << pointer << std::endl;
    delete pointer;
  }
};
 
// 防止编译器优化
A return_rvalue(bool test) {
  A a, b;
  if (test)
    return a; // 等价于 static_cast<A&&>(a);
  else
    return b; // 等价于 static_cast<A&&>(b);
}
int main() {
  A obj = return_rvalue(false);
  std::cout << "obj:" << std::endl;
  std::cout << obj.pointer << std::endl;
  std::cout << *obj.pointer << std::endl;
  return 0;
}
  1. 首先会在 return_rvalue 内部构造两个 A 对象,于是获得两个构造函数的输出;
  2. 函数返回后,产生一个将亡值,被 A 的移动构造(A(A&&))引用,从而延长生命周期,并将这个右值中的指针拿到,保存到了 obj 中,而将亡值的指针被设置为 nullptr,防止了这块内存区域被销毁。
std::string str = "Hello world.";
std::vector<std::string> v;
 
// 将使用 push_back(const T&), 即产生拷贝行为
v.push_back(str);
// 将输出 "str: Hello world."
std::cout << "str: " << str << std::endl;
 
// 将使用 push_back(const T&&), 不会出现拷贝行为
// 而整个字符串会被移动到 vector 中,所以有时候 std::move 会用来减少拷贝出现的开销
// 这步操作后, str 中的值会变为空
v.push_back(std::move(str));
// 将输出 "str: "
std::cout << "str: " << str << std::endl;

完美转发

引用坍缩规则:在传统 C++ 中,我们不能够对一个引用类型继续进行引用, 但 C++ 由于右值引用的出现而放宽了这一做法,从而产生了引用坍缩规则,允许我们对引用进行引用, 既能左引用,又能右引用。但是却遵循如下规则:

函数形参类型实参参数类型推导后函数形参类型
T&左引用T&
T&右引用T&
T&&左引用T&
T&&右引用T&&

因此,模板函数中使用 T&& 不一定能进行右值引用,当传入左值时,此函数的引用将被推导为左值。 更准确的讲,无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型。 这才使得 v 作为左值的成功传递。

所谓完美转发,就是为了让我们在传递参数的时候, 保持原来的参数类型(左引用保持左引用,右引用保持右引用)

#include <iostream>
#include <utility>
void reference(int& v) {
    std::cout << "左值引用" << std::endl;
}
void reference(int&& v) {
    std::cout << "右值引用" << std::endl;
}
template <typename T>
void pass(T&& v) {
    std::cout << "              普通传参: ";
    reference(v);
    std::cout << "       std::move 传参: ";
    reference(std::move(v));
    std::cout << "    std::forward 传参: ";
    reference(std::forward<T>(v));
    std::cout << "static_cast<T&&> 传参: ";
    reference(static_cast<T&&>(v));
}
int main() {
    std::cout << "传递右值:" << std::endl;
    pass(1);
 
    std::cout << "传递左值:" << std::endl;
    int v = 1;
    pass(v);
 
    return 0;
}
传递右值:
             普通传参: 左值引用
       std::move 传参: 右值引用
    std::forward 传参: 右值引用
static_cast<T&&> 传参: 右值引用
传递左值:
             普通传参: 左值引用
       std::move 传参: 右值引用
    std::forward 传参: 左值引用
static_cast<T&&> 传参: 左值引用

std::forward 和 std::move 一样,没有做任何事情,std::move 单纯的将左值转化为右值, std::forward 也只是单纯的将参数做了一个类型的转换,从现象上来看, std::forward<T>(v) 和 static_cast<T&&>(v) 是完全一样的。

为什么在使用循环语句的过程中,auto&& 是最安全的方式? 因为当 auto 被推导为不同的左右引用时,与 && 的坍缩组合是完美转发。

容器

std::vector<int> v;
std::cout << "size:" << v.size() << std::endl;         // 输出 0
std::cout << "capacity:" << v.capacity() << std::endl; // 输出 0
 
// 如下可看出 std::vector 的存储是自动管理的,按需自动扩张
// 但是如果空间不足,需要重新分配更多内存,而重分配内存通常是性能上有开销的操作
v.push_back(1);
v.push_back(2);
v.push_back(3);
std::cout << "size:" << v.size() << std::endl;         // 输出 3
std::cout << "capacity:" << v.capacity() << std::endl; // 输出 4
 
// 这里的自动扩张逻辑与 Golang 的 slice 很像
v.push_back(4);
v.push_back(5);
std::cout << "size:" << v.size() << std::endl;         // 输出 5
std::cout << "capacity:" << v.capacity() << std::endl; // 输出 8
 
// 如下可看出容器虽然清空了元素,但是被清空元素的内存并没有归还
v.clear();
std::cout << "size:" << v.size() << std::endl;         // 输出 0
std::cout << "capacity:" << v.capacity() << std::endl; // 输出 8
 
// 额外内存可通过 shrink_to_fit() 调用返回给系统
v.shrink_to_fit();
std::cout << "size:" << v.size() << std::endl;         // 输出 0
std::cout << "capacity:" << v.capacity() << std::endl; // 输出 0
 
std::array<int, 4> arr = {1, 2, 3, 4};
 
arr.empty(); // 检查容器是否为空
arr.size();  // 返回容纳的元素数
 
// 迭代器支持
for (auto &i : arr)
{
    // ...
}
 
// 用 lambda 表达式排序
std::sort(arr.begin(), arr.end(), [](int a, int b) {
    return b < a;
});
 
// 数组大小参数必须是常量表达式
constexpr int len = 4;
std::array<int, len> arr = {1, 2, 3, 4};
 
// 非法,不同于 C 风格数组,std::array 不会自动退化成 T*
// int *arr_p = arr;
 
void foo(int *p, int len) {
    return;
}
 
std::array<int, 4> arr = {1,2,3,4};
 
// C 风格接口传参
// foo(arr, arr.size()); // 非法, 无法隐式转换
foo(&arr[0], arr.size());
foo(arr.data(), arr.size());
 
// 使用 `std::sort`
std::sort(arr.begin(), arr.end());

std::forward_list 是一个列表容器,使用单向链表实现,使用方法和 std::list 基本类似

其他有序容器: std::map/std::set

C++11 引入了的两组无序容器分别是:std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset

元组

std::pair 的缺陷是显而易见的,只能保存两个元素

关于元组的使用有三个核心的函数:

  1. std::make_tuple: 构造元组
  2. std::get: 获得元组某个位置的值
  3. std::tie: 元组拆包
#include <tuple>
#include <iostream>
 
auto get_student(int id)
{
    // 返回类型被推断为 std::tuple<double, char, std::string>
 
    if (id == 0)
        return std::make_tuple(3.8, 'A', "张三");
    if (id == 1)
        return std::make_tuple(2.9, 'C', "李四");
    if (id == 2)
        return std::make_tuple(1.7, 'D', "王五");
    return std::make_tuple(0.0, 'D', "null");
    // 如果只写 0 会出现推断错误, 编译失败
}
 
int main()
{
    auto student = get_student(0);
    std::cout << "ID: 0, "
    << "GPA: " << std::get<0>(student) << ", "
    << "成绩: " << std::get<1>(student) << ", "
    << "姓名: " << std::get<2>(student) << '\n';
 
    double gpa;
    char grade;
    std::string name;
 
    // 元组进行拆包
    std::tie(gpa, grade, name) = get_student(1);
    std::cout << "ID: 1, "
    << "GPA: " << gpa << ", "
    << "成绩: " << grade << ", "
    << "姓名: " << name << '\n';
}

std::get 除了使用常量获取元组对象外,C++14 增加了使用类型来获取元组中的对象:

std::tuple<std::string, double, double, int> t("123", 4.5, 6.7, 8);
std::cout << std::get<std::string>(t) << std::endl;
std::cout << std::get<double>(t) << std::endl; // 非法, 引发编译期错误
std::cout << std::get<3>(t) << std::endl;

运行期索引

std::get<> 依赖一个编译期的常量,所以下面的方式是不合法的:

int index = 1;
std::get<index>(t);

使用 std::variant<>(C++ 17 引入),提供给 variant<> 的类型模板参数 可以让一个 variant<> 从而容纳提供的几种类型的变量(在其他语言,例如 Python/JavaScript 等,表现为动态类型):

#include <variant>
template <size_t n, typename... T>
constexpr std::variant<T...> _tuple_index(const std::tuple<T...>& tpl, size_t i) {
    if constexpr (n >= sizeof...(T))
        throw std::out_of_range("越界.");
    if (i == n)
        return std::variant<T...>{ std::in_place_index<n>, std::get<n>(tpl) };
    return _tuple_index<(n < sizeof...(T)-1 ? n+1 : 0)>(tpl, i);
}
template <typename... T>
constexpr std::variant<T...> tuple_index(const std::tuple<T...>& tpl, size_t i) {
    return _tuple_index<0>(tpl, i);
}
template <typename T0, typename ... Ts>
std::ostream & operator<< (std::ostream & s, std::variant<T0, Ts...> const & v) {
    std::visit([&](auto && x){ s << x;}, v);
    return s;
}
 
 
int i = 1;
std::cout << tuple_index(t, i) << std::endl;
// 元组合并
auto new_tuple = std::tuple_cat(get_student(1), std::move(t));
 
template <typename T>
auto tuple_len(T &tpl) {
    return std::tuple_size<T>::value;
}
 
// 迭代
for(int i = 0; i != tuple_len(new_tuple); ++i)
    // 运行期索引
    std::cout << tuple_index(new_tuple, i) << std::endl;

智能指针

对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间, 也就是我们常说的 RAII 资源获取即初始化技术。

std::shared_ptr 是一种智能指针,它能够记录多少个 shared_ptr 共同指向一个对象,从而消除显式的调用 delete,当引用计数变为零的时候就会将对象自动删除。

#include <iostream>
#include <memory>
void foo(std::shared_ptr<int> i) {
    (*i)++;
}
int main() {
    // auto pointer = new int(10); // illegal, no direct assignment
    // Constructed a std::shared_ptr
    auto pointer = std::make_shared<int>(10);
    foo(pointer);
    std::cout << *pointer << std::endl; // 11
    // The shared_ptr will be destructed before leaving the scope
    return 0;
}

std::shared_ptr 可以通过 get() 方法来获取原始指针,通过 reset() 来减少一个引用计数, 并通过use_count()来查看一个对象的引用计数。

auto pointer = std::make_shared<int>(10);
auto pointer2 = pointer; // 引用计数+1
auto pointer3 = pointer; // 引用计数+1
int *p = pointer.get();  // 这样不会增加引用计数
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl;   // 3
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3
 
pointer2.reset();
std::cout << "reset pointer2:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl;   // 2
std::cout << "pointer2.use_count() = "
          << pointer2.use_count() << std::endl;           // pointer2 已 reset; 0
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2
pointer3.reset();
std::cout << "reset pointer3:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl;   // 1
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0
std::cout << "pointer3.use_count() = "
          << pointer3.use_count() << std::endl;           // pointer3 已 reset; 0

std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全:

std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique 从 C++14 引入
std::unique_ptr<int> pointer2 = pointer; // 非法
 
template<typename T, typename ...Args>
std::unique_ptr<T> make_unique( Args&& ...args ) {
  return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) );
}

既然是独占,换句话说就是不可复制。但是,我们可以利用 std::move 将其转移给其他的 unique_ptr,例如:

#include <iostream>
#include <memory>
 
struct Foo {
    Foo() { std::cout << "Foo::Foo" << std::endl; }
    ~Foo() { std::cout << "Foo::~Foo" << std::endl; }
    void foo() { std::cout << "Foo::foo" << std::endl; }
};
 
void f(const Foo &) {
    std::cout << "f(const Foo&)" << std::endl;
}
 
int main() {
    std::unique_ptr<Foo> p1(std::make_unique<Foo>());
    // p1 不空, 输出
    if (p1) p1->foo();
    {
        std::unique_ptr<Foo> p2(std::move(p1));
        // p2 不空, 输出
        f(*p2);
        // p2 不空, 输出
        if(p2) p2->foo();
        // p1 为空, 无输出
        if(p1) p1->foo();
        p1 = std::move(p2);
        // p2 为空, 无输出
        if(p2) p2->foo();
        std::cout << "p2 被销毁" << std::endl;
    }
    // p1 不空, 输出
    if (p1) p1->foo();
    // Foo 的实例会在离开作用域时被销毁
}

std::weak_ptr是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)。弱引用不会引起引用计数增加。

std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作,它可以用于检查 std::shared_ptr 是否存在,其 expired() 方法能在资源未被释放时,会返回 false,否则返回 true;除此之外,它也可以用于获取指向原始对象的 std::shared_ptr 指针,其 lock() 方法在原始对象未被释放时,返回一个指向原始对象的 std::shared_ptr 指针,进而访问原始对象的资源,否则返回nullptr

自定义删除器:

auto loggingDel = [](Widget *pw)        //自定义删除器
                  {                     //(和条款18一样)
                      makeLogEntry(pw);
                      delete pw;
                  };
 
std::unique_ptr<                        //删除器类型是
    Widget, decltype(loggingDel)        //指针类型的一部分
    > upw(new Widget, loggingDel);
std::shared_ptr<Widget>                 //删除器类型不是
    spw(new Widget, loggingDel);        //指针类型的一部分

成员函数

Rule of Three: 如果你声明了拷贝构造函数,拷贝赋值运算符,或者析构函数三者之一,你应该也声明其余两个

C++11对于特殊成员函数处理的规则如下:

  • 默认构造函数:和C++98规则相同。仅当类不存在用户声明的构造函数时才自动生成。
  • 析构函数:基本上和C++98相同;稍微不同的是现在析构默认noexcept 。和C++98一样,仅当基类析构为虚函数时该类析构才为虚函数。
  • 拷贝构造函数:和C++98运行时行为一样:逐成员拷贝non-static数据。仅当类没有用户定义的拷贝构造时才生成。如果类声明了移动操作它就是delete的。当用户声明了拷贝赋值或者析构,该函数自动生成已被废弃。
  • 拷贝赋值运算符:和C++98运行时行为一样:逐成员拷贝赋值non-static数据。仅当类没有用户定义的拷贝赋值时才生成。如果类声明了移动操作它就是delete的。当用户声明了拷贝构造或者析构,该函数自动生成已被废弃。
  • 移动构造函数移动赋值运算符:都对非static数据执行逐成员移动。仅当类没有用户定义的拷贝操作,移动操作或析构时才自动生成。

多态基类通常有一个虚析构函数,因为如果它们非虚,一些操作(比如通过一个基类指针或者引用对派生类对象使用delete或者typeid)会产生未定义或错误结果。除非类继承了一个已经是virtual的析构函数,否则要想析构函数为虚函数的唯一方法就是加上virtual关键字。通常,默认实现是对的,\= default是一个不错的方式表达默认实现。然而用户声明的析构函数会抑制编译器生成移动操作,所以如果该类需要具有移动性,就为移动操作加上\= default。声明移动会抑制拷贝生成,所以如果拷贝性也需要支持,再为拷贝操作加上\= default

class Base {
public:
    virtual ~Base() = default;              //使析构函数virtual
 
    Base(Base&&) = default;                 //支持移动
    Base& operator=(Base&&) = default;
 
    Base(const Base&) = default;            //支持拷贝
    Base& operator=(const Base&) = default;

};

https://cntransgroup.github.io/EffectiveModernCppChinese/3.MovingToModernCpp/item17.html

正则表达式

一般的解决方案就是使用 boost 的正则表达式库。 而 C++11 正式将正则表达式的的处理方法纳入标准库的行列,从语言级上提供了标准的支持, 不再依赖第三方。

std::regex base_regex("([a-z]+)\\.txt");
std::smatch base_match;
for(const auto &fname: fnames) {
    if (std::regex_match(fname, base_match, base_regex)) {
        // std::smatch 的第一个元素匹配整个字符串
        // std::smatch 的第二个元素匹配了第一个括号表达式
        if (base_match.size() == 2) {
            std::string base = base_match[1].str();
            std::cout << "sub-match[0]: " << base_match[0].str() << std::endl;
            std::cout << fname << " sub-match[1]: " << base << std::endl;
        }
    }
}

并行与并发

mutex

std::thread 用于创建一个执行的线程实例,所以它是一切并发编程的基础,使用时需要包含 <thread> 头文件, 它提供了很多基本的线程操作,例如 get_id() 来获取所创建线程的线程 ID,使用 join() 来等待一个线程结束(与该线程汇合)等等

#include <iostream>
#include <thread>
 
int main() {
    std::thread t([](){
        std::cout << "hello world." << std::endl;
    });
    t.join();
    return 0;
}

std::mutex 是 C++11 中最基本的互斥量类,可以通过构造 std::mutex 对象创建互斥量, 而通过其成员函数 lock() 可以进行上锁,unlock() 可以进行解锁。 但是在实际编写代码的过程中,最好不去直接调用成员函数, 因为调用成员函数就需要在每个临界区的出口处调用 unlock(),当然,还包括异常。 而 C++11 为互斥量提供了一个 RAII 机制的模板类 std::lock_guard

在 RAII 用法下,对于临界区的互斥量的创建只需要在作用域的开始部分:

#include <iostream>
#include <mutex>
#include <thread>
 
int v = 1;
 
void critical_section(int change_v) {
    static std::mutex mtx;
    std::lock_guard<std::mutex> lock(mtx);
 
    // 执行竞争操作
    v = change_v;
 
    // 离开此作用域后 mtx 会被释放
}
 
int main() {
    std::thread t1(critical_section, 2), t2(critical_section, 3);
    t1.join();
    t2.join();
 
    std::cout << v << std::endl;
    return 0;
}

std::unique_lock 则是相对于 std::lock_guard 出现的,std::unique_lock 更加灵活, std::unique_lock 的对象会以独占所有权(没有其他的 unique_lock 对象同时拥有某个 mutex 对象的所有权) 的方式管理 mutex 对象上的上锁和解锁的操作。所以在并发编程中,推荐使用 std::unique_lock

#include <iostream>
#include <mutex>
#include <thread>
 
int v = 1;
 
void critical_section(int change_v) {
    static std::mutex mtx;
    std::unique_lock<std::mutex> lock(mtx);
    // 执行竞争操作
    v = change_v;
    std::cout << v << std::endl;
    // 将锁进行释放
    lock.unlock();
 
    // 在此期间,任何人都可以抢夺 v 的持有权
 
    // 开始另一组竞争操作,再次加锁
    lock.lock();
    v += 1;
    std::cout << v << std::endl;
}
 
int main() {
    std::thread t1(critical_section, 2), t2(critical_section, 3);
    t1.join();
    t2.join();
    return 0;
}

future

C++11 提供的 std::future 可以用来获取异步任务的结果。 自然地,我们很容易能够想象到把它作为一种简单的线程同步手段,即屏障(barrier)。

为了看一个例子,我们这里额外使用 std::packaged_task,它可以用来封装任何可以调用的目标,从而用于实现异步的调用。 举例来说:

#include <iostream>
#include <future>
#include <thread>
 
int main() {
    // 将一个返回值为7的 lambda 表达式封装到 task 中
    // std::packaged_task 的模板参数为要封装函数的类型
    std::packaged_task<int()> task([](){return 7;});
    // 获得 task 的期物
    std::future<int> result = task.get_future(); // 在一个线程中执行 task
    std::thread(std::move(task)).detach();
    std::cout << "waiting...";
    result.wait(); // 在此设置屏障,阻塞到期物的完成
    // 输出执行结果
    std::cout << "done!" << std:: endl << "future result is "
              << result.get() << std::endl;
    return 0;
}

condition variable

条件变量 std::condition_variable 是为了解决死锁而生,当互斥操作不够用而引入的。 比如,线程可能需要等待某个条件为真才能继续执行, 而一个忙等待循环中可能会导致所有其他线程都无法进入临界区使得条件为真时,就会发生死锁。 所以,condition_variable 对象被创建出现主要就是用于唤醒等待线程从而避免死锁。 std::condition_variablenotify_one() 用于唤醒一个线程; notify_all() 则是通知所有线程。下面是一个生产者和消费者模型的例子:

#include <queue>
#include <chrono>
#include <mutex>
#include <thread>
#include <iostream>
#include <condition_variable>
 
 
int main() {
    std::queue<int> produced_nums;
    std::mutex mtx;
    std::condition_variable cv;
    bool notified = false;  // 通知信号
 
    // 生产者
    auto producer = [&]() {
        for (int i = 0; ; i++) {
            std::this_thread::sleep_for(std::chrono::milliseconds(900));
            std::unique_lock<std::mutex> lock(mtx);
            std::cout << "producing " << i << std::endl;
            produced_nums.push(i);
            notified = true;
            cv.notify_all(); // 此处也可以使用 notify_one
        }
    };
    // 消费者
    auto consumer = [&]() {
        while (true) {
            std::unique_lock<std::mutex> lock(mtx);
            while (!notified) {  // 避免虚假唤醒
                cv.wait(lock);
            }
            // 短暂取消锁,使得生产者有机会在消费者消费空前继续生产
            lock.unlock();
            // 消费者慢于生产者
            std::this_thread::sleep_for(std::chrono::milliseconds(1000));
            lock.lock();
            while (!produced_nums.empty()) {
                std::cout << "consuming " << produced_nums.front() << std::endl;
                produced_nums.pop();
            }
            notified = false;
        }
    };
 
    // 分别在不同的线程中运行
    std::thread p(producer);
    std::thread cs[2];
    for (int i = 0; i < 2; ++i) {
        cs[i] = std::thread(consumer);
    }
    p.join();
    for (int i = 0; i < 2; ++i) {
        cs[i].join();
    }
    return 0;
}
现代cpp学习笔记
作者发布于许可
Connor2022 年 5 月 1 日CC BY-NC-SA 4.0
 2025 Connor. All Rights Reserved. / RSS
Powered by Astro & React