本文记录了阅读 BS_thread_pool 源码时的一些知识点,最好结合源码一起食用。
BS_thread_pool 是 github 一个开源的线程池,源码地址:thread-pool

# i、invoke_result_t

1
using concurrency_t = std::invoke_result_t<decltype(std::thread::hardware_concurrency)>;

这行代码定义了一个别名 concurrency_t ,该别名代表 std::thread::hardware_concurrency 函数的返回类型。以下是对这行代码的详细解释:

分解解释

  1. std::thread::hardware_concurrency :

    • std::thread::hardware_concurrency 是一个静态成员函数,用于返回系统中可用并发线程的数量。该函数的返回类型通常是 unsigned int
  2. decltype(std::thread::hardware_concurrency) :

    • decltype 关键字用于查询表达式的类型。 decltype(std::thread::hardware_concurrency) 返回 std::thread::hardware_concurrency 函数的类型。
    • 因为 std::thread::hardware_concurrency 是一个不接受任何参数的函数,所以 decltype(std::thread::hardware_concurrency) 返回的是一个函数类型(不是返回值类型,而是函数本身的类型)。
  3. std::invoke_result_t<...> :

    • std::invoke_result_t 是一个类型别名模板,用于获取调用给定可调用对象的结果类型。在这种情况下, std::invoke_result_t<decltype(std::thread::hardware_concurrency)> 表示调用 std::thread::hardware_concurrency 函数的结果类型。
    • 换句话说, std::invoke_result_t<decltype(std::thread::hardware_concurrency)> 的作用是确定 std::thread::hardware_concurrency() 调用的返回类型。

组合解释

通过组合这些部分,整行代码的作用如下:

  • decltype(std::thread::hardware_concurrency) :获取 std::thread::hardware_concurrency 函数的类型。
  • std::invoke_result_t<decltype(std::thread::hardware_concurrency)> :获取调用 std::thread::hardware_concurrency() 函数的返回类型。
  • using concurrency_t = std::invoke_result_t<decltype(std::thread::hardware_concurrency)> :定义一个别名 concurrency_t ,该别名代表 std::thread::hardware_concurrency() 的返回类型。

代码效果

这个别名的实际效果是为 std::thread::hardware_concurrency 的返回类型提供一个简短的名称。由于 std::thread::hardware_concurrency 通常返回 unsigned int ,所以在大多数情况下, concurrency_t 就是 unsigned int

# i、nodiscard class

在一个类的定义中,直接在 class 后面加 nodiscard 是什么作用?

1
2
3
4
class [[nodiscard]] thread_pool
{
...
}

在 C++17 及其后的标准中, [[nodiscard]] 属性(attribute)被引入,用于指示特定函数或返回类型的结果不应被忽略当应用于类时,它会要求所有返回该类类型的对象必须被使用。如果忽略了这样的返回值,编译器会生成一个警告。这对于提示程序员注意某些操作的结果是非常有用的,避免忽略可能的重要结果。

[[nodiscard]] 应用于一个类时,如果一个函数返回该类的对象并且调用者忽略了这个返回值,编译器将会发出一个警告。例如:

1
2
3
4
5
6
7
8
9
thread_pool create_thread_pool() {
return thread_pool();
}

int main() {
create_thread_pool(); // 这里会产生警告,因为返回的 thread_pool 对象被忽略了
auto pool = create_thread_pool(); // 这里不会产生警告,因为返回的对象被使用了
return 0;
}

在这个例子中, create_thread_pool 函数返回一个 thread_pool 对象。
如果调用 create_thread_pool 时忽略了其返回值,编译器会生成一个警告,提示你可能忘记处理这个对象。

目的和优点

  1. 防止资源泄漏:对于表示资源管理的类,如线程池、文件句柄或智能指针,忽略返回值可能导致资源未正确管理。 [[nodiscard]] 可以提醒程序员必须处理这些对象。
  2. 强制检查操作结果:有些操作可能失败,返回一个错误对象或状态对象。如果忽略了这些返回值,程序可能无法正确处理错误情况。
  3. 提高代码安全性:通过强制检查返回值,可以减少因忽略重要操作结果而引入的潜在错误。

具体应用场景

使用 [[nodiscard]] 属性的典型场景包括但不限于:

  1. 资源管理类:如线程池、文件操作类、网络连接类等。
  2. 错误处理类:如表示错误状态或异常信息的类。
  3. 智能指针:如自定义的智能指针实现。

nodiscard 是一种属性说明符,在 属性说明符 中还介绍了其他说明符。

# i、委托构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class [[nodiscard]] thread_pool
{
public:
thread_pool() : thread_pool(0, [] {}) {}

explicit thread_pool(const concurrency_t num_threads) : thread_pool(num_threads, [] {}) {}

explicit thread_pool(const std::function<void()>& init_task) : thread_pool(0, init_task) {}

thread_pool(const concurrency_t num_threads, const std::function<void()>& init_task) : thread_count(determine_thread_count(num_threads)), threads(std::make_unique<std::thread[]>(determine_thread_count(num_threads)))
{
create_threads(init_task);
}

...
}

