目录

一.std::future

1.1.std::async

1.1.1.通俗版介绍

1.1.2.专业描述

1.1.3.示例

1.2.std::packaged_task

1.2.1.介绍

1.2.2.示例

1.3.std::promise

1.3.1.介绍

1.3.2.示例

二.C++11 线程池实现

2.1.前置C++基础知识

2.1.1.可变模板参数

2.1.2.decltype

2.1.3.std::forward

2.2.线程池实现


一.std::future

在C++中,std::future是一个模板类,它提供了一种机制来获取异步操作的结果。这里我将以纯文字方式详细描述std::future的使用和相关概念。

首先,我们需要了解异步操作。异步操作是指那些可以在后台执行的操作,不会阻塞当前线程。当我们需要执行一个耗时操作(比如计算、文件读写、网络请求等)时,我们可以将其作为异步任务,这样主线程可以继续执行其他任务,直到需要异步任务的结果时再等待其完成。

std::future就是用来表示这个异步任务的结果。它并不存储结果,而是作为一个句柄,通过它可以获取异步任务的结果。当异步任务完成时,结果会被存储在一个共享状态中,std::future可以访问这个共享状态。

使用std::future的基本步骤:

  1. 启动异步任务:我们可以使用std::async、std::packaged_task或者std::promise来创建一个异步任务,并获取与这个任务关联的std::future对象。

  2. 在需要结果的地方,通过std::future对象获取结果。

1.1.std::async

1.1.1.通俗版介绍

你可以把 std::async 想象成一个帮你“外包任务”的聪明管家。

  • 你:就是主线程,正在处理主要工作(比如做菜)。
  • 任务:一件你想完成,但又不想自己停下来去做的耗时工作(比如去楼下买瓶酱油)。
  • std::async 管家:你把这个买酱油的任务“外包”给了他。
  • std::future 收据/承诺:管家不会立刻把酱油给你,而是给你一张“收据”或一个“承诺”。这张收据上写着“未来会有一瓶酱油”。这张收据就是 std::future 对象。

现在,关键来了:这个管家派谁去执行“买酱油”这个任务呢?这取决于你给他的指令,也就是 std::launch 启动策略。

三种“外包”策略(启动策略)

策略一:懒汉模式 - std::launch::deferred

  • 管家行为:管家接过你的任务,说:“好的,我知道了。”但他立刻就把任务忘在脑后了,根本没派人出去。
  • 实际执行时机:什么时候才执行呢?直到你拿着那张收据(future),主动对他说:“喂,我的酱油呢?我现在就要!”(即调用 get() 或 wait() 方法)。这时,管家才一拍脑袋:“哎呀!我还没派人去呢!”然后他会立刻、亲自、就在你面前(在当前调用 get() 的线程里)跑去把酱油买回来给你。
  • 特点:根本没有创建新线程。任务是“延迟”执行的,并且是同步的(你必须停下来等他把事办完)。适合那些“可能不需要,但如果需要就马上做”的任务。

策略二:勤快模式 - std::launch::async

管家行为:管家接过任务,立刻转身雇佣了一个新的临时工(创建一个新线程),把钱和任务清单交给他说:“快去快回!”

实际执行时机:任务立刻就开始了。新线程(临时工)立刻出发去买酱油。

你的状态:你可以继续做你的主菜(主线程继续运行),完全不用等待。

取结果:当你需要酱油时(调用 get()),会有两种情况:

  • 如果临时工已经回来了,你直接拿到酱油。
  • 如果临时工还没回来,那你必须停下来等他(主线程被阻塞),直到他回来把酱油交给你。

特点:明确创建了新线程,任务是真正异步执行的。适合那些明确需要后台并发执行的任务。


策略三:自动模式 - std::launch::deferred | std::launch::async(默认策略)

管家行为:管家很精明。他没有立刻决定。他会根据一些内部因素(比如系统忙不忙、任务重不重等)自己偷偷做决定。

可能的结果:

  • 他可能选择“懒汉模式”(deferred),先记下来,等你要的时候再做。
  • 他也可能选择“勤快模式”(async),立刻派个新线程去做。

对你的影响:你无法预测任务会怎么执行! 这对程序的行为有重大影响:

  • 如果你假设它是异步的(认为它在新线程运行了),而它实际是延迟的,那么当你第一次调用 get() 时,会意外地导致长时间等待,破坏你的并发计划。
  • 如果你依赖它创建新线程(比如任务有线程局部变量),而它选择了延迟执行,那么这些依赖就会出错。

