std::function

目录

一、基础核心

[1. 什么是可调用对象?(std::function的包装目标)](#1. 什么是可调用对象?(std::function的包装目标))

[2. std::function的语法与基础用法](#2. std::function的语法与基础用法)

[3. std::function vs 函数指针](#3. std::function vs 函数指针)

二、std::function的实现原理与关键机制

[1. 核心原理:类型擦除(Type Erasure)](#1. 核心原理:类型擦除(Type Erasure))

[2. std::function与std::bind的配合](#2. std::function与std::bind的配合)

[3. 空std::function的处理](#3. 空std::function的处理)

[4. 性能开销分析](#4. 性能开销分析)

[5. 优化技巧](#5. 优化技巧)

三、基于std::function构建核心组件

[1. 实战 1:事件驱动架构 ------ 实现 muduo 风格的事件回调](#1. 实战 1:事件驱动架构 —— 实现 muduo 风格的事件回调)

[2. 实战 2:线程池任务队列 ------ 包装任意任务](#2. 实战 2:线程池任务队列 —— 包装任意任务)

[3. 实战 3:策略模式 ------ 动态切换算法](#3. 实战 3:策略模式 —— 动态切换算法)

四、避坑指南

[坑点 1:绑定局部对象的this指针](#坑点 1:绑定局部对象的this指针)

[坑点 2:捕获悬空引用的 lambda](#坑点 2:捕获悬空引用的 lambda)

[坑点 3:滥用std::function导致性能下降](#坑点 3:滥用std::function导致性能下降)

[坑点 4:忽略std::function的拷贝开销](#坑点 4:忽略std::function的拷贝开销)

[Linux 调试技巧:排查std::function问题](#Linux 调试技巧:排查std::function问题)

五、总结


在 Linux C++ 后端开发中,std::function 是 C++11 引入的可调用对象包装器 ,是实现回调机制事件驱动架构策略模式的核心工具。它解决了传统函数指针的局限性 ------ 能统一包装函数指针、仿函数、lambda 表达式、类成员函数等所有可调用对象,实现类型无关的回调管理。

一、基础核心

1. 什么是可调用对象?(std::function的包装目标)

在 C++ 中,可调用对象 是指所有可以通过()运算符执行的实体,包含 4 类,这是理解std::function的前提:

  1. 普通函数 / 函数指针 :如int add(int, int)
  2. 仿函数(重载()的类对象) :如struct Add { int operator()(int a, int b) { return a+b; } }
  3. lambda 表达式 :如[](int a, int b) { return a+b; }
  4. 类成员函数 / 成员函数指针 :如class Math { public: int mul(int a, int b) { return a*b; } }

传统函数指针只能指向普通函数,无法兼容 lambda(尤其是带捕获的 lambda)和成员函数;而std::function的核心作用就是用统一的接口包装所有可调用对象,实现 "类型擦除"。

2. std::function的语法与基础用法

std::function是一个模板类,模板参数为函数签名(返回值类型 + 参数类型列表),语法格式:

cpp 复制代码
std::function<返回值类型(参数类型1, 参数类型2, ...)>

核心特性:

  • 可以存储任何签名匹配的可调用对象;
  • 支持空状态(默认构造的std::function为空,调用会抛异常);
  • 可拷贝、可赋值,支持作为容器元素(如std::vector<std::function<void()>>)。

示例 1:包装普通函数、仿函数、lambda

cpp 复制代码
#include <iostream>
#include <functional> // 必须包含头文件

// 1. 普通函数
int add(int a, int b) {
    return a + b;
}

// 2. 仿函数(重载())
struct Mul {
    int operator()(int a, int b) const {
        return a * b;
    }
};

int main() {
    // 定义function对象,签名为int(int, int)
    std::function<int(int, int)> func;

    // 包装普通函数
    func = add;
    std::cout << "add: " << func(3, 5) << std::endl; // 输出8

    // 包装仿函数对象
    func = Mul();
    std::cout << "mul: " << func(3, 5) << std::endl; // 输出15

    // 包装lambda表达式(带捕获)
    int base = 10;
    func = [base](int a, int b) { return a + b + base; };
    std::cout << "lambda: " << func(3, 5) << std::endl; // 输出18

    return 0;
}

编译运行(Linux)

bash 复制代码
g++ -std=c++11 function_basic.cpp -o function_basic
./function_basic
# 输出:
# add: 8
# mul: 15
# lambda: 18

示例 2:包装类成员函数(关键坑点)

类成员函数有一个隐式的this参数 ,无法直接赋值给std::function,需配合std::bind或 lambda 捕获this指针:

cpp 复制代码
#include <iostream>
#include <functional>
#include <memory>

class Math {
public:
    int div(int a, int b) const {
        return a / b;
    }
};

int main() {
    std::function<int(const Math&, int, int)> func_div;
    // 包装成员函数:用&获取成员函数指针
    func_div = &Math::div;

    Math math;
    // 调用时必须传入对象(或指针)作为第一个参数
    std::cout << "div: " << func_div(math, 10, 2) << std::endl; // 输出5

    // 更常用:用lambda捕获对象,隐藏this参数
    std::function<int(int, int)> func_div_lambda = [&math](int a, int b) {
        return math.div(a, b);
    };
    std::cout << "div lambda: " << func_div_lambda(10, 2) << std::endl; // 输出5

    return 0;
}
3. std::function vs 函数指针
维度 std::function 函数指针
包装范围 支持普通函数、仿函数、lambda、成员函数 仅支持普通函数 / 静态成员函数
类型安全 编译期检查签名匹配,不匹配则编译报错 弱类型,强制转换可能导致运行时错误
空状态处理 支持空状态,调用空对象抛std::bad_function_call 空指针调用直接崩溃(段错误)
灵活性 可拷贝、可作为容器元素、支持赋值 不可拷贝(本质是指针),无法直接存入容器
性能开销 有轻微间接调用开销(类型擦除导致) 无额外开销,直接调用

二、std::function的实现原理与关键机制

1. 核心原理:类型擦除(Type Erasure)

std::function能包装任意类型的可调用对象,核心是类型擦除 ------ 通过基类多态隐藏具体可调用对象的类型,只暴露统一的调用接口。

简化版实现原理(手写迷你std::function

cpp 复制代码
#include <iostream>
#include <memory>

// 步骤1:定义抽象基类,暴露统一调用接口
template <typename Ret, typename... Args>
class FunctionBase {
public:
    virtual ~FunctionBase() = default;
    virtual Ret invoke(Args... args) const = 0;
};

// 步骤2:定义派生类,存储具体可调用对象
template <typename Func, typename Ret, typename... Args>
class FunctionImpl : public FunctionBase<Ret, Args...> {
private:
    Func func_; // 存储可调用对象(函数、lambda、仿函数等)
public:
    explicit FunctionImpl(Func func) : func_(std::move(func)) {}

    Ret invoke(Args... args) const override {
        return func_(std::forward<Args>(args)...); // 调用具体对象
    }
};

// 步骤3:包装类,实现类型擦除
template <typename Signature>
class MyFunction;

// 偏特化:提取返回值和参数类型
template <typename Ret, typename... Args>
class MyFunction<Ret(Args...)> {
private:
    std::unique_ptr<FunctionBase<Ret, Args...>> impl_; // 指向派生类对象
public:
    // 默认构造:空状态
    MyFunction() = default;

    // 构造函数:接受任意可调用对象
    template <typename Func>
    MyFunction(Func func) : impl_(std::make_unique<FunctionImpl<Func, Ret, Args...>>(std::move(func))) {}

    // 重载()运算符,调用可调用对象
    Ret operator()(Args... args) const {
        if (!impl_) {
            throw std::bad_function_call(); // 空对象调用抛异常
        }
        return impl_->invoke(std::forward<Args>(args)...);
    }
};

// 测试
int add(int a, int b) { return a + b; }

int main() {
    MyFunction<int(int, int)> func;
    func = add;
    std::cout << func(3,5) << std::endl; // 输出8

    func = [](int a, int b) { return a*b; };
    std::cout << func(3,5) << std::endl; // 输出15

    return 0;
}

类型擦除原理

  • std::function内部维护一个指向抽象基类的指针;
  • 当包装一个可调用对象时,会生成一个派生类 ,存储该可调用对象,并实现基类的invoke接口;
  • 调用std::function()运算符时,通过基类指针调用派生类的invoke方法,间接调用底层可调用对象;
  • 整个过程隐藏了可调用对象的具体类型,只暴露统一的函数签名 ------ 这就是类型擦除
2. std::functionstd::bind的配合

std::bind的作用是绑定可调用对象的参数,返回一个新的可调用对象,常用于:

  • 绑定类成员函数的this指针;
  • 固定部分参数(柯里化);
  • 调整参数顺序。

示例:绑定类成员函数与参数柯里化

cpp 复制代码
#include <iostream>
#include <functional>

class Calculator {
public:
    int compute(int a, int b, char op) const {
        switch(op) {
            case '+': return a + b;
            case '*': return a * b;
            default: return 0;
        }
    }
};

int main() {
    Calculator calc;

    // 1. 绑定成员函数+this指针:固定对象,参数留空
    std::function<int(int, int, char)> func1 = std::bind(&Calculator::compute, &calc, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
    std::cout << func1(3,5,'+') << std::endl; // 输出8

    // 2. 参数柯里化:固定运算符为'*',只传两个数
    std::function<int(int, int)> func2 = std::bind(&Calculator::compute, &calc, std::placeholders::_1, std::placeholders::_2, '*');
    std::cout << func2(3,5) << std::endl; // 输出15

    return 0;
}
  • std::bind绑定的参数是值拷贝 ,若需传递引用,需用std::ref/std::cref
  • 绑定的对象生命周期必须长于std::function,否则会出现悬空指针 (如绑定局部对象的this)。
3. 空std::function的处理

默认构造的std::function为空,直接调用会抛出std::bad_function_call异常,工业级代码必须处理空状态:

cpp 复制代码
#include <iostream>
#include <functional>
#include <stdexcept>

int main() {
    std::function<int(int, int)> func; // 空对象

    // 错误用法:直接调用,抛异常
    // std::cout << func(3,5) << std::endl;

    // 正确用法1:判断是否为空
    if (func) {
        std::cout << func(3,5) << std::endl;
    } else {
        std::cout << "func is empty" << std::endl;
    }

    // 正确用法2:捕获异常
    try {
        func(3,5);
    } catch (const std::bad_function_call& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }

    return 0;
}
4. 性能开销分析

std::function的性能开销主要来自类型擦除的间接调用,相比直接调用函数指针,有以下开销:

  1. 虚函数调用 :通过基类指针调用派生类的invoke方法,有一次虚函数跳转;
  2. 内存分配:默认实现中,小的可调用对象(如 lambda)会被优化为栈存储,大对象则需堆分配;
  3. 拷贝开销std::function的拷贝会拷贝底层可调用对象,若对象较大,开销明显。
5. 优化技巧
  • 避免不必要的拷贝 :用std::move转移std::function的所有权,减少拷贝;
  • 优先使用无捕获 lambda:无捕获 lambda 可隐式转换为函数指针,性能接近直接调用;
  • 性能敏感场景用函数模板 :编译期确定类型,无运行时开销,仅在需要运行时多态时用std::function

三、基于std::function构建核心组件

1. 实战 1:事件驱动架构 ------ 实现 muduo 风格的事件回调

在 Linux 网络开发中,事件驱动架构(如 Reactor 模式)的核心是事件回调 ------ 当有可读 / 可写事件发生时,调用注册的回调函数。std::function是实现回调管理的最佳选择。

示例:实现简单的事件循环(EventLoop)

cpp 复制代码
#include <iostream>
#include <functional>
#include <vector>
#include <memory>
#include <unistd.h>

// 事件类型枚举
enum class EventType {
    READ,
    WRITE
};

// 事件回调类型:参数为事件类型
using EventCallback = std::function<void(EventType)>;

// 文件描述符事件类
class FdEvent {
public:
    FdEvent(int fd, EventCallback cb) : fd_(fd), callback_(std::move(cb)) {}

    // 触发事件
    void handle_event(EventType type) {
        if (callback_) {
            callback_(type);
        }
    }

    int fd() const { return fd_; }

private:
    int fd_; // 关联的文件描述符
    EventCallback callback_; // 事件回调函数
};

// 事件循环类
class EventLoop {
public:
    // 注册事件
    void add_event(std::shared_ptr<FdEvent> event) {
        events_.push_back(std::move(event));
    }

    // 运行事件循环(模拟)
    void run() {
        std::cout << "EventLoop running..." << std::endl;
        // 模拟触发第一个事件的读事件
        if (!events_.empty()) {
            events_[0]->handle_event(EventType::READ);
        }
    }

private:
    std::vector<std::shared_ptr<FdEvent>> events_; // 事件列表
};

int main() {
    EventLoop loop;

    // 注册socket读事件回调(模拟)
    int sock_fd = 1; // 模拟socket fd
    auto read_cb = [](EventType type) {
        if (type == EventType::READ) {
            std::cout << "Socket readable, handle data..." << std::endl;
        }
    };
    loop.add_event(std::make_shared<FdEvent>(sock_fd, read_cb));

    // 运行事件循环
    loop.run();

    return 0;
}

亮点

  1. 类型无关:回调函数可以是任意可调用对象,无需关心具体类型;
  2. 灵活扩展:新增事件类型或回调逻辑,无需修改 EventLoop 核心代码;
  3. 内存安全 :用std::shared_ptr管理 FdEvent,避免悬空指针。
2. 实战 2:线程池任务队列 ------ 包装任意任务

Linux 下的线程池通常用std::queue存储任务,std::function<void()>是任务的最佳包装类型,能兼容任意无参任务。

示例:实现简单的线程池

cpp 复制代码
#include <iostream>
#include <functional>
#include <queue>
#include <vector>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>

// 任务类型:无参可调用对象
using Task = std::function<void()>;

class ThreadPool {
public:
    explicit ThreadPool(size_t thread_num) : running_(true) {
        // 创建工作线程
        for (size_t i = 0; i < thread_num; ++i) {
            threads_.emplace_back([this]() {
                while (running_) {
                    Task task;
                    // 加锁取任务
                    {
                        std::unique_lock<std::mutex> lock(mutex_);
                        cond_.wait(lock, [this]() {
                            return !running_ || !tasks_.empty();
                        });
                        if (!running_ && tasks_.empty()) {
                            return;
                        }
                        task = std::move(tasks_.front());
                        tasks_.pop();
                    }
                    // 执行任务
                    task();
                }
            });
        }
    }

    // 销毁线程池
    ~ThreadPool() {
        running_ = false;
        cond_.notify_all(); // 唤醒所有线程
        for (auto& t : threads_) {
            t.join();
        }
    }

    // 添加任务
    template <typename Func, typename... Args>
    void add_task(Func&& func, Args&&... args) {
        // 包装任务为Task类型
        Task task = std::bind(std::forward<Func>(func), std::forward<Args>(args)...);
        {
            std::lock_guard<std::mutex> lock(mutex_);
            tasks_.push(std::move(task));
        }
        cond_.notify_one(); // 唤醒一个线程
    }

private:
    std::vector<std::thread> threads_; // 工作线程列表
    std::queue<Task> tasks_; // 任务队列
    std::mutex mutex_; // 互斥锁
    std::condition_variable cond_; // 条件变量
    std::atomic<bool> running_; // 线程池运行状态
};

// 测试任务
void print_num(int num) {
    std::cout << "Thread " << std::this_thread::get_id() << " print: " << num << std::endl;
}

int main() {
    ThreadPool pool(4); // 4个工作线程

    // 添加10个任务
    for (int i = 0; i < 10; ++i) {
        pool.add_task(print_num, i);
    }

    // 等待任务执行完成
    sleep(1);

    return 0;
}

编译运行(Linux)

cpp 复制代码
g++ -std=c++11 thread_pool.cpp -o thread_pool -lpthread
./thread_pool

扩展

  1. 支持带返回值的任务:用std::packaged_taskstd::future包装任务,获取执行结果;
  2. 任务优先级:用std::priority_queue替代std::queue,实现优先级任务调度;
  3. 优雅关闭:处理剩余任务后再销毁线程池,避免任务丢失。
3. 实战 3:策略模式 ------ 动态切换算法

std::function可用于实现策略模式,动态切换不同的算法逻辑,无需修改核心代码。

示例:排序算法策略切换

cpp 复制代码
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>

// 排序策略类型:参数为vector<int>&
using SortStrategy = std::function<void(std::vector<int>&)>;

// 排序类
class Sorter {
public:
    explicit Sorter(SortStrategy strategy) : strategy_(std::move(strategy)) {}

    // 切换排序策略
    void set_strategy(SortStrategy strategy) {
        strategy_ = std::move(strategy);
    }

    // 执行排序
    void sort(std::vector<int>& vec) {
        if (strategy_) {
            strategy_(vec);
        }
    }

private:
    SortStrategy strategy_;
};

// 冒泡排序
void bubble_sort(std::vector<int>& vec) {
    size_t n = vec.size();
    for (size_t i = 0; i < n-1; ++i) {
        for (size_t j = 0; j < n-i-1; ++j) {
            if (vec[j] > vec[j+1]) {
                std::swap(vec[j], vec[j+1]);
            }
        }
    }
}

int main() {
    std::vector<int> vec = {3,1,4,1,5,9};

    // 初始策略:冒泡排序
    Sorter sorter(bubble_sort);
    sorter.sort(vec);
    for (int num : vec) { std::cout << num << " "; } // 输出1 1 3 4 5 9
    std::cout << std::endl;

    // 切换策略:标准库排序
    sorter.set_strategy([](std::vector<int>& v) {
        std::sort(v.begin(), v.end());
    });
    vec = {3,1,4,1,5,9};
    sorter.sort(vec);
    for (int num : vec) { std::cout << num << " "; } // 输出1 1 3 4 5 9
    std::cout << std::endl;

    return 0;
}

四、避坑指南

坑点 1:绑定局部对象的this指针
cpp 复制代码
void func() {
    Math math; // 局部对象
    std::function<int(int, int)> func = std::bind(&Math::mul, &math, std::placeholders::_1, std::placeholders::_2);
} // math销毁,func悬空

解决方案 :确保绑定的对象生命周期长于std::function,或用智能指针管理对象。

坑点 2:捕获悬空引用的 lambda
cpp 复制代码
std::function<int()> get_func() {
    int num = 10;
    return [&num]() { return num; }; // 捕获局部变量引用,返回后悬空
}

解决方案:捕获值而非引用,或确保捕获的变量生命周期足够长。

坑点 3:滥用std::function导致性能下降

解决方案 :性能敏感场景(如高频网络回调)优先用函数模板或无捕获 lambda,仅在需要运行时多态时使用std::function

坑点 4:忽略std::function的拷贝开销

解决方案 :用std::move转移std::function所有权,避免不必要的拷贝:

cpp 复制代码
std::function<void()> func = []() { /* ... */ };
queue.push(std::move(func)); // 转移所有权,无拷贝
Linux 调试技巧:排查std::function问题
  • 查看std::function是否为空 :用gdb调试时,打印func.operator bool()
  • 分析性能开销 :用perf工具采样,查看std::function调用的耗时占比;
  • 定位悬空指针 :用valgrind检测内存越界,排查因绑定无效对象导致的崩溃。

五、总结

  • 本质std::function是可调用对象的包装器,通过类型擦除实现统一管理;
  • 核心优势:兼容所有可调用对象,支持运行时多态,灵活性高;
  • 性能特点:有轻微间接调用开销,需在灵活性与性能间权衡;
  • 工业级场景:事件回调、线程池任务、策略模式、回调函数管理。
相关推荐
多多*2 小时前
图解Redis的分布式锁的历程 从单机到集群
java·开发语言·javascript·vue.js·spring·tomcat·maven
吃花椒的冰冰2 小时前
ubuntu自动检测断网重联
运维·服务器
刘哥测评技术zcwz6262 小时前
希音shein自养号测评怎么做,有哪些技术要求
运维·服务器·网络
a程序小傲2 小时前
国家电网面试被问:FactoryBean与BeanFactory的区别和动态代理生成
java·linux·服务器·spring boot·spring·面试·职场和发展
电商API&Tina2 小时前
Python请求淘宝商品评论API接口全指南||taobao评论API
java·开发语言·数据库·python·json·php
学嵌入式的小杨同学3 小时前
【嵌入式 C 语言实战】交互式栈管理系统:从功能实现到用户交互全解析
c语言·开发语言·arm开发·数据结构·c++·算法·链表
txinyu的博客3 小时前
static_cast、const_cast、dynamic_cast、reinterpret_cast
linux·c++
“αβ”3 小时前
TCP相关实验
运维·服务器·网络·c++·网络协议·tcp/ip·udp
小杍随笔3 小时前
【Rust Cargo 目录迁移到 D 盘:不改变安装路径和环境变量的终极方案】
开发语言·后端·rust