在 C++ 中,构造函数可以调用其他构造函数来初始化对象。这种技术被称为 “构造函数委托”(constructor delegation),可以简化构造函数的实现,避免重复代码。

  1. 默认构造函数
    1
    thread_pool() : thread_pool(0, [] {}) {}

这个默认构造函数实际上是委托给另一个构造函数 thread_pool(const concurrency_t num_threads, const std::function<void()>& init_task) ,它传递了两个参数: 0[] {} 。这里的 0 表示使用默认的线程数量, [] {} 是一个空的 lambda 函数,表示没有初始化任务。

  1. 带线程数的构造函数
    1
    explicit thread_pool(const concurrency_t num_threads) : thread_pool(num_threads, [] {}) {}

这个构造函数接收一个参数 num_threads ,表示线程池的线程数量。它委托给另一个构造函数,传递 num_threads 和一个空的 lambda 函数 [] {} ,表示没有初始化任务。

  1. 带初始化任务的构造函数
    1
    explicit thread_pool(const std::function<void()>& init_task) : thread_pool(0, init_task) {}

这个构造函数接收一个参数 init_task ,表示初始化任务。它委托给另一个构造函数,传递 0init_task0 表示使用默认的线程数量。

  1. 主构造函数
    1
    2
    3
    4
    5
    6
    thread_pool(const concurrency_t num_threads, const std::function<void()>& init_task)
    : thread_count(determine_thread_count(num_threads)),
    threads(std::make_unique<std::thread[]>(determine_thread_count(num_threads)))
    {
    create_threads(init_task);
    }

这个构造函数接收两个参数: num_threadsinit_task 。它初始化 thread_countthreads ,然后调用 create_threads(init_task) 来创建线程,并执行初始化任务。

通过构造函数委托,你可以避免在每个构造函数中重复代码。每个构造函数只需要提供必要的参数,并将其委托给主构造函数进行实际的初始化。这使得代码更加简洁和易于维护。

关于委托构造函数的更多信息,可参考:委托构造函数

# i、explicit

追问:为什么这里的其中两个构造函数前面加了 explicit?

在 C++ 中, explicit 关键字用于防止构造函数和转换运算符在某些情况下被隐式调用,从而避免意外的类型转换或构造函数调用。使用 explicit 可以增加代码的可读性和安全性,减少因为隐式转换带来的潜在问题。

这两个构造函数分别是:

1
2
explicit thread_pool(const concurrency_t num_threads) : thread_pool(num_threads, [] {}) {}
explicit thread_pool(const std::function<void()>& init_task) : thread_pool(0, init_task) {}

  1. explicit thread_pool(const concurrency_t num_threads)

如果不加 explicit ,这个构造函数会允许隐式转换。例如:

1
thread_pool pool = 4;  // 可能会导致意外的隐式转换

加上 explicit 后,这种隐式转换将被禁止,必须显式调用构造函数:

1
thread_pool pool(4);  // 必须显式调用构造函数

  1. explicit thread_pool(const std::function<void()>& init_task)

类似地,如果不加 explicit ,这个构造函数也会允许隐式转换。例如:

1
2
std::function<void()> init_task = [] {};
thread_pool pool = init_task; // 可能会导致意外的隐式转换

加上 explicit 后,这种隐式转换也将被禁止,必须显式调用构造函数:

1
thread_pool pool(init_task);  // 必须显式调用构造函数

通过在构造函数前加 explicit ,可以防止对象在不小心的隐式转换中被意外构造。这对于构造函数接受单一参数的情况特别有用,因为这种情况下编译器会自动尝试进行隐式转换。如果没有 explicit ,程序员可能会在不经意间触发这些转换,从而导致难以调试的错误。

使用 explicit 可以增强代码的可读性和安全性,强制要求调用者显式地创建对象,避免意外的隐式转换行为。

# i、 std::unique_ptr<T[]>

1
std::unique_ptr<std::thread[]> threads = nullptr;

这行代码创建了一个名为 threads 的变量,该变量是一个指向 std::thread 对象数组的智能指针。

对于 std::unique_ptr<std::thread[]> :

  • std::unique_ptr 是一个模板类,用于独占式地管理动态分配的资源(通常是通过 new 分配的内存)。当 std::unique_ptr 对象被销毁时,它会自动释放所管理的资源。
  • std::unique_ptr<T[]>std::unique_ptr 的一个特殊实例,用于管理动态分配的数组。与管理单个对象的 std::unique_ptr<T> 不同, std::unique_ptr<T[]> 用于管理数组(即动态分配的多个对象)。
  • 在这个例子中, Tstd::thread ,所以 std::unique_ptr<std::thread[]> 是一个指向 std::thread 对象数组的智能指针。

这行代码的作用是声明并初始化一个智能指针 threads ,它能够独占地管理一个 std::thread 对象数组,但在初始化时并没有分配任何 std::thread 对象数组。此时, threads 是一个空指针,不指向任何内存。

于是,进一步地,我们回过头看刚才的主构造函数:

1
2
3
4
5
6
thread_pool(const concurrency_t num_threads, const std::function<void()>& init_task)
: thread_count(determine_thread_count(num_threads)),
threads(std::make_unique<std::thread[]>(determine_thread_count(num_threads)))
{
create_threads(init_task);
}