给初学者的建议:尽量不要使用这个默认的自动模式。明确指定你想要的策略(async 或 deferred),这样代码的行为才是清晰、可预测的。不确定的时候,问问自己:“这个任务我必须立刻开始做吗?”,如果答案是“是”,就用 async;如果“不一定,也许等会儿做也行”,就用 deferred。

一个生活化的比喻总结

想象你要写一封信并寄出去。

  • std::launch::deferred:你把信和邮票放在桌上,继续工作。只有当邮差上门问你“有信要寄吗?”的时候(get()),你才停下工作,现场写信、贴邮票、交给他。
  • std::launch::async:你立刻叫来一个跑腿小弟,把写好的信和钱给他,说:“去帮我寄了。”然后你继续工作。小弟立刻出发。等他回来问你要小费(get())时,你才需要理他。
  • 默认策略:你可能把信放在桌上(打算等邮差),也可能立刻叫了跑腿小弟。但你记性不好,过一会儿就忘了自己是怎么安排的了,导致邮差来时你措手不及。

1.1.2.专业描述

首先,std::async 是C++11中提供的一个用于异步任务处理的工具。

它可以将一个可调用对象(比如函数、Lambda表达式、函数对象等)包装起来,并使其可能在新线程中运行,然后返回一个std::future对象,通过这个future对象可以获取异步任务的结果。

当我们使用std::async时,我们可以指定一个启动策略(launch policy),这个策略决定了任务何时以及如何运行。

  1. std::launch::deferred(延迟启动):
    如果我们使用这个策略,那么任务不会立即开始执行。相反,它会延迟到我们调用future的get()或wait()方法时才会执行。而且,任务会在调用get()或wait()的线程中执行,而不是在新线程中。也就是说,任务会同步执行,直到任务完成,get()或wait()才会返回。

  2. std::launch::async(异步启动):
    这个策略会立即开始执行任务,任务会在一个新创建的线程中运行。我们可以在主线程中继续做其他事情,然后在需要的时候通过future来获取任务的结果。注意,如果任务已经开始执行,那么即使我们不调用get(),它也会在后台运行,直到完成。

  3. 默认策略(std::launch::deferred | std::launch::async):
    当我们不指定策略时,或者指定为两者组合时,由系统决定是立即启动一个新线程(类似async)还是延迟执行(类似deferred)。这取决于系统当时的资源状况和实现。因此,使用默认策略时,我们无法确定任务是会延迟执行还是异步执行。

1.1.3.示例

示例1:std::launch::deferred(延迟执行)

#include <iostream>
#include <future>
#include <thread>
#include <chrono>

int compute(int x, int y) {
    std::cout << "计算线程ID: " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return x + y;
}

int main() {
    std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
    
    // 使用延迟策略
    auto future = std::async(std::launch::deferred, compute, 10, 20);
    
    std::cout << "主线程继续执行其他工作..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "主线程工作完成" << std::endl;
    
    // 只有在这里才会真正执行compute函数
    int result = future.get();
    std::cout << "计算结果: " << result << std::endl;
    
    return 0;
}

停顿1秒

停顿2秒

恰好说明了如果我们使用这个策略,那么任务不会立即开始执行。相反,它会延迟到我们调用future的get()或wait()方法时才会执行。

示例2:std::launch::async(异步执行)

#include <iostream>
#include <future>
#include <thread>
#include <chrono>

int compute(int x, int y) {
    std::cout << "计算线程ID: " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return x * y;
}

int main() {
    std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
    
    // 使用异步策略
    auto future = std::async(std::launch::async, compute, 10, 20);
    
    std::cout << "主线程继续执行其他工作..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "主线程工作完成" << std::endl;
    
    // 任务已经在执行,这里只是等待结果
    int result = future.get();
    std::cout << "计算结果: " << result << std::endl;
    
    return 0;
}

可以看到,这里主线程和计算线程是同时存在的,也就是说这个策略会立即开始执行任务,任务会在一个新创建的线程中运行。

这里就是在等待计算线程返回结果了(等待事件不足2秒)

示例3——默认策略(std::launch::deferred | std::launch::async)

当我们不指定策略时,或者指定为两者组合时,由系统决定是立即启动一个新线程(类似async)还是延迟执行(类似deferred)。

auto future = std::async(compute, 10, 20);

完全等价于:

auto future = std::async(std::launch::async | std::launch::deferred, compute, 10, 20);

至于效果我就不演示了,没什么好说的,就是上面两种策略的任意一种!!

1.2.std::packaged_task

1.2.1.介绍

std::packaged_task也是一个将任务和future关联起来的工具,但是它的使用方式与std::async不同。你可以把std::packaged_task看作是一个包装了可调用对象(如函数、lambda表达式等)的包装器,并且它允许你获取一个与包装的任务相关联的std::future对象。

