「C++ 可调用对象」是线程池、异步编程、泛型编程的基础
一、先给「可调用对象」下一个终极人话定义
C++ 中的可调用对象(Callable Object) = 所有能通过「加括号()」执行的实体。
简单说:只要你能写出 对象() 或 对象(参数) 并且编译器不报错,这个 "对象" 就是可调用对象。
类比:可调用对象就像 "能按一下就干活的工具"------ 不管是螺丝刀(普通函数)、带手柄的螺丝刀(函数指针)、定制扳手(函数对象)、临时做的工具(Lambda),只要 "按一下(加())" 就能干活,都是可调用对象。
cpptemplate<typename F, typename... Args> void submit(F&& f, Args&&... args) { auto task = std::bind(std::forward<F>(f), std::forward<Args>(args)...); { std::unique_lock<std::mutex> lk(mtx); if (stop) throw std::runtime_error("submit to stopped pool"); tasks.emplace(std::move(task)); } cv.notify_one(); }在这个泛型函数中:
F是所有能加()调用的 "可调用对象" 的类型 ------C++ 里只要能通过对象()或对象(参数)执行的东西,都能作为F,核心分 6 类:
- 普通函数(全局 / 局部)
- 函数指针
- 类的静态成员函数
- 类的非静态成员函数(需要绑定对象)
- 函数对象(仿函数,struct 重载
operator())- Lambda 表达式(最常用)
二、可调用对象的 5 大分类(从简单到复杂,逐个拆解)
C++ 里的可调用对象分 5 类,每类都配「定义 + 示例 + 直接调用 + 线程池 submit 调用」,你能直接复制运行,一看就懂。
前置准备:简化版线程池(保留核心submit,方便测试)
cpp
#include <iostream>
#include <functional>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <queue>
#include <thread>
#include <atomic>
// 简化版线程池(仅保留submit核心逻辑)
class ThreadPool {
public:
explicit ThreadPool(size_t n = 1) : stop(false) {
for (size_t i=0; i<n; ++i) {
workers.emplace_back([this]() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lk(mtx);
cv.wait(lk, [this](){ return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task(); // 执行任务
}
});
}
}
template<typename F, typename... Args>
void submit(F&& f, Args&&... args) {
auto task = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
{
std::unique_lock<std::mutex> lk(mtx);
if (stop) throw std::runtime_error("submit to stopped pool");
tasks.emplace(std::move(task));
}
cv.notify_one();
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lk(mtx);
stop = true;
}
cv.notify_all();
for (auto& t : workers) if (t.joinable()) t.join();
}
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
std::atomic<bool> stop;
};
1. 第一类:普通函数 & 函数指针(最基础)
定义
- 普通函数:写在全局 / 局部的、有名字的 "固定功能代码块",能直接
函数名(参数)调用; - 函数指针:指向普通函数的 "地址变量",相当于 "函数的把手",能通过
指针名(参数)调用。
示例代码(直接调用 + 线程池调用)
cpp
// 1. 普通全局函数
void print_num(int num) {
std::cout << "[普通函数] 数字:" << num << " | 线程ID:" << std::this_thread::get_id() << std::endl;
}
int main() {
ThreadPool pool(1);
// ① 直接调用普通函数
print_num(100);
// ② 函数指针:指向普通函数
void (*fp)(int) = print_num;
fp(200); // 直接调用函数指针
// ③ 线程池submit调用(普通函数)
pool.submit(print_num, 300);
// ④ 线程池submit调用(函数指针)
pool.submit(fp, 400);
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0;
}
关键特点
- 无状态:调用结果只和参数有关,和 "调用次数 / 外部变量" 无关;
- 函数指针本质是 "地址",可以像普通变量一样传递、存储;
- 线程池调用时,直接传函数名 / 函数指针 + 参数即可。
2. 第二类:类的成员函数(静态 vs 非静态)
定义
- 静态成员函数:属于 "类本身" 的函数,没有隐含的
this指针,和普通函数几乎一样; - 非静态成员函数:属于 "类的对象" 的函数,有隐含的
this指针(指向调用的对象),必须绑定对象才能调用。
示例代码(直接调用 + 线程池调用)
cpp
class ToolBox {
public:
int base = 10; // 非静态成员变量(对象独有)
// ① 静态成员函数(属于类)
static void static_func(int num) {
std::cout << "[静态成员函数] 数字:" << num << " | 线程ID:" << std::this_thread::get_id() << std::endl;
}
// ② 非静态成员函数(属于对象)
void non_static_func(int num) {
// this指针指向调用该函数的对象
std::cout << "[非静态成员函数] 数字:" << num + base << " | 线程ID:" << std::this_thread::get_id() << std::endl;
}
};
int main() {
ThreadPool pool(1);
ToolBox obj; // 非静态成员函数必须有对象
// ① 直接调用静态成员函数(类名::函数名)
ToolBox::static_func(100);
// ② 直接调用非静态成员函数(对象.函数名)
obj.non_static_func(200); // 200+obj.base=10 → 210
// ③ 线程池submit调用静态成员函数(和普通函数一样)
pool.submit(ToolBox::static_func, 300);
// ④ 线程池submit调用非静态成员函数(必须绑定对象)
// 格式:&类名::函数名, 对象指针/引用, 函数参数
pool.submit(&ToolBox::non_static_func, &obj, 400); // 400+10=410
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0;
}
关键特点
- 静态成员函数:无
this,可直接调用,线程池调用和普通函数一致; - 非静态成员函数:必须绑对象(
&obj),因为this指针需要知道 "操作哪个对象"; - 线程池调用非静态成员函数时,第一个参数是
&类名::函数名,第二个参数是对象指针 / 引用,后续是函数参数。
3. 第三类:函数对象(仿函数)
定义
重载了operator()的结构体 / 类(也就是 "能当函数用的对象")------ 本质是 "有状态的函数",对象里的成员变量可以保存 "上下文"。
示例代码(直接调用 + 线程池调用)
cpp
// 函数对象:重载operator()的结构体
struct Calculate {
// 有状态:保存一个偏移量(对象独有)
int offset = 5;
// 重载operator(),变成"可调用"
void operator()(int num) {
std::cout << "[函数对象] 计算结果:" << num + offset << " | 线程ID:" << std::this_thread::get_id() << std::endl;
}
};
int main() {
ThreadPool pool(1);
// ① 直接调用函数对象(先创建对象,再加())
Calculate calc; // 创建对象(offset=5)
calc(100); // 100+5=105 → 调用operator()
// ② 线程池submit调用(传临时对象 + 参数)
pool.submit(Calculate(), 200); // 200+5=205
// ③ 自定义状态的函数对象
Calculate calc2;
calc2.offset = 10; // 修改状态
pool.submit(calc2, 300); // 300+10=310
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0;
}
关键特点
- 有状态:对象的成员变量可以保存上下文(比如偏移量、配置等),调用结果和状态有关;
- 类型明确:每个函数对象都是一个独立的类型(比如
Calculate); - 线程池调用时,传对象(临时对象 / 已有对象) + 参数即可。
4. 第四类:Lambda 表达式(实战最常用)
定义
编译器自动生成的 "匿名函数对象"------ 不用定义结构体 / 函数,直接写 "临时的可调用逻辑",还能捕获外部变量(核心优势)。
示例代码(直接调用 + 线程池调用)
cpp
int main() {
ThreadPool pool(1);
int external_var = 100; // 外部变量
// ① 无捕获Lambda(和普通函数一样,无状态)
auto lambda1 = [](int num) {
std::cout << "[无捕获Lambda] 数字:" << num << " | 线程ID:" << std::this_thread::get_id() << std::endl;
};
lambda1(100); // 直接调用
pool.submit(lambda1, 200); // 线程池调用
// ② 按值捕获Lambda(拷贝外部变量,有状态)
auto lambda2 = [external_var](int num) {
// 捕获的external_var是拷贝,和原变量无关
std::cout << "[值捕获Lambda] 结果:" << num + external_var << " | 线程ID:" << std::this_thread::get_id() << std::endl;
};
lambda2(300); // 300+100=400
pool.submit(lambda2, 400); // 400+100=500
// ③ 按引用捕获Lambda(引用外部变量,注意生命周期)
auto lambda3 = [&external_var](int num) {
external_var += 10; // 修改原变量
std::cout << "[引用捕获Lambda] 结果:" << num + external_var << " | 线程ID:" << std::this_thread::get_id() << std::endl;
};
lambda3(500); // 500+(100+10)=610
pool.submit(lambda3, 600); // 600+(110+10)=720
// ④ 线程池直接传匿名Lambda(最常用写法)
pool.submit([](int a, int b) {
std::cout << "[匿名Lambda] 求和:" << a + b << " | 线程ID:" << std::this_thread::get_id() << std::endl;
}, 10, 20); // 30
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0;
}
关键特点
- 匿名性:没有名字,可直接写在调用处(比如 submit 里),代码更简洁;
- 可捕获外部变量:
- 按值捕获
[var]:拷贝外部变量,Lambda 内部修改不影响原变量; - 按引用捕获
[&var]:引用外部变量,修改会影响原变量(注意:必须保证外部变量生命周期 ≥ Lambda 执行周期);
- 按值捕获
- 实战首选:线程池里 90% 的场景用 Lambda,因为灵活、不用定义额外结构。
为什么Lambda有状态:
Lambda 之所以能有状态,是因为它本质是编译器自动生成的 "匿名函数对象" ,你捕获的外部变量会被编译器打包成这个匿名对象的成员变量(归 Lambda 自己所有),而不是像普通函数那样 "借用外部变量的引用"。
简单说:Lambda 的 "捕获" 不是 "临时借用",而是 "把外部变量变成自己的东西" ------ 这些变量成了 Lambda 自身的状态,跨调用保留,这就是它有状态的原因。
第一步:先拆 Lambda 的底层本质(编译器偷偷干的事)
你写的 Lambda,编译器会自动帮你生成一个匿名的结构体(和你之前写的函数对象一模一样),捕获的变量会变成这个结构体的成员变量。
比如你写:
cpp
int external = 10;
auto lambda = [external](int num) {
return external + num;
};
编译器会自动生成这样的代码(伪代码):
cpp
// 编译器生成的匿名结构体(你看不到,但真实存在)
struct __AnonymousLambdaXXXX {
// 捕获的external变成结构体的成员变量(Lambda自身的状态)
int external;
// 构造函数:把外部的external拷贝进来
__AnonymousLambdaXXXX(int ext) : external(ext) {}
// 重载operator()(可调用)
int operator()(int num) {
return external + num;
}
};
// 你定义的lambda,其实是这个匿名结构体的对象
__AnonymousLambdaXXXX lambda(10);
看到了吗?你捕获的external,已经不是 "外部变量" 了,而是Lambda 对象自己的成员变量------ 状态归 Lambda 所有,不是归外部所有。
第二步:用示例对比 "Lambda 捕获" vs "普通函数传引用"
示例 1:Lambda 按值捕获(自身持有状态,跨调用保留)
cpp
#include <iostream>
int main() {
int external = 10;
// Lambda按值捕获external → 变成自己的成员变量(状态归Lambda)
auto adder = [external](int num) mutable { // mutable允许修改自身状态
external += num; // 修改的是Lambda自己的external,不是外部的!
std::cout << "Lambda自身状态:" << external << std::endl;
return external;
};
// 第一次调用:修改Lambda自身的状态(external=10+5=15)
adder(5);
// 第二次调用:用的是Lambda自身保留的状态(15+3=18)
adder(3);
// 外部的external完全没变化!因为Lambda改的是自己的副本
std::cout << "外部变量:" << external << std::endl;
return 0;
}
5. 第五类:绑定对象(std::bind 的结果)
定义
std::bind把 "可调用对象 + 部分 / 全部参数" 绑定后生成的新可调用对象 ------ 相当于 "预设了参数的工具",调用时不用传绑定过的参数。
示例代码(直接调用 + 线程池调用)
cpp
// 普通函数
void print_info(const std::string& prefix, int num) {
std::cout << "[绑定对象] " << prefix << ":" << num << " | 线程ID:" << std::this_thread::get_id() << std::endl;
}
int main() {
ThreadPool pool(1);
// ① 绑定部分参数:固定prefix为"绑定测试",留num为可变参数
auto bound_func1 = std::bind(print_info, "绑定测试", std::placeholders::_1);
bound_func1(100); // 等价于print_info("绑定测试", 100)
// ② 绑定全部参数:prefix和num都固定,变成无参可调用对象
auto bound_func2 = std::bind(print_info, "无参绑定", 200);
bound_func2(); // 等价于print_info("无参绑定", 200)
// ③ 线程池调用绑定对象(部分参数)
pool.submit(bound_func1, 300); // 传剩下的num=300
// ④ 线程池调用绑定对象(全部参数,无参调用)
pool.submit(bound_func2); // 不用传参数
std::this_thread::sleep_for(std::chrono::seconds(1));
return 0;
}
关键特点
- 预设参数:可以把可调用对象的部分 / 全部参数提前绑定,简化后续调用;
std::placeholders::_1:占位符,表示 "这个参数留到调用时传";- 线程池 submit 里的核心逻辑:就是用
std::bind把任意可调用对象 + 参数绑定成无参对象,存入队列。
三、可调用对象的核心共性 & 关键区别(表格版)
| 类型 | 是否有实例专属状态 | 是否需要绑定对象 | 能否捕获外部变量 | 实战使用频率 | 核心优势 |
|---|---|---|---|---|---|
| 普通函数 / 函数指针 | ❌ 无 | ❌ 不需要 | ❌ 不能 | ★★★☆☆ | 简单、无状态 |
| 类静态成员函数 | ❌ 无 | ❌ 不需要 | ❌ 不能 | ★★☆☆☆ | 属于类,全局可用 |
| 类非静态成员函数 | ✅ 有(对象) | ✅ 必须绑定 | ✅ 可访问对象成员 | ★★☆☆☆ | 操作对象状态 |
| 函数对象(仿函数) | ✅ 有 | ❌ 不需要 | ❌ 直接捕获(需成员变量) | ★★★☆☆ | 自定义状态、类型明确 |
| Lambda 表达式 | ✅ 可选 | ❌ 不需要 | ✅ 灵活捕获 | ★★★★★ | 简洁、灵活、可捕获变量 |
| 绑定对象(std::bind) | ✅ 可选 | ❌ 不需要 | ✅ 继承原对象 | ★★★★☆ | 预设参数、统一接口 |
**实例专属状态:**每个实例都有一份只属于自己的 "私有数据",修改这个数据只会影响当前实例,和其他同类型实例完全无关。
四、可调用对象的 "统一化":std::function(函数包装器)
定义
std::function是 "可调用对象的万能收纳盒"------ 不管是哪种可调用对象,都能装进std::function里,变成统一类型,方便存储、传递(比如线程池的任务队列)。
示例代码(统一存储不同可调用对象)
cpp
int main() {
// 定义统一类型:无参、无返回值的可调用对象
std::function<void()> func;
// 1. 装Lambda
func = []() { std::cout << "Lambda装进function\n"; };
func();
// 2. 装函数对象
func = Calculate();
func();
// 3. 装绑定后的普通函数
func = std::bind(print_num, 100);
func();
// 线程池的任务队列就是std::queue<std::function<void()>>,统一存储所有可调用对象
return 0;
}
关键作用
- 解决 "不同可调用对象类型不同,无法存入同一容器" 的问题;
- 线程池的任务队列能接收任意可调用对象,全靠
std::function<void()>统一类型。
五、总结(核心要点,记死就行)
- 可调用对象的本质 :能加
()调用的实体,核心是 "可以执行的代码块"; - 核心分类 :
- 无状态:普通函数 / 函数指针 / 静态成员函数;
- 有状态:非静态成员函数(绑定对象)、函数对象、Lambda、绑定对象;
- 实战首选:Lambda 表达式(简洁、可捕获变量);
- 统一化工具 :
std::function(收纳所有可调用对象); - 线程池的核心逻辑 :用
std::bind把任意可调用对象 + 参数绑定成std::function<void()>,存入队列执行。
可调用对象的核心价值是 "统一执行接口"------ 不管你是写函数、写结构体、写 Lambda,最终都能通过()调用,这也是 C++ 泛型编程、异步编程的基础。