这里的 std::make_unique<std::thread[]>(...)

  • std::make_unique 是一个用于创建 std::unique_ptr 的工厂函数。它在 C++14 中引入,用于安全且简洁地创建 std::unique_ptr 对象。
  • std::make_unique<std::thread[]> 表示创建一个指向 std::thread 对象数组的 std::unique_ptr
  • std::make_unique<std::thread[]>(n) 创建一个包含 nstd::thread 对象的数组,并返回一个指向该数组的 std::unique_ptr

# i、thread 绑定成员函数

1
2
3
4
5
6
7
8
9
10
11
12
void create_threads(const std::function<void()>& init_task)
{
{
const std::scoped_lock tasks_lock(tasks_mutex);
tasks_running = thread_count;
workers_running = true;
}
for (concurrency_t i = 0; i < thread_count; ++i)
{
threads[i] = std::thread(&thread_pool::worker, this, i, init_task);
}
}

create_threads 函数的作用是:

  1. 使用一个 std::scoped_lock 来保护对共享资源的访问,确保 tasks_runningworkers_running 的设置是线程安全的。(关于 scoped_lock 的更多细节,见 scpoed_lock
  2. 使用一个循环来创建和启动多个线程,并将这些线程存储在 threads 数组中。每个线程都会调用 thread_pool 类的成员函数 worker ,并传递当前线程的索引和初始化任务。

这个函数有效地初始化了一个线程池,其中每个线程在启动时都会执行 worker 函数,并在启动之前执行 init_task 函数。

这里具体来看看线程的创建:

  • threads[i] = std::thread(&thread_pool::worker, this, i, init_task); :在 threads 数组的第 i 个位置创建一个新的 std::thread 对象,并启动该线程。
    • std::thread(&thread_pool::worker, this, i, init_task) :创建一个新的线程,并将其绑定到 thread_pool 类的成员函数 worker 上。
      • &thread_pool::worker :指向 worker 成员函数的指针。
      • this :指向当前的 thread_pool 实例。
      • i :传递给 worker 函数的线程索引。
      • init_task :传递给 worker 函数的初始化任务。

进一步地,我们关注 thread 参数的传递:—— 为什么要传递 this 指针?
threads[i] = std::thread(&thread_pool::worker, this, i, init_task); 这行代码创建并启动了一个新线程,该线程将执行 thread_pool 类的成员函数 worker 。为了理解这行代码,尤其是为什么要传递 this 指针,我们需要详细解释一下成员函数绑定和线程创建的过程。

成员函数绑定

在 C++ 中,成员函数和普通的非成员函数不同,成员函数需要一个对象实例来调用它。成员函数实际上需要两个东西:

  1. 成员函数本身的地址。
  2. 一个对象实例(通过 this 指针)来调用该成员函数。

代码解释

1
threads[i] = std::thread(&thread_pool::worker, this, i, init_task);

这行代码的每个部分解释如下:

  1. std::thread 构造函数:

    • std::thread 是 C++ 标准库中的一个类,用于创建和管理线程。
    • std::thread 的构造函数可以接受一个可调用对象(函数指针、成员函数指针、lambda 表达式等)和一组参数。这些参数会传递给可调用对象。
  2. &thread_pool::worker :

    • &thread_pool::worker 是一个指向 thread_pool 类成员函数 worker 的指针。
    • 由于 worker 是一个成员函数,它需要一个 thread_pool 实例来调用。
  3. this :

    • this 指针指向当前的 thread_pool 实例。
    • 通过传递 this 指针, std::thread 构造函数能够知道在哪个 thread_pool 实例上调用 worker 成员函数。
  4. 参数传递:

    • iinit_task 是传递给 worker 函数的参数。

当你创建一个新的线程并希望在线程中运行一个成员函数时,你需要告诉线程该成员函数属于哪个对象实例。通过传递 this 指针,新的线程就知道在当前 thread_pool 实例上调用 worker 函数。

传递 this 指针是为了让新创建的线程知道在哪个对象实例上调用成员函数。这样,成员函数就能够正确访问和操作该对象实例的成员变量和其他成员函数。这是成员函数与非成员函数的一个重要区别,确保在多线程环境中能够正确地工作。

对于 thread 的使用,可参考 [[thread# 构造函数及其参数 | thread]]

# i、移动拷贝和移动赋值

1
2
3
4
thread_pool(const thread_pool&) = delete;
thread_pool(thread_pool&&) = delete;
thread_pool& operator=(const thread_pool&) = delete;
thread_pool& operator=(thread_pool&&) = delete;

这里禁用了拷贝构造、移动构造和移动赋值函数。

关于移动拷贝和移动赋值,下面是一些使用示例:

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
#include <iostream>
#include <utility> // For std::move

class MyClass {
public:
int* data;

// 构造函数
MyClass(int value) : data(new int(value)) {
std::cout << "Constructed with value: " << value << std::endl;
}

// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr; // 移动后,源对象的指针被置为nullptr
std::cout << "Move constructed" << std::endl;
}

// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if(this != &other) {
delete data; // 释放当前对象的资源
data = other.data; // 转移资源
other.data = nullptr; // 源对象的指针被置为nullptr
std::cout << "Move assigned" << std::endl;
}
return *this;
}

// 析构函数
~MyClass() {
delete data;
std::cout << "Destroyed" << std::endl;
}
};