核心概念:将任务“打包”

想象一下,你有一个任务(比如一个函数),你想在未来的某个时候执行它,并且希望得到它的结果。

你可以使用std::packaged_task来打包这个任务,然后你可以把这个打包好的任务传递给其他线程,或者稍后执行。

同时,你可以通过这个打包的任务获取一个std::future对象,用于在将来获取任务的结果。

一个简单的比喻

假设你是一个公司的老板,你有一个任务(比如做一份财务报告)。你把这个任务写在一张任务卡上,然后把这张任务卡放进一个特殊的信封里(这个信封就是std::packaged_task)。

这个信封有一个特点:当你把任务卡放进去的时候,信封会给你一张收据(std::future),这张收据可以用来领取任务完成后的结果(财务报告)。

然后,你可以把这个信封交给一个员工(另一个线程)去执行,或者你可以选择在某个时候自己打开信封执行任务。

如何使用?

  1. 创建打包任务:你需要创建一个std::packaged_task对象,并指定它要包装的任务(函数、lambda等)。注意,模板参数是函数签名,例如int(int, int)表示一个返回int并接受两个int参数的函数。

  2. 获取future:通过调用get_future()方法,你可以获得一个与任务结果关联的std::future对象。这样,当任务执行完毕后,你就可以通过这个future来获取结果。

  3. 执行任务:std::packaged_task本身是一个可调用对象,你可以直接调用它(就像调用函数一样),或者将它传递给另一个线程,让那个线程去执行。注意,任务通常只会被执行一次。

  4. 获取结果:通过之前获得的std::future对象,你可以获取任务的结果。如果任务还没有执行完毕,调用get()会阻塞直到结果可用。

与std::async的区别

  • std::async:它直接创建了一个异步任务,并且返回一个future。你不需要显式地管理任务的执行,它可能会在新线程中执行,也可能在调用get()时同步执行(取决于启动策略)。

  • std::packaged_task:它只是将任务包装起来,并允许你获取一个future,但是任务的执行需要你自己来安排。你可以选择在哪个线程、什么时候执行这个任务。它更像是一个可以移动和传递的任务对象,并且与future关联。

一个生活化的例子

假设你周末要做两件事:

  1. 去超市买东西(主线程任务)。

  2. 计算家庭月度开销(一个计算任务,比较耗时)。

你可以这样安排:

  • 你创建一个计算家庭月度开销的任务,并用std::packaged_task把它打包。然后你得到一个future,用于以后获取计算结果。

  • 然后,你可以把这个打包的任务(packaged_task)交给你的另一半,让他/她在你有空的时候去计算(或者你可以自己稍后计算)。同时,你可以先去超市买东西。

  • 当你从超市回来,你需要知道家庭月度开销的结果时,你可以通过之前得到的future来获取。如果另一半已经计算完了,你立刻得到结果;如果还没算完,你就得等他/她算完。

为什么使用std::packaged_task?

它提供了更多的灵活性。

你可以控制任务的执行时机和方式,而且可以将任务对象(packaged_task)在多个线程之间传递,而std::async则不能这样控制。

另外,std::packaged_task还可以用于线程池等更复杂的并发模式中。

1.2.2.示例

示例1:基础使用(在同一线程中执行)

#include <iostream>
#include <future>
#include <thread>

int add(int a, int b) {
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
    return a + b;
}

int main() {
    // 1. 创建packaged_task,包装add函数
    // 模板参数是函数签名:int(int, int)
    std::packaged_task<int(int, int)> task(add);
    
    // 2. 获取与任务结果关联的future
    std::future<int> result = task.get_future();
    
    // 3. 执行任务(直接调用)
    // 可以直接调用,也可以在线程中执行
    task(10, 20);  // 在这里调用,当前线程会执行任务
    
    // 4. 获取结果(如果任务未完成,会阻塞等待)
    std::cout << "计算结果: " << result.get() << std::endl;
    
    return 0;
}

我们直接编译运行

等待了1秒

示例2:在子线程中执行任务

我们可以将任务移交给其他线程!!!

#include <iostream>
#include <future>
#include <thread>
#include <memory>

int multiply(int x, int y) {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return x * y;
}

