c++11:fuction包装器

深入理解 C++11 std::function

一、引言

在 C++11 之前,想把"函数"当作参数传递是一件很别扭的事:要么用函数指针(无法携带状态),要么用抽象基类 + 虚函数(代码冗长)。C++11 引入了 std::function,它是标准库对任意可调用对象的统一类型擦除封装,从此回调、事件、策略模式都变得优雅起来。


二、为什么需要 std::function

2.1 函数指针的局限

cpp 复制代码
int add(int a, int b) { return a + b; }

// 函数指针只能指向普通函数,无法携带状态
int (*fp)(int, int) = add;

函数指针无法保存 lambda 捕获的变量,也无法保存带状态的仿函数。

2.2 模板的局限

cpp 复制代码
template <typename F>
void run(F f) { f(); }

struct Handler { void onEvent(); };

模板虽然高效,但每个不同的 F 都会实例化一份新代码,而且无法把不同类型的可调用对象存进同一个容器:

cpp 复制代码
std::vector<???> handlers; // 这里的类型怎么写?

std::function 正是为了解决这两个问题:统一的类型 + 统一的接口。


三、基本用法

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

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

int main() {
    // 模板参数是签名:返回值(参数列表)
    std::function<int(int, int)> f = add;
    std::cout << f(1, 2) << '\n';  // 3

    // 空 function 的判断
    std::function<void()> g;
    if (!g) std::cout << "g is empty\n";
}

注意签名是 int(int, int),不包含函数名。


四、可以包装哪些可调用对象

std::function 几乎可以包装一切能"用 () 调用"的东西。

4.1 普通函数与函数指针

cpp 复制代码
int add(int a, int b) { return a + b; }
std::function<int(int,int)> f1 = add;
std::function<int(int,int)> f2 = &add;   // 等价

4.2 Lambda 表达式(最常用)

cpp 复制代码
int base = 100;
std::function<int(int)> f = [base](int x) { return x + base; };
std::cout << f(5);   // 105

捕获列表中的状态被"打包"进了 std::function 内部。

4.3 仿函数(Functor)

cpp 复制代码
struct Multiply {
    int factor;
    int operator()(int x) const { return x * factor; }
};

std::function<int(int)> f = Multiply{3};
std::cout << f(7);   // 21

4.4 成员函数指针

cpp 复制代码
struct Widget {
    int value = 42;
    int get() const { return value; }
};

Widget w;
// 注意:非静态成员函数第一个参数是对象
std::function<int(const Widget&)> f = &Widget::get;
std::cout << f(w);   // 42

4.5 std::bind 的结果

cpp 复制代码
#include <functional>

int add(int a, int b) { return a + b; }
std::function<int(int)> add10 = std::bind(add, 10, std::placeholders::_1);
std::cout << add10(5);   // 15

五、与 std::bind 配合

std::bind 用来"绑定部分参数",但现代 C++ 更推荐用 lambda 代替 bind,因为 lambda 更易读、更容易被内联优化。

cpp 复制代码
struct Server {
    void onMessage(int id, const std::string& msg);
};

Server srv;

// bind 写法(参数顺序容易搞混)
auto cb1 = std::bind(&Server::onMessage, &srv, std::placeholders::_2,
                     std::placeholders::_1);

// lambda 写法(推荐,清晰)
auto cb2 = [&srv](int id, const std::string& msg) {
    srv.onMessage(id, msg);
};

六、典型应用场景

6.1 回调注册表

cpp 复制代码
class EventBus {
    std::vector<std::function<void(const std::string&)>> listeners_;
public:
    void subscribe(std::function<void(const std::string&)> cb) {
        listeners_.push_back(std::move(cb));
    }
    void emit(const std::string& e) {
        for (auto& cb : listeners_) cb(e);
    }
};

不同类型的 lambda 可以存进同一个 vector,这正是类型擦除的威力。

6.2 策略模式

cpp 复制代码
class Sorter {
public:
    using Compare = std::function<bool(int, int)>;
    void sort(std::vector<int>& v, Compare cmp) {
        std::sort(v.begin(), v.end(), cmp);
    }
};

Sorter s;
std::vector<int> v{3, 1, 2};
s.sort(v, [](int a, int b) { return a > b; });  // 降序

七、实现原理:类型擦除

std::function 的核心技术叫类型擦除(Type Erasure)。思路是:

  1. 定义一个抽象基类,声明 operator() 对应的虚函数;
  2. 用一个模板派生类保存具体的可调用对象;
  3. std::function 持有一个指向基类的指针。

一个极简的实现骨架:

cpp 复制代码
template <typename R, typename... Args>
class MyFunction {
    struct Base {
        virtual ~Base() = default;
        virtual R invoke(Args... args) = 0;
    };

    template <typename F>
    struct Impl : Base {
        F f;
        explicit Impl(F fn) : f(std::move(fn)) {}
        R invoke(Args... args) override { return f(std::forward<Args>(args)...); }
    };

    std::unique_ptr<Base> ptr_;

public:
    template <typename F>
    MyFunction(F f) : ptr_(new Impl<F>(std::move(f))) {}