int main() {
// 使用构造函数创建对象
MyClass a(10);

// 使用移动构造函数
MyClass b(std::move(a)); // 触发移动构造函数

// 使用构造函数创建另一个对象
MyClass c(20);

// 使用移动赋值运算符
c = std::move(b); // 触发移动赋值运算符

return 0;
}

  • 移动构造和移动赋值的意义:通过移动构造和移动赋值,我们可以高效地转移资源,避免不必要的深拷贝。
  • 删除移动构造和移动赋值:在某些情况下,为了确保资源管理的安全性,可以禁用移动构造函数和移动赋值运算符,防止对象被意外地移动。

# i、mutable mutex

1
mutable std::mutex tasks_mutex = {};

这行代码定义并初始化了一个可变的互斥锁( std::mutex ),这里主要讨论 mutable:

mutable 关键字允许即使在一个 const 成员函数中也可以修改这个成员变量。通常,成员变量在 const 成员函数中是不可修改的,但使用 mutable 关键字可以使这个限制失效。

在多线程编程中,一个常见的使用场景是,当一个类的成员函数声明为 const 时,我们可能仍需要锁定互斥锁来保护共享数据。这时就需要将互斥锁声明为 mutable

总得来说,这行代码定义了一个名为 tasks_mutex 的互斥锁,并将其声明为 mutable ,这样它可以在任何成员函数中(包括 const 成员函数)被修改。默认初始化器 {} 用于默认构造 tasks_mutex ,即互斥锁处于未锁定状态。

# i、lambda 表达式捕获 this 指针

1
2
3
4
5
6
7
8
9
10
11
void wait()
{
std::unique_lock tasks_lock(tasks_mutex);
waiting = true;
tasks_done_cv.wait(tasks_lock,
[this]
{
return (tasks_running == 0) && BS_THREAD_POOL_PAUSED_OR_EMPTY;
});
waiting = false;
}

这段代码定义了一个名为 wait 的成员函数,它的作用是在某些条件满足之前使当前线程进入等待状态。它使用 std::unique_lockstd::condition_variable 来实现线程同步。

这里主要讨论 std::condition_variable 的 wait 以及 lambda 表达式:

  1. tasks_done_cv.wait
    1
    2
    3
    4
    5
    tasks_done_cv.wait(tasks_lock,
    [this]
    {
    return (tasks_running == 0) && BS_THREAD_POOL_PAUSED_OR_EMPTY;
    });

这里, tasks_done_cv 是一个 std::condition_variable 对象,用于实现线程之间的等待和通知机制。

  • tasks_done_cv.wait(tasks_lock, ...) :这行代码让当前线程进入等待状态,直到条件变量被通知并且条件表达式返回 true
  • tasks_lock :条件变量等待时会释放这个锁,等到被通知后再重新锁定它。
  • [this] { ... } :这是一个 lambda 表达式,用于定义等待的条件。
  1. Lambda 表达式
    1
    2
    3
    4
    [this]
    {
    return (tasks_running == 0) && BS_THREAD_POOL_PAUSED_OR_EMPTY;
    }

这个 lambda 表达式捕获 this 指针,访问类的成员变量,并返回一个布尔值。具体条件是:

  • (tasks_running == 0) :表示没有正在运行的任务。
  • BS_THREAD_POOL_PAUSED_OR_EMPTY :一个宏或常量,表示线程池要么处于暂停状态,要么任务队列为空。

只有在这两个条件都满足时,lambda 表达式才会返回 true ,从而使 wait 函数解除等待状态。

追问:这里这个 wait 本身就已经是成员函数了,为什么在条件变量的 wait 函数中传递的 lambda 表达式还需要捕获 this 指针呢?

在 C++ 中,成员函数会隐式地包含一个 this 指针,用于指向调用该函数的对象。然而,lambda 表达式默认不捕获任何外部变量,包括成员函数的 this 指针。如果需要在 lambda 表达式中访问成员变量或成员函数,必须显式地捕获 this 指针。

在上述代码中,lambda 表达式需要访问类的成员变量 tasks_runningBS_THREAD_POOL_PAUSED_OR_EMPTY 。为了使 lambda 表达式能够访问这些成员变量,必须捕获 this 指针。具体来说, this 指针允许 lambda 表达式访问当前对象的成员。

如果不捕获 this 指针,lambda 表达式将无法访问 tasks_runningBS_THREAD_POOL_PAUSED_OR_EMPTY ,编译器会报错。

lambda 表达式默认不捕获任何外部变量,包括成员函数的 this 指针。如果需要在 lambda 表达式中访问成员变量或成员函数,必须显式地捕获 this 指针。因此,在代码中,为了在 lambda 表达式中访问 tasks_runningBS_THREAD_POOL_PAUSED_OR_EMPTY ,必须捕获 this 指针。

# i、优先队列