int main() {
    // 使用shared_ptr包装packaged_task
    auto taskPtr = std::make_shared<std::packaged_task<int()>>(
        std::bind(multiply, 5, 6)
    );
    
    // 获取future
    std::future<int> result = taskPtr->get_future();
    
    // 创建一个线程来执行任务
    // 使用lambda表达式捕获shared_ptr,这样任务可以安全地在线程中执行
    std::thread worker([taskPtr]() {
        // 调用packaged_task
        (*taskPtr)();
    });
    
    // 主线程可以做其他事情
    std::cout << "主线程正在做其他工作..." << std::endl;
    
    // 当需要结果时,获取它(会阻塞直到结果可用)
    std::cout << "计算结果: " << result.get() << std::endl;
    
    // 等待工作线程结束
    worker.join();
    
    return 0;
}

等待2秒(注意这两秒是父进程在等待子进程退出)

注意:我们想要使用std::packaged_task来封装一个函数,然后通过std::future来获取结果。

但是,std::packaged_task不能直接传递给std::thread,因为std::packaged_task不可拷贝,而且它也不是一个函数对象(虽然它重载了(),但它的类型是类模板,不能直接作为线程函数)。

1.3.std::promise

1.3.1.介绍

首先,我们需要理解std::future和std::promise的基本概念:

  1. std::future:这是一个模板类,它提供了一种访问异步操作结果的机制。我们可以通过它来获取一个异步任务(比如在另一个线程中运行的函数)的返回值。但是,这个返回值可能还没有准备好,所以std::future提供了等待(wait)或检查(通过valid()等方法)的功能,直到异步操作完成并设置了值。

  2. std::promise:这也是一个模板类,它提供了一种存储值(或异常)的方式,这个值可以被同一个线程或其他线程中的std::future对象读取。换句话说,std::promise允许你手动设置一个值,然后与它关联的std::future就可以读取这个值。

现在,我们来看它们如何配合工作:

  • 当我们创建一个std::promise对象时,它内部会有一个共享状态(shared state),这个共享状态可以存储一个值(或异常)。

  • 通过调用std::promise的get_future()成员函数,我们可以获得一个与这个promise对象关联的std::future对象。

  • 然后,我们可以在某个线程中运行一个任务,这个任务会计算一个值,然后通过调用std::promise的set_value()方法将值存储到共享状态中。

  • 同时,在另一个线程(或同一个线程)中,我们可以通过std::future的get()方法来获取这个值。如果共享状态中还没有值,那么get()方法会阻塞,直到值被设置。

这样,我们就实现了一个线程向另一个线程传递数据的效果。

注意:一个std::promise对象只能关联一个std::future对象,并且只能设置一次值。如果多次设置,会抛出异常。

详细的工作流程

让我们跟着代码的典型生命周期走一遍:

步骤 1:创建配对

std::promise<int> prom;              // 创建一个 promise 对象,用于将来产生一个 int 类型的值。
std::future<int> fut = prom.get_future(); // 从 promise 获取与之关联的 future 对象。

此时,共享状态被创建,但里面是“空”的。future 还没有就绪。

步骤 2:生产者设置值(让 future “就绪”)

在生产者线程(比如一个工作线程)中,你操作 promise:

// 生产者线程
void producer(std::promise<int>& prom) {
    // ... 执行一些复杂的计算 ...
    int result = 42;
    prom.set_value(result); // 关键操作!
}

prom.set_value(result) 做了两件至关重要的事:

  • 存储数据:将计算结果 42 写入到共享状态中。
  • 通知就绪:将共享状态标记为“就绪”,并唤醒任何正在等待这个 future 的线程(比如消费者线程)。

步骤 3:消费者获取值

在消费者线程(比如主线程)中,你操作 future:

// 消费者线程
int main() {
    // ... 创建 promise 和 future,启动生产者线程 ...
    int x = fut.get(); // 关键操作!
    std::cout << "Got value: " << x << std::endl; // 输出 42
}

fut.get() 也做了两件关键的事:

  • 等待就绪:如果共享状态还没就绪(即生产者还没调用 set_value),那么 get() 会阻塞当前线程,直到就绪。
  • 取回数据:一旦就绪,它就从共享状态中取出值(这里是 42),然后返回。

关键点:get() 只能调用一次。调用后,共享状态中的数据就被取走(移动)了,future 变为无效。

1.3.2.示例

示例1——基本用法

  • 步骤1:创建一个std::promise对象。
  • 步骤2:通过这个std::promise对象获取一个std::future对象。
  • 步骤3:启动一个线程,将std::promise对象传递给这个线程(或者通过其他方式让线程可以访问到这个promise)。
  • 步骤4:在这个线程中执行一些计算,然后通过set_value()方法将结果设置到promise中。
  • 步骤5:在原来的线程中,通过future的get()方法获取结果。

示例代码:

#include <iostream>
#include <thread>
#include <future>

