可调用对象

「C++ 可调用对象」是线程池、异步编程、泛型编程的基础

一、先给「可调用对象」下一个终极人话定义

C++ 中的可调用对象(Callable Object) = 所有能通过「加括号()」执行的实体

简单说:只要你能写出 对象()对象(参数) 并且编译器不报错,这个 "对象" 就是可调用对象

类比:可调用对象就像 "能按一下就干活的工具"------ 不管是螺丝刀(普通函数)、带手柄的螺丝刀(函数指针)、定制扳手(函数对象)、临时做的工具(Lambda),只要 "按一下(加())" 就能干活,都是可调用对象。

cpp 复制代码
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();
}

在这个泛型函数中: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()>统一类型。

五、总结(核心要点,记死就行)

  1. 可调用对象的本质 :能加()调用的实体,核心是 "可以执行的代码块";
  2. 核心分类
    • 无状态:普通函数 / 函数指针 / 静态成员函数;
    • 有状态:非静态成员函数(绑定对象)、函数对象、Lambda、绑定对象;
  3. 实战首选:Lambda 表达式(简洁、可捕获变量);
  4. 统一化工具std::function(收纳所有可调用对象);
  5. 线程池的核心逻辑 :用std::bind把任意可调用对象 + 参数绑定成std::function<void()>,存入队列执行。

可调用对象的核心价值是 "统一执行接口"------ 不管你是写函数、写结构体、写 Lambda,最终都能通过()调用,这也是 C++ 泛型编程、异步编程的基础。

相关推荐
2 小时前
java关于引用
java·开发语言
小小码农Come on2 小时前
QT布局介绍
开发语言·qt
晚风吹长发2 小时前
初步了解Linux中的线程概率及线程控制
linux·运维·服务器·开发语言·c++·centos·线程
毕设源码-朱学姐2 小时前
【开题答辩全过程】以 基于python网络安全知识在线答题系统为例,包含答辩的问题和答案
开发语言·python·web安全
wjs20242 小时前
PHP Misc
开发语言
Highcharts.js2 小时前
Next.js 集成 Highcharts 官网文档说明(2025 新版)
开发语言·前端·javascript·react.js·开发文档·next.js·highcharts
CodeByV2 小时前
【Qt】信号与槽
开发语言·qt
爱学习的阿磊2 小时前
模板代码跨编译器兼容
开发语言·c++·算法
带鱼吃猫2 小时前
C++STL:从 0 到 1 手写 C++ string以及高频易错点复盘
开发语言·c++