1
2
3
4
5
#ifdef BS_THREAD_POOL_ENABLE_PRIORITY
std::priority_queue<pr_task> tasks = {};
#else
std::queue<std::function<void()>> tasks = {};
#endif

  1. std::priority_queue<pr_task> tasks = {};

    • 声明并初始化一个名为 tasks 的优先级队列。这个优先级队列用于存储类型为 pr_task 的任务。 pr_task 应该是一个自定义类型,用于表示带有优先级的任务。
    • 优先级队列 std::priority_queue 会根据任务的优先级来自动排序,保证高优先级的任务先被处理。
  2. std::queue<std::function<void()>> tasks = {};

    • 声明并初始化一个名为 tasks 的普通队列。这个普通队列用于存储 std::function<void()> 类型的任务。
    • 普通队列 std::queue 是一个先进先出(FIFO)的数据结构,任务会按照添加的顺序依次被处理。

这里展开讲一下 优先队列

std::priority_queue 是一种适用于需要快速访问最大(或最小)元素的场景的容器。它通常用于调度任务、管理事件或需要优先处理的其他情况。 std::priority_queue 本质上是一个最大堆(max-heap),默认情况下它总是保证堆顶元素是最大值。

  1. 应用场景
  • 任务调度:需要根据优先级处理任务时, std::priority_queue 可以确保高优先级任务先被处理。
  • 事件管理:在模拟系统中,可以使用优先队列来处理时间戳最小的事件。
  • 路径搜索算法:如 Dijkstra 算法和 A* 算法,用于管理待处理节点。
  1. 底层原理
    std::priority_queue 通常基于堆(heap)数据结构实现。堆是一种特殊的二叉树,可以用数组来表示。最大堆的特点是每个节点的值都大于或等于其子节点的值。C++ 标准库使用 std::vector 作为底层容器,并使用 std::push_heapstd::pop_heap 等算法来维护堆的性质。

  2. 普通类型的使用
    默认情况下, std::priority_queue 是最大堆。如果需要最小堆,可以使用 std::greater<T> 比较器

  3. 自定义复杂类型的使用

对于自定义类型,需要提供比较器来定义优先级 —— 实质上就是重载 < 运算符。
例如,假设我们有一个 Task 结构:

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
#include <iostream>
#include <queue>
#include <vector>
#include <functional>

struct Task {
int priority;
std::string description;

// 自定义比较器,用于优先队列
bool operator<(const Task& other) const {
return priority < other.priority; // 优先级越高越优先
}
};

int main() {
std::priority_queue<Task> task_pq;

task_pq.push({1, "Low priority task"});
task_pq.push({3, "High priority task"});
task_pq.push({2, "Medium priority task"});

while(!task_pq.empty()) {
const Task& task = task_pq.top();
std::cout << "Priority: " << task.priority << ", Description: " << task.description << std::endl;
task_pq.pop();
} return 0;
}

上述代码将输出:

1
2
3
Priority: 3, Description: High priority task
Priority: 2, Description: Medium priority task
Priority: 1, Description: Low priority task

std::priority_queue 是一个强大的数据结构,适用于许多需要优先级处理的场景。通过灵活地定义比较器和底层容器,可以适应各种具体需求。

此时我们再回过头来看 pr_task 的定义,便一目了然:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class [[nodiscard]] pr_task
{
friend class thread_pool;

public:
explicit pr_task(const std::function<void()>& task_, const priority_t priority_ = 0) : task(task_), priority(priority_) {}

explicit pr_task(std::function<void()>&& task_, const priority_t priority_ = 0) : task(std::move(task_)), priority(priority_) {}

[[nodiscard]] friend bool operator<(const pr_task& lhs, const pr_task& rhs)
{
return lhs.priority < rhs.priority;
}

private:
std::function<void()> task = {};
priority_t priority = 0;
};

# i、std::optional

#CPP17
std::optional 是 C++17 引入的一个标准库模板类,用于表示一个可能包含或不包含值的对象。它是一个类型安全的替代方案,用于表示函数的返回值可能为空的情况,避免使用指针或特定的标记值。

# std::optional 的定义

std::optional 是一个模板类,其定义类似于:

1
2
template <typename T>
class optional;

它可以包含一个类型为 T 的值,也可以不包含任何值。

# 主要功能和用法
  1. 创建 std::optional 对象

你可以通过默认构造函数、初始化构造函数或使用工厂函数来创建一个 std::optional 对象。

1
2
3
std::optional<int> opt1;           // 不包含值
std::optional<int> opt2 = 42; // 包含值 42
auto opt3 = std::make_optional(42); // 包含值 42

  1. 检查是否包含值
    使用 has_value() 方法或布尔运算符来检查 std::optional 对象是否包含值。

1
2
3
4
5
6
7
if (opt2.has_value()) {
// opt2 包含值
}

if (opt2) {
// opt2 包含值
}

  1. 访问包含的值

可以使用 value() 方法或者解引用运算符 * 和箭头运算符 -> 来访问包含的值。

1
2
3
4
5
6
int value = opt2.value(); // 如果 opt2 不包含值,会抛出 std::bad_optional_access 异常
int value = *opt2; // 如果 opt2 不包含值,行为未定义