void compute(std::promise<int> prom) {
    // 模拟一个耗时的计算
    std::this_thread::sleep_for(std::chrono::seconds(1));
    int result = 42;
    prom.set_value(result); // 将结果设置到promise中
}

int main() {
    std::promise<int> prom; // 步骤1:创建一个std::promise对象
    std::future<int> fut = prom.get_future(); // 步骤2:获取对应的future

    std::thread t(compute, std::move(prom)); // 步骤3:启动线程,并将promise移动给线程

    // 步骤5:在主线程中通过future获取结果
    int result = fut.get(); // 这会阻塞,直到compute线程设置了值
    std::cout << "Result: " << result << std::endl;

    t.join();
    return 0;
}

直接编译运行

等待1秒后

二.C++11 线程池实现

2.1.前置C++基础知识

2.1.1.可变模板参数

可变模板参数(Variadic Templates)是C++11引入的特性,允许函数或类模板接受任意数量的模板参数。

// 语法解释:
// typename... Args  - 这是一个模板参数包(template parameter pack)
// Args... args     - 这是一个函数参数包(function parameter pack)
template <typename... Args>
void func(Args... args) {
    // 函数体
}

我们这里使用可变参数,其实是为了给任务函数传递参数进去的

话不多说,我们直接看例子

#include <iostream>

// 1. 接受函数和参数包,调用该函数
template<typename Func, typename... Args>
void call_func(Func f, Args... args) {
    f(args...);  // 使用参数包展开调用函数
}

// 2. 示例函数1
void print_sum(int a, int b) {
    std::cout << "Sum: " << a + b << std::endl;
}

// 3. 示例函数2
void print_three(int a, int b, int c) {
    std::cout << "Three numbers: " << a << ", " << b << ", " << c << std::endl;
}

int main() {
    // 例1: 调用两个参数的函数
    call_func(print_sum, 10, 20);      // 输出: Sum: 30
    
    // 例2: 调用三个参数的函数
    call_func(print_three, 1, 2, 3);   // 输出: Three numbers: 1, 2, 3
    
    return 0;
}

怎么样?还是很容易懂的吧

2.1.2.decltype

 我们在使用函数模板时可能会遇到下面这种情况

template<class T1,class T2>
void A(T1 a,T2,b)
{
?type? c=a+b;//c的类型不确定啊
....
}

这个时候怎么办呢?

C++11引入了关键字decltype,用来推断类型

用法·如下

decltype(expression) var;

expression可以是下面这几种情况

1.没有用括号括起的标识符,var的类型和该标识符的类型相同,包括const等限定符 

我们可以举个例子

int x;
decltype(x) y;//将y定义为和x是同一类型的变量
const int y=22;
decltype(y) c=99;//c为const int类型

2.如果expression是一个函数调用,则var的类型与函数返回类型一样

 这个过程不会调用函数哦!!!

int A()
{
return 33;
}
int main()
{
decltype(A()) c;//c的类型是A()的返回类型int
}

3,如果expression是一个左值,则var的类型是指向其类型的引用,前提是必须是要用括号括起的标识符

int x=99;
decltype((x)) y=x;
//y的类型就是int&

4.如果和上面三种情况都不符合的话,var的类型和expression的类型相同

decltype(3+9) y;
//3+9为int型,所以y为int型

学会decltype,我们就可以解决上面那个问题了

template<class T1,class T2>
void A(T1 a,T2,b)
{
decltype(a+b) c=a+b;//c的类型就是a+b的类型了
....
}

我们还可能遇到下面这种问题

template<class T1,class T2>
?type? A(T1 a,T2 b)//不知道返回类型是什么
{
return a+b;
}

为了解决这个问题,C++引入了一种新语法

double A();
auto A()->double;
//这两种写法是等价的

 这样子我们也可以解决这个问题了

template<class T1,class T2>
auto A(T1 a,T2 b)->decltype(a+b)
{
return a+b;
}

示例

现在我们就能对我们的模板函数进行进一步的加工,我们加上返回值

#include <iostream>

// 带返回值的示例
int multiply1(int a, int b) {
    return a * b;
}

// 带返回值的示例
float multiply2(float a, float b) {
    return a * b;
}

//  支持返回值的版本
template<typename Func, typename... Args>
auto call_with_return(Func f, Args... args) -> decltype(f(args...)) {
    return f(args...);  // 返回函数调用的结果
}

int main() {
    
    // 例1: 调用带返回值的函数
    int result1 = call_with_return(multiply1, 5, 6);
    std::cout << "result1: " << result1 << std::endl;
    
    // 例: 调用带返回值的函数
    float result2 = call_with_return(multiply2, 3.55, 6.9);
    std::cout << "result2: " << result2 << std::endl;
    
    return 0;
}