    R operator()(Args... args) { return ptr_->invoke(std::forward<Args>(args)...); }
};

真实的 std::function 在此基础上做了两点优化:

  • 小对象优化(SBO):当可调用对象足够小(libstdc++ 中约 16 字节)且可无异常移动时,直接存放在对象内部的缓冲区,避免堆分配。
  • 函数指针表:部分实现用函数指针代替虚表,进一步减少开销。

八、性能考量

std::function 不是零成本的,主要体现在:

开销来源 说明
类型擦除 编译器无法内联,无法做跨调用优化
间接调用 虚函数或函数指针跳转
潜在堆分配 捕获内容超过 SBO 缓冲区时
拷贝成本 std::function 本身可拷贝,每次拷贝都要复制内部状态

8.1 性能对比示例

cpp 复制代码
// 模板:可内联,无间接调用,最快
template <typename F>
void call_template(F f) { for (int i = 0; i < N; ++i) f(i); }

// std::function:有间接调用开销
void call_function(std::function<void(int)> f) {
    for (int i = 0; i < N; ++i) f(i);
}

在热路径中,模板版本往往比 std::function 快数倍。

8.2 传递方式

cpp 复制代码
void f1(std::function<void()> cb);        // 值传递,可能拷贝
void f2(const std::function<void()>& cb); // 只读场景推荐
void f3(std::function<void()>&& cb);      // 需要持有所有权时

九、常见陷阱

9.1 悬垂引用(最危险)

cpp 复制代码
std::function<int()> make_bad() {
    int local = 42;
    return [&local]() { return local; };  // 危险!local 已销毁
}

std::function<int()> make_good() {
    int local = 42;
    return [local]() { return local; };   // 按值捕获,安全
}

9.2 空 function 调用

cpp 复制代码
std::function<void()> f;
f();  // 抛出 std::bad_function_call

使用前务必检查:

cpp 复制代码
if (f) f();

9.3 重载函数无法直接赋值

cpp 复制代码
void foo(int);
void foo(double);

std::function<void(int)> f = foo;  // 编译错误:二义性
std::function<void(int)> f = static_cast<void(*)(int)>(foo);  // OK
// 或者用 lambda 包一层
std::function<void(int)> f = [](int x) { foo(x); };

9.4 可调用对象必须可拷贝

cpp 复制代码
std::function<void()> f = [p = std::unique_ptr<int>(new int(1))]() {};
// C++11/14 编译错误:unique_ptr 不可拷贝

// C++14 起可用 std::ref 绕过(需自己保证生命周期)
// C++23 起应改用 std::move_only_function

9.5 生命周期问题:捕获 this

cpp 复制代码
class Foo {
    int x_ = 0;
public:
    std::function<void()> getCallback() {
        return [this]() { ++x_; };  // 若 Foo 先析构则悬垂
    }
};

十、最佳实践

  1. 能用模板就用模板 :只在需要"统一类型"或"运行时存储"时才用 std::function。
  2. 优先按 const& 传参,除非需要接管所有权。
  3. Lambda 优于 std::bind:可读性更好,优化空间更大。
  4. 注意捕获方式:跨作用域保存的回调一律按值捕获,或明确保证生命周期。
  5. 热路径慎用:高频调用的回调(如每帧渲染)考虑模板或函数指针。
  6. C++23 之后的新选择 :
    • std::move_only_function:支持只移动的可调用对象(如捕获 unique_ptr 的 lambda)。
    • std::function_ref(C++26):非拥有引用,零开销,适合纯参数传递。

十一、总结

std::function 是 C++11 中最实用的工具之一,它用类型擦除把各种可调用对象统一成一种类型,让回调、事件、策略模式变得简洁优雅。

但它的代价是运行时的间接调用与潜在的堆分配。把它当作"需要运行时多态的可调用对象容器"来用,而不是"万能函数参数"------这是用好它的关键。

一句话记住:

需要统一类型 → 用 std::function;

追求极致性能 → 用模板 / auto。


相关推荐
kimnoic1 小时前
Python 爬虫的原理
开发语言·python
Fluxproxy1 小时前
Python爬虫异步提速终极方案|线程池ThreadPoolExecutor实战,告别单线程慢速爬取
开发语言·爬虫·python
淡海水1 小时前
02-07-原理篇-GC暂停时间的本质
开发语言·unity·c#·gc·暂停时间
霍霍的袁2 小时前
【C++】哈希表实现指南:从原理到开放定址法、链地址法的实现
c++·学习·哈希算法·散列表·visual studio
北i3 小时前
Dubbo 默认超时重试:把下游打雪崩的隐形放大器
开发语言·qt·dubbo
10mAh3 小时前
【Java】ArrayList 遍历删除为什么报错?三种写法与漏删复现
java·开发语言·windows
WL学习笔记3 小时前
类和对象(下)
c++·类和对象
wewowilling3 小时前
AI大模型开发--01Python基础(无废话)
开发语言·python·函数
小羊没烦恼!3 小时前
OS X 上使用.NET开发应用程序
开发语言·前端·数据库·人工智能·c#·策略模式