// 也可以使用箭头运算符访问成员
std::optional<std::string> optStr = "hello";
std::cout << optStr->length() << std::endl; // 打印 5

  1. 获取默认值

使用 value_or() 方法可以在 std::optional 对象不包含值时提供一个默认值。

1
int value = opt1.value_or(0); // 如果 opt1 不包含值,则返回默认值 0

  1. 赋值和重置

你可以向 std::optional 对象赋值,也可以通过调用 reset() 方法将其重置为不包含值的状态。

1
2
opt1 = 10;      // opt1 现在包含值 10
opt1.reset(); // opt1 现在不包含值

# std::optional 的使用场景

std::optional 非常适合用来处理函数可能返回空值或无效值的情况:

  1. 函数返回值:当函数可能无法返回有效结果时,使用 std::optional 比返回指针或特殊值(如 -1NULL )更加安全和可读。

1
2
3
4
5
6
7
std::optional<std::string> find_name(int id) {
if (id == 1) {
return "Alice";
} else {
return std::nullopt; // 或者 return {};
}
}

  1. 延迟初始化:当一个变量的初始化依赖某些条件时,可以使用 std::optional 来延迟初始化。

    1
    2
    3
    4
    std::optional<std::string> name;
    if(condition) {
    name = "Alice";
    }

    (这里举的例子不是特别好,它大致要表达的意思是,这里 name 并没有立即被初始化、调用构造函数,而是在条件满足之后在赋值时才初始化。)

  2. 配置选项:在配置文件或命令行参数解析中,可以使用 std::optional 来表示某个选项是否被提供。

    1
    2
    3
    4
    5
    6
    std::optional<int> port = get_port_from_config();
    if(port) {
    std::cout << "Using port: " << *port << std::endl;
    } else {
    std::cout << "Using default port" << std::endl;
    }

# i、继承构造函数

#CPP11
C++ 的继承构造函数(inheriting constructors)是 C++11 引入的一项特性,旨在简化继承体系中的构造函数定义。这一特性使得派生类可以直接继承并使用基类的构造函数,减少代码冗余和重复工作。

  1. 基本概念
    继承构造函数允许派生类继承基类的所有构造函数,而无需显式地在派生类中重新定义它们。这是通过在派生类中使用 using 声明实现的。

  2. 语法

    1
    2
    3
    4
    class Derived : public Base {
    public:
    using Base::Base; // 继承Base类的所有构造函数
    };

# 示例

假设我们有一个基类 Base ,以及一个派生类 Derived ,我们希望派生类能够继承基类的所有构造函数。

  1. 基类定义

    1
    2
    3
    4
    5
    class Base {
    public:
    Base(int x) { /*...*/ }
    Base(int x, int y) { /*...*/ }
    };

  2. 派生类定义

    1
    2
    3
    4
    class Derived : public Base {
    public:
    using Base::Base; // 继承Base类的所有构造函数
    };

  3. 使用

    1
    2
    3
    4
    5
    int main() {
    Derived d1(10); // 使用Base(int x)构造函数
    Derived d2(10, 20); // 使用Base(int x, int y)构造函数
    return 0;
    }

# 继承构造函数的优点
  1. 减少代码重复:无需在派生类中重复定义基类已有的构造函数。
  2. 简化代码维护:基类构造函数的修改自动反映到派生类,无需额外修改派生类。
  3. 提高代码可读性:代码更简洁明了,容易理解。
# 继承构造函数的限制和注意事项
  1. 继承构造函数无法继承默认构造函数:如果基类只有用户定义的构造函数且没有默认构造函数,派生类需要显式定义一个默认构造函数。
  2. 继承构造函数无法继承析构函数:析构函数不会被继承,即使使用继承构造函数,派生类的析构函数仍然需要单独定义。
  3. 菱形继承问题:在多重继承的情况下,使用继承构造函数可能会引发菱形继承问题,需要小心处理。
# 实际应用示例
  1. 基类和派生类的定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Person {
    public:
    Person(const std::string& name) : name(name) {}
    Person(const std::string& name, int age) : name(name), age(age) {}
    private:
    std::string name;
    int age;
    };

    class Employee : public Person {
    public:
    using Person::Person;
    };

  2. 使用派生类的构造函数

    1
    2
    3
    4
    5
    int main() {
    Employee e1("Alice");
    Employee e2("Bob", 30);
    return 0;
    }

# 在 BS_thread_pool 中的使用

具体地,在 BS_thread_pool 中,在 multi_future 类的定义中使用了这一语法:

1
2
3
4
5
6
7
8
template <typename T>
class [[nodiscard]] multi_future : public std::vector<std::future<T>>
{
public:
// Inherit all constructors from the base class `std::vector`.
using std::vector<std::future<T>>::vector;
...
}

这行代码是一个使用了 using 关键字的构造函数继承声明。
具体来说,它是继承了 std::vector<std::future<T>> 类的构造函数。

  1. std::vector<std::future<T>>
    std::vector<std::future<T>> 是一个容器类,存储了多个 std::future<T> 对象。 std::future<T> 是用于表示异步操作结果的标准库类型。

  2. using
    using 关键字在这种上下文中用来继承构造函数。这是 C++11 引入的一项特性,允许一个类继承其基类的构造函数。通过继承构造函数,派生类可以直接使用基类的构造函数初始化自己,而不需要重新定义这些构造函数。

  3. using std::vector<std::future<T>>::vector
    这行代码的意思是让当前类(假设当前类是某个类的派生类)继承 std::vector<std::future<T>> 的所有构造函数。这样一来,你可以使用 std::vector<std::future<T>> 的各种构造函数来构造你的派生类对象。

# i、 std::conditional_tstd::is_void_v

1
[[nodiscard]] std::conditional_t<std::is_void_v<T>, void, std::vector<T>> get()

这个函数的返回值类型使用了一些 C++ 模板编程和条件编译的高级特性。

  • [[nodiscard]] :这是一个属性,表示调用者不应忽略这个函数的返回值。
  • std::conditional_t<std::is_void_v<T>, void, std::vector<T>> :这部分是函数的返回类型。
    它使用了 std::conditional_tstd::is_void_v<T> 来根据模板参数 T 的类型选择返回类型。
    • std::is_void_v<T> :这是一个类型特征,用来检测 T 是否为 void 。如果 Tvoid ,则其值为 true ,否则为 false
    • std::conditional_t<condition, true_type, false_type> :这是一个条件类型,根据 condition 的值选择返回类型。如果 conditiontrue ,则返回 true_type ,否则返回 false_type
    • 因此, std::conditional_t<std::is_void_v<T>, void, std::vector<T>> 表示如果 Tvoid ,则函数返回 void 类型,否则返回 std::vector<T> 类型。

# i、if constexpr

#CPP17

  • if constexpr 是 C++17 引入的一种条件编译指令。与传统的 if 语句不同, if constexpr 在编译时对条件表达式求值,并根据结果选择编译哪一段代码。如果条件为 true ,则编译 if constexpr 块内的代码;如果条件为 false ,则编译 else 块内的代码。
  • 这意味着在编译时就能确定哪一段代码会被编译,而另一段代码则会被完全忽略。这对于模板编程尤其有用,因为它可以避免在编译期检测不到的非法代码。

# i、构造函数的禁用

在 C++ 中,有些类的拷贝构造函数、拷贝赋值运算、移动构造函数、移动赋值运算符会被显式地禁用(通过 delete 关键字)。这样做的原因通常是为了避免对象的错误复制,保护资源的独占性或者防止出现不期望的行为。以下是几种常见的情况以及每种情况下为什么要禁用构造函数:

  1. 管理独占资源

如果一个类管理独占资源(例如文件句柄、网络连接、内存等),则应禁用拷贝操作以防止多个对象试图管理同一个资源。这会导致资源冲突和意外行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FileHandler {
public:
FileHandler(const std::string& filename) {
// 打开文件并获取文件句柄
}
~FileHandler() {
// 关闭文件并释放资源
}

// 禁用拷贝构造函数和拷贝赋值运算符
FileHandler(const FileHandler&) = delete;
FileHandler& operator=(const FileHandler&) = delete;

private:
int file_descriptor; // 文件句柄
};

  1. 防止不安全的深拷贝

有些类包含指向动态分配内存的指针。拷贝这些对象需要实现深拷贝操作,否则会导致浅拷贝问题(即多个对象指向同一块内存),这可能会引发双重释放(double free)或其他内存管理错误。如果不想实现深拷贝,最简单的方法是禁用拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
class DynamicArray {
public:
DynamicArray(size_t size) : size(size), data(new int[size]) {}
~DynamicArray() { delete[] data; }

// 禁用拷贝构造函数和拷贝赋值运算符
DynamicArray(const DynamicArray&) = delete;
DynamicArray& operator=(const DynamicArray&) = delete;

private:
size_t size;
int* data;
};

  1. 单例模式

单例模式要求一个类只有一个实例。为了防止创建多个实例,需要禁用拷贝构造函数和拷贝赋值运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}

// 禁用拷贝构造函数和拷贝赋值运算符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

private:
Singleton() {} // 私有构造函数
};

  1. 通过移动语义管理资源

有些类支持移动语义(move semantics),这意味着资源的所有权可以转移,而不是复制。在这种情况下,通常会禁用拷贝操作,只允许移动操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class UniqueResource {
public:
UniqueResource(Resource&& resource) : resource(std::move(resource)) {}

// 禁用拷贝构造函数和拷贝赋值运算符
UniqueResource(const UniqueResource&) = delete;
UniqueResource& operator=(const UniqueResource&) = delete;

// 允许移动构造函数和移动赋值运算符
UniqueResource(UniqueResource&&) = default;
UniqueResource& operator=(UniqueResource&&) = default;

private:
Resource resource;
};

  1. 实现不可复制类型

有时候,一个类的语义意味着它不应该被复制,例如同步原语(mutex)或者一些逻辑控制类。在这种情况下,显式禁用拷贝操作是合适的。

1
2
3
4
5
6
7
8
class NonCopyable {
public:
NonCopyable() {}

// 禁用拷贝构造函数和拷贝赋值运算符
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};

# i、thread_local

thread_local 关键字用于声明线程局部变量,使得每个线程有自己的独立实例。这些变量的生命周期与线程相同:线程开始时变量被创建,线程结束时变量被销毁。

特点

  1. 独立副本:每个线程都有自己的变量副本,变量之间互不干扰。
  2. 自动初始化:每个线程在访问 thread_local 变量时,该变量会根据定义时的初始化表达式进行初始化。
  3. 生命周期:变量的生命周期从线程创建开始,到线程结束为止。

语法
thread_local 关键字可以用于全局变量、局部变量和静态成员变量。使用方法如下:

1
2
3
4
5
6
7
8
9
10
thread_local int global_var = 0;  // 全局线程局部变量

void function() {
thread_local int local_var = 0; // 局部线程局部变量
}

class MyClass {
static thread_local int static_member_var; // 静态成员线程局部变量
};
thread_local int MyClass::static_member_var = 0; // 静态成员变量定义

使用场景

  1. 线程安全:在多线程环境下使用全局变量或静态变量时,使用 thread_local 可以避免数据竞争。
  2. 缓存局部数据:需要在线程中缓存一些局部数据,每个线程都有独立的缓存。
  3. 性能优化:避免线程间同步开销,提升性能。

以下是一个使用 thread_local 关键字的示例,展示了如何在多线程中使用线程局部变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <thread>

thread_local int counter = 0;

void increment(const std::string& thread_name) {
++counter;
std::cout << thread_name << ": counter = " << counter << std::endl;
}

int main() {
std::thread t1(increment, "Thread 1");
std::thread t2(increment, "Thread 2");

t1.join();
t2.join();

return 0;
}

运行结果:

1
2
Thread 1: counter = 1
Thread 2: counter = 1

从输出可以看出,每个线程都有自己独立的 counter 变量,互不干扰。

关键点

  1. 定义时机thread_local 变量应在线程创建之前定义,以确保线程能够正确访问这些变量。
  2. 内存开销:每个线程都有独立的变量副本,可能会增加内存使用量,尤其是变量占用较多内存时。
  3. 初始化表达式:每个线程的 thread_local 变量使用相同的初始化表达式进行初始化。

回到 BS_thread_pool 中,在命名空间 this_thread 中定义了两个 thread_local 的变量:

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
namespace this_thread {
using optional_index = std::optional<size_t>;
using optional_pool = std::optional<thread_pool*>;


class [[nodiscard]] thread_info_index
{
friend class BS::thread_pool;

public:
[[nodiscard]] optional_index operator()() const
{
return index;
}

private:
optional_index index = std::nullopt;
}; // class thread_info_index


class [[nodiscard]] thread_info_pool
{
friend class BS::thread_pool;

public:
[[nodiscard]] optional_pool operator()() const
{
return pool;
}

private:
optional_pool pool = std::nullopt;
}; // class thread_info_pool

inline thread_local thread_info_index get_index;
inline thread_local thread_info_pool get_pool;
} // namespace this_thread

然后,在 thread_pool 这个类中使用了上面的内容,这里以 worker 函数举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
void worker(const concurrency_t idx, const std::function<void()>& init_task)
{
this_thread::get_index.index = idx;
this_thread::get_pool.pool = this;
init_task();
std::unique_lock tasks_lock(tasks_mutex);
while (true)
{
...
}
this_thread::get_index.index = std::nullopt;
this_thread::get_pool.pool = std::nullopt;
}

可以看到这里我们对 this_thread::get_index.indexthis_thread::get_pool.pool 进行了赋值。

那么,问题来了,为什么不直接在 this_thread 命名空间中定义 indexpool ,而还要加入两个类作为中间层?为什么不能如下:

1
2
3
4
5
6
7
namespace this_thread {
using optional_index = std::optional<size_t>;
using optional_pool = std::optional<thread_pool*>;

inline thread_local optional_index index = std::nullopt;
inline thread_local optional_pool pool = std::nullopt;
} // namespace this_thread

this_thread 命名空间中直接定义两个变量 indexpool 是可以的,但使用类 thread_info_indexthread_info_pool 来封装这些变量也有其独特的优势和目的。

以下是一些关键原因:

  1. 访问控制

—— 在这里我觉得是主要的原因。

使用类可以更好地控制变量的访问权限。通过类的 publicprivate 成员,可以明确哪些成员可以被外部访问,哪些成员是内部实现细节。例如, indexpool 的设置和访问可以通过公有成员函数进行,而变量本身可以是私有的,从而防止意外修改。

  1. 增强可读性和组织性

使用类可以将相关的功能和数据组织在一起,使代码更具可读性和结构化。特别是当需要扩展功能时,可以方便地在类中添加新的成员函数,而不需要修改原有的全局变量定义。

  1. 提供额外功能

类可以包含额外的功能。例如,类可以提供特定的操作函数(如获取、设置、重置变量),而这些功能通过简单的全局变量是无法实现的。这样可以确保对变量的操作更加规范和受控。

  1. 一致的接口

通过类可以提供一致的接口,特别是在涉及多个类似功能的变量时。例如, thread_info_indexthread_info_pool 类都提供了一个一致的操作接口( operator() ),这使得在代码中使用这些变量更加方便和统一。