2.1.3.std::forward

左值(lvalue)和右值(rvalue)

在C++中,每一个表达式都有两个属性:类型(type)和值类别(value category)。
值类别分为左值和右值(实际上还有更细的分类,但初学者可以先理解为两种)。

左值(lvalue):

  • 指的是一个持久的内存位置,我们可以取地址。
  • 通常,变量名、函数名、返回左值引用的函数调用、前置自增/自减运算符等都是左值。
  • 简单来说,左值可以出现在赋值语句的左边(但并不是所有左值都可以被赋值,比如被const修饰的左值就不能被赋值)。

右值(rvalue):

  • 通常是临时的、生命周期很短的值,我们无法取地址。
  • 例如:字面量(除了字符串字面量,因为字符串字面量是左值,它存储在静态存储区)、临时对象、返回非引用类型的函数调用、算术运算结果等。

例子:

int a = 5; // a是左值,5是右值。
int b = a; // a是左值,它可以出现在赋值语句的右边,但a本身是左值,因为我们可以取地址&a。

左值引用(lvalue reference)

左值引用就是我们通常使用的引用,用&表示。它必须绑定到一个左值上。

例如:

int x = 10;
int &ref = x; // 正确,ref是x的别名,x是左值。
int &ref2 = 5; // 错误,5是右值,不能绑定到左值引用(除非是const左值引用,见后)。

注意:const左值引用可以绑定到右值,因为const引用保证了不会修改绑定的值,所以允许绑定到临时对象。

const int &ref3 = 5; // 正确,const左值引用可以绑定到右值。

右值引用(rvalue reference)

C++11引入了右值引用,用&&表示。它只能绑定到右值,不能绑定到左值(除非使用std::move,后面会讲)。

例如:

int &&rref = 10; // 正确,10是右值。
int x = 10;
int &&rref2 = x; // 错误,x是左值,不能绑定到右值引用。

右值引用的出现主要是为了支持移动语义(move semantics)和完美转发(perfect forwarding)。

为什么需要 std::forward?

 std::forward 是 C++11 中引入的完美转发工具,主要解决参数转发过程中的值类别保持问题。

核心问题:为什么需要 std::forward?

考虑这个场景:

#include <iostream>
using namespace std;

void process(int& x) 
{ 
    cout << "左值引用" << endl; 
}
void process(int&& x) 
{ 
    cout << "右值引用" << endl; 
}

template<typename T>
void wrapper(T&& arg) {
    // 这里 arg 是一个具名变量,总是左值
    process(arg);  // 丢失了右值属性!
}

int main() {
    int x = 10;
    wrapper(x);    // 调用左值版本 √
    wrapper(10);   // 期望调用右值版本,但实际调用了左值版本 ×
}

std::forward 的解决方案

完美转发是指在函数模板中,将参数按照原来的类型转发给另一个函数。这通常用于泛型编程,保持参数的值类别(左值或右值)。

template<typename T>
void wrapper(T&& arg) {
// 将arg按照原来的类型转发给另一个函数
another_function(std::forward<T>(arg));
}

这里,T&&是一个通用引用(universal reference),它既可以绑定到左值,也可以绑定到右值。然后使用std::forward来保持参数原来的值类别。

在C++中,当你看到函数模板 template<typename T> void wrapper(T&& arg) 这样的声明时,这里的 T&& 并不是一个普通的右值引用,而是一个特殊的类型,叫做“转发引用”或“通用引用”。

要理解这一点,我们需要从C++模板的类型推导和引用折叠规则说起。

  • 首先,普通的右值引用(比如 int&&)只能绑定到右值(临时对象、即将销毁的对象),不能绑定到左值(有名字的变量、持久对象)。这是一个硬性规定。
  • 但模板中的 T&& 不同。这里的 T 是一个待推导的类型参数。当编译器看到这样的声明并进行类型推导时,它会根据传入的参数来决定 T 到底是什么类型。

规则是这样的:

  • 如果你传入一个左值给 wrapper 函数,比如 wrapper(x),其中 x 是一个 int 类型的变量,那么编译器会把 T 推导为左值引用 int&
  • 如果你传入一个右值给 wrapper 函数,比如 wrapper(10) 或 wrapper(std::move(x)),那么编译器会把 T 推导为普通的非引用类型 int。

现在,我们有了一个看似矛盾的情况:T&& 中的 T 可能被推导为引用类型。这就引出了C++的“引用折叠”规则。

引用折叠规则是处理引用的引用的规则。在C++中,你通常不能直接声明引用的引用,但在模板推导和类型别名中可能会隐式产生。规则很简单:

  • & & 折叠成 &(左值引用)
  • & && 折叠成 &(左值引用)
  • && & 折叠成 &(左值引用)
  • && && 折叠成 &&(右值引用)

应用这个规则,让我们看看前面的例子:

  • 当传入左值 x 时,T 被推导为 int&,所以 T&& 变成 int& &&,根据引用折叠规则,折叠成 int&。因此,arg 实际上是左值引用。
  • 当传入右值 10 时,T 被推导为 int,所以 T&& 就是 int&&。因此,arg 是右值引用。

这就是为什么 T&& 被称为“转发引用”或“通用引用”——它能接受左值和右值,并推导出相应的类型。

但这里还有一个问题:在函数内部,即使 arg 被推导为右值引用类型,它本身作为一个有名字的变量,实际上是一个左值。如果你直接传递 arg 给另一个函数,它会被当作左值处理。

这就是需要 std::forward<T> 的原因。std::forward 是一个条件转换:

  • 如果 T 是左值引用类型(意味着原始传入的是左值),std::forward<T>(arg) 返回左值引用。
  • 如果 T 是右值引用类型(意味着原始传入的是右值),std::forward<T>(arg) 返回右值引用。

换句话说,std::forward 的任务是保持参数的原始值类别:如果参数最初是左值,就保持为左值;如果最初是右值,就保持为右值。

所以,在 another_function(std::forward<T>(arg)) 这行代码中:

  • 如果 wrapper 被调用时传入的是左值,arg 是左值引用,std::forward 将其作为左值传递给 another_function。
  • 如果 wrapper 被调用时传入的是右值,arg 是右值引用但本身是左值,std::forward 将其转换为右值引用(实际上是右值),然后传递给 another_function。

这种技术被称为“完美转发”——完美地将参数按原样转发给另一个函数,包括它的值类别(左值/右值)、const限定符等所有属性。


完整示例

#include <iostream>
#include <utility>

// 1. 处理左值引用的函数
void process(int& x) {
    std::cout << "处理左值引用: " << x << std::endl;
}

// 2. 处理右值引用的函数
void process(int&& x) {
    std::cout << "处理右值引用: " << x << std::endl;
}

// 3. 处理常量的版本
void process(const int& x) {
    std::cout << "处理常量引用: " << x << std::endl;
}

// 4. 普通转发(错误示例)
template<typename T>
void bad_wrapper(T&& arg) {
    process(arg);  // arg 总是被当作左值
}

// 5. 完美转发(正确示例)
template<typename T>
void good_wrapper(T&& arg) {
    process(std::forward<T>(arg));  // 保持值类别
}

int main() {
    std::cout << "=== 错误示例 ===" << std::endl;
    int a = 10;
    const int b = 20;
    
    bad_wrapper(a);       // 输出: 处理左值引用: 10
    bad_wrapper(b);       // 输出: 处理常量引用: 20
    bad_wrapper(30);      // 期望调用右值版本,但输出: 处理左值引用: 30
    bad_wrapper(std::move(a)); // 输出: 处理左值引用: 10
    
    std::cout << "\n=== 正确示例 ===" << std::endl;
    good_wrapper(a);       // 输出: 处理左值引用: 10
    good_wrapper(b);       // 输出: 处理常量引用: 20
    good_wrapper(30);      // 输出: 处理右值引用: 30 ✓
    good_wrapper(std::move(a)); // 输出: 处理右值引用: 10 ✓
    
    return 0;
}

怎么样?是不是保留了这些属性!!

2.1.4.可变模板参数和std::forward结合

#include <iostream>
#include <utility>

// 模拟一个接受多个参数的函数
void process(int a, double b, const std::string& c) {
    std::cout << "处理: " << a << ", " << b << ", " << c << std::endl;
}

// 通用包装器
template<typename... Args>
void wrapper(Args&&... args) {
    // 将所有参数完美转发给process
    process(std::forward<Args>(args)...);
}

int main() {
    std::string str = "hello";
    
    wrapper(1, 3.14, "world");        // 右值转发
    wrapper(2, 2.71, str);            // str是左值,以左值引用转发
}

这个其实很简单

2.2.线程池实现

    基于线程池执行任务的时候,入口函数内部执行逻辑是固定的,因此选择 std::packaged_task 加上 std::future 的组合来实现。

    #include <iostream>
    #include <vector>
    #include <thread>
    #include <mutex>
    #include <condition_variable>
    #include <future>
    #include <functional>
    
    /* 实现一个能够获取任务返回值的线程池 */
    class ThreadPool
    {
    public:
        // 定义任务类型为无参数无返回值的函数对象
        //也就是给函数类型void() 起别名为Functor
        using Functor = std::function<void()>;
    
        // 构造函数,创建指定数量的工作线程
        ThreadPool(size_t tcount = 1) : _stop(false)
        {
            // 循环创建指定数量的工作线程
            for (int i = 0; i < tcount; i++)
            {
                // 每个线程执行entry函数,this指针传递当前对象
                _threads.emplace_back(&ThreadPool::entry, this);
            }
        }
        
        // 析构函数,确保线程池正确停止
        ~ThreadPool() 
        { 
            stop(); 
        }
    
        // 向线程池提交任务,支持任意类型函数和参数
        //push函数内部,会将这个传入的函数封装成一个异步任务(packaged_task),抛入到std::vector<Functor> _tasks; 任务队列里面
        // 返回std::future对象,这个std::future对象是模板类,然后就通过这个std::future对象来获取任务的结果
        template <typename F, typename... Args>
        auto push(F &&func, Args &&...args) -> std::future<decltype(func(args...))>
        {  
            // 推导传入函数的返回值类型
            using return_type = decltype(func(args...));
    
            // 使用std::bind将函数和参数绑定,创建无参可调用对象
            //这个时候调用nfunc()就等价于调用func(args...)
            auto nfunc = std::bind(std::forward<F>(func),std::forward<Args>(args)...);
    
            //std::packaged_task不能直接传递给std::thread,因为std::packaged_task不可拷贝,而且它也不是一个函数对象
            //所以把它放到智能指针里面去
            auto task = std::make_shared<std::packaged_task<return_type()>>(nfunc);
    
            // 在移动packaged_task之前获取future对象
            std::future<return_type> ret = task->get_future();
    
            //
            {
                // 加锁保护任务队列
                std::unique_lock<std::mutex> lock(_mutex);
                
                // 如果线程池未停止,将任务加入队列
                if (!_stop)
                {
                    //将构造出来的任务放到任务队列里面(使用lambda表达式)
                    _tasks.push_back([task]() {
                        (*task)();//(*task)()本质是可调用对象
                    });
                    // 通知一个等待的工作线程有新任务
                    _cond.notify_one();
                }
            }
            // 返回future对象,调用者可以通过它获取任务执行结果
            return ret;
        }
        
        // 停止线程池,等待所有工作线程完成
        void stop()
        {
            if(_stop==true) return;
            
            {
                // 加锁设置停止标志
                std::unique_lock<std::mutex> lock(_mutex);
                _stop = true;
            }
            
            // 通知所有等待的工作线程
            _cond.notify_all();//唤醒所有工作线程
            
            // 等待所有工作线程结束
            for (auto &thread : _threads)
            {
                if (thread.joinable())
                    thread.join();
            }
        }
    
    private:
        // 工作线程的入口函数
        void entry()
        {
            while (!_stop)
            {
                std::vector<Functor> tmp_taskpool;
                
                {
                    // 加锁访问任务队列
                    std::unique_lock<std::mutex> lock(_mutex);
                    
                    // 如果线程池已停止且任务队列为空,退出循环
                    if (_stop && _tasks.empty())
                        break;
                        
                    // 等待条件:线程池停止或有新任务
                    // wait会在条件满足时自动释放锁,被唤醒后重新获取锁
                    _cond.wait(lock, [this]
                               { return this->_stop || !this->_tasks.empty(); });
                    
                    // 交换任务队列,减少锁的持有时间
                    // 这样可以一次性取出所有待处理任务,在锁外执行
                    tmp_taskpool.swap(_tasks);
                }
                
                // 执行所有取出的任务
                for (auto &task : tmp_taskpool)
                {
                    task();//运行任何队列里面的函数
                }
            }
        }
    
    private:
        std::atomic<bool> _stop;           // 线程池停止标志,原子操作保证线程安全
        std::mutex _mutex;                 // 互斥锁,保护任务队列和停止标志
        std::condition_variable _cond;     // 条件变量,用于线程间同步
        std::vector<std::thread> _threads; // 工作线程池
        std::vector<Functor> _tasks;       // 任务队列,存储待执行的任务
    };

    我们去写一个测试案例

    int Add(int num1,int num2)
    {
        return num1+num2;
    }
    int main()
    {
        ThreadPool pool(4);
    
        for(int i=0;i<10;i++)
        {
            std::future<int> fu=pool.push(Add,11,i);
            std::cout<<fu.get()<<std::endl;
        }
        pool.stop();
    }

    没有问题啊!!

    Logo

    Agent 垂直技术社区,欢迎活跃、内容共建。

    更多推荐