You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++中std::thread如何存储构造传入的可变参数并完成调用?

核心实现逻辑

你觉得tuple必须手动指定类型、没法存可变参数,是因为你只见过手动声明tuple的场景,std::thread和std::function的实现根本不需要“绕过”这个限制——靠模板参数自动推导 + 类型擦除 + 编译期索引展开三个特性就能完整实现整套逻辑,没有任何黑魔法。

参数是怎么存进tuple的

你写std::thread t(printStuff, "beer", 900000000);的时候,std::thread的构造函数是可变参数模板,编译器会根据你传入的实参自动推导出所有参数的类型:

  • 传入的printStuff是函数类型void(const char*, long),衰变后存为函数指针
  • 传入的"beer"是const char[5]类型的字符串字面量,衰变后存为const char*
  • 传入的900000000是整数,匹配函数参数类型后存为long

推导完成后,构造函数内部直接用这组推导出来的类型,实例化一个对应类型的tuple存所有参数,全程不需要你手动写tuple的模板参数。

为了把不同类型的tuple和可调用对象统一存在std::thread对象里,实现会用一层类型擦除:定义一个无模板的基类,基类里声明纯虚的入口方法,再写一个带模板参数的派生类,派生类里持有可调用对象和对应类型的tuple,重写入口方法实现实际调用。std::thread内部只需要持有一个基类指针,就能藏住所有具体类型细节。

给你写个极简的可运行简化版实现,一看就懂:

#include <tuple>
#include <memory>
#include <iostream>
#include <thread>
#include <functional> // for std::apply

class SimpleThread {
private:
    // 类型擦除基类,不持有任何具体类型信息
    struct StateBase {
        virtual void entry() = 0;
        virtual ~StateBase() = default;
    };

    // 派生类,持有具体类型的函数和参数tuple
    template<class F, class... Args>
    struct State : StateBase {
        F func;
        std::tuple<std::decay_t<Args>...> stored_args; // 自动匹配参数类型的tuple

        State(F&& f, Args&&... args)
            : func(std::forward<F>(f)),
              stored_args(std::forward<Args>(args)...) {}

        void entry() override {
            // 展开tuple里的参数调用目标函数
            std::apply(func, stored_args);
        }
    };

    std::unique_ptr<StateBase> state_;
    std::thread native_thread_;

public:
    template<class F, class... Args>
    explicit SimpleThread(F&& f, Args&&... args) {
        // 这里编译器已经推导出F和Args...的具体类型,直接实例化State即可
        state_ = std::make_unique<State<F, Args...>>(
            std::forward<F>(f),
            std::forward<Args>(args)...
        );
        // 启动系统线程,入口就是state_->entry
        native_thread_ = std::thread([this](){ state_->entry(); });
    }

    void join() {
        native_thread_.join();
    }

    // 省略detach、移动构造、禁止拷贝等逻辑
};

// 测试你写的例子
void printStuff(const char* c, long x) {
   std::cout << x << " bottles of " << c << " on the wall\n";
}

int main() {
    SimpleThread t(printStuff, "beer", 900000000);
    t.join();
}

这里要纠正一个错误认知:参数不是在join()/detach()的时候才传入函数的。线程构造完成的瞬间,所有参数就已经被拷贝/移动到内部的tuple里了,子线程启动时直接拿存好的参数调用函数,join()只是阻塞等待子线程执行结束,本身不参与参数传递和函数调用。

怎么取出tuple里的参数完成调用

从tuple里取可变参数的核心是编译期整数序列(std::index_sequence),整个展开过程在编译期完成,没有运行时开销。C++17提供的std::apply就是封装好的展开工具,你自己实现也只需要十几行代码:

// 辅助函数,用编译期索引展开tuple
template<class F, class Tuple, size_t... Index>
decltype(auto) apply_impl(F&& f, Tuple&& t, std::index_sequence<Index...>) {
    // 编译期展开后等价于直接调用 f(std::get<0>(t), std::get<1>(t), std::get<2>(t)...)
    return std::forward<F>(f)(std::get<Index>(std::forward<Tuple>(t))...);
}

template<class F, class Tuple>
decltype(auto) my_apply(F&& f, Tuple&& t) {
    // 生成和tuple长度一致的0,1,2...N的索引序列
    constexpr size_t tuple_len = std::tuple_size_v<std::remove_reference_t<Tuple>>;
    return apply_impl(
        std::forward<F>(f),
        std::forward<Tuple>(t),
        std::make_index_sequence<tuple_len>{}
    );
}

std::function的实现差异

std::function的逻辑和std::thread几乎一致,只有一点区别:

  • std::thread的参数类型是靠构造函数的实参推导的
  • std::function的参数类型和返回值类型是类模板声明时就固定的,比如std::function<void(const char*, long)>从定义开始就确定了要存的函数签名,内部存储参数的tuple类型直接和类模板参数绑定,不需要靠构造函数推导。

你自己实现简易版std::function的时候,完全可以复用上面的类型擦除+tuple+索引展开逻辑,只需要在operator()里接收调用时传入的参数,和提前绑定的参数(比如std::bind存的固定参数)合并后展开调用即可。

实现细节提醒:存参数的时候一定要用std::decay_t处理类型,把数组类型转成指针、去掉引用和cv限定符,避免存悬垂引用或者类型不匹配的问题——比如你传的字符串字面量是数组类型,不做衰变处理的话tuple里会存数组类型,根本没法正常传参。

内容的提问来源于stack exchange,提问作者TwistedBlizzard

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 18:27:42