Skip to main content

Chapter 35: Std Function

std::function 解决的是“把不同具体类型的可调用对象放进同一种运行时容器或接口”这个问题。函数指针、捕获 lambda、带状态函数对象、std::bind 表达式都能被调用,但它们的具体类型不同;需要把它们存入同一个字段、同一个数组或同一个回调槽位时,调用方需要一个统一对象。

本章用一个事件分发器作为贯穿材料:系统中有多个处理函数,调用点只关心签名 void(const Event&),注册点可能交给它函数指针、小捕获 lambda、大捕获 lambda 或函数对象。读完本章后,应能定位 std::function 内部存了什么,判断一次封装是否会分配,解释调用开销来自哪里,并在模板参数、函数指针、lambda 和 std::function 之间做工程选择。

std::function 的标准语义在 C++11 引入,属于 <functional> 中的多态函数包装器;它可以存储、复制并调用满足指定调用签名的可调用目标。标准工作草案把它描述为 generalized function pointer 的 polymorphic wrapper,并规定空对象调用会抛出 std::bad_function_call;可对照 C++ working draft 的 std::function 条款cppreference 的 std::function 页面。这些资料支撑标准层语义,本章正文把语义转成源码形状和使用判断。

本章的核心结论是:std::function<R(Args...)> 固定调用签名,擦除目标类型,保存一份可复制的目标对象或引用包装,并通过一层运行时间接入口完成调用。它适合存储回调和跨模块 API 边界;它对热路径、频繁构造、频繁复制和大捕获对象有明确成本。判断顺序应先看是否需要运行时存储多种 callable,再看目标对象生命周期,再看复制语义,再看分配和调用成本。

35.1 type erasure 与 callable wrapper

type erasure 的工作定义是:对象在构造时接收一个具体类型,之后对外只暴露固定接口,把具体类型藏在统一包装对象内部。std::function<void(const Event&)> 的固定接口是 operator()(const Event&),内部目标可以是函数指针、捕获 lambda 或类对象。调用方看见的是同一个 wrapper 类型,具体目标类型只在构造、复制、销毁和调用转发时由内部管理逻辑使用。

贯穿材料从一个事件分发器开始。注册阶段允许不同 callable 进入同一个容器;分发阶段只按统一签名调用。

#include <functional>
#include <iostream>
#include <string>
#include <vector>

struct Event {
std::string name;
int value{};
};

class Dispatcher {
public:
using Handler = std::function<void(const Event&)>;

void add(Handler handler) {
handlers_.push_back(std::move(handler));
}

void emit(const Event& event) const {
for (const auto& handler : handlers_) {
handler(event);
}
}

private:
std::vector<Handler> handlers_;
};

void print_event(const Event& event) {
std::cout << event.name << ':' << event.value << '\n';
}

int main() {
Dispatcher dispatcher;
int threshold = 10;

dispatcher.add(print_event);
dispatcher.add([threshold](const Event& event) {
if (event.value >= threshold) {
std::cout << "accepted\n";
}
});

dispatcher.emit(Event{"load", 12});
}

这段代码要证明的是:Dispatcher 的存储类型只出现一次,注册点仍能接收两种不同 callable。print_event 在类型层面接近函数指针;捕获 lambda 有编译器生成的闭包类型,且闭包对象中保存 threshold 的副本。二者都能被转成 std::function<void(const Event&)>,因为它们都能以 const Event& 作为参数完成一次 void 返回调用。

std::function<R(Args...)>R(Args...) 是调用签名。它约束了进入包装器的目标:目标对象以 Args... 调用后,返回值必须能用于 R。对 std::function<void(const Event&)> 来说,目标对象的调用结果可以被丢弃;对 std::function<int(std::string_view)> 来说,目标调用结果必须能转成 int。签名固定后,wrapper 的类型稳定,容器才能保存它。

常见实现会把 std::function 拆成三类状态:目标对象存储区、调用入口、管理入口。目标对象存储区保存具体 callable;调用入口知道如何把 Args... 转交给目标对象;管理入口处理拷贝、移动、析构和类型查询。不同标准库实现细节不同,但这个形状能解释 std::function 的主要行为。

图中的关键路径是 调用方 → wrapper → invoker → 具体目标。调用方不需要知道 E 的真实类型,但 wrapper 内部必须保存足够信息,让 D 在运行时找到正确调用方式。manager 负责对象生命周期,所以 std::function 的复制语义会触发目标对象复制;目标不可复制时,普通 std::function 无法表达这个所有权模型。

std::function 是一个没有目标对象的 wrapper。默认构造、从 nullptr 构造、赋值为 nullptr 都会得到空状态。operator bool() 用来检查目标是否存在;对空对象执行 operator() 会抛出 std::bad_function_call。这条规则把“是否注册了回调”变成运行时状态,事件系统在调用前应固定检查策略。

std::function<void(const Event&)> handler;

if (handler) {
handler(Event{"ready", 1});
}

这段检查说明:std::function 的值状态和它的类型状态分离。类型 std::function<void(const Event&)> 只说明“它能表示这种签名的调用”;handler 当前是否真的有目标,需要运行时判断。工程代码中,必填回调可以在构造函数处强制传入并校验;可选回调可以保留空状态,但调用点应显式处理。

target_type()target<T>() 提供有限的目标访问能力。它们适合调试、桥接旧接口或确认某个注册目标是否为指定类型。常规业务路径中频繁向下探测目标类型,会抵消类型擦除带来的接口收益。使用 std::function 的主要收益来自统一调用表面,而非把 erased type 再恢复成具体类型。

本节建立的判断是:std::function 的抽象层级高于具体 callable,低于完整对象协议。它只抽象一次调用,不抽象“暂停、取消、比较、序列化、优先级”等额外行为。事件系统需要的如果只是统一回调槽位,std::function 合适;如果回调还需要身份、注销、优先级和统计字段,应把这些信息放在外层 handler record 中,由 std::function 只负责可调用目标。

35.2 small object optimization 与 heap allocation

small object optimizationstd::function 语境中指小目标对象内联存入 wrapper 自身的缓冲区,从而减少动态分配。标准把小对象免动态分配写成推荐实践,常见标准库也会采用类似策略;具体内联容量、对齐条件和启用条件属于实现细节。工程判断应把它当作常见优化机会,而非跨实现保证。

同一个 Handler 可以保存三类目标:函数指针、只保存少量整数的小 lambda、保存大对象的大 lambda。它们的调用签名一致,但对象大小和复制成本不同。

#include <array>
#include <functional>
#include <string>

struct Event {
std::string name;
int value{};
};

void print_event(const Event&);

void register_handlers() {
int threshold = 10;
std::array<int, 128> table{};

std::function<void(const Event&)> a = print_event;

std::function<void(const Event&)> b = [threshold](const Event& event) {
if (event.value >= threshold) {
print_event(event);
}
};

std::function<void(const Event&)> c = [table](const Event& event) {
if (event.value < static_cast<int>(table.size())) {
print_event(event);
}
};
}

这段代码要证明的是:abc 对外类型相同,内部目标对象的体积差异很大。a 的目标通常只是函数地址;b 的闭包保存一个 intc 的闭包保存整个 std::array<int, 128>std::function 必须为这些目标提供统一生命周期管理,所以它的存储策略会直接影响构造、复制、移动和析构成本。

常见实现会在 wrapper 对象里放一块内联存储。目标对象满足大小、对齐和移动管理条件时,目标直接放入这块存储;目标过大或条件不满足时,wrapper 在堆上分配一块存储,再保存指针。源码中通常能看到类似“local storage / small buffer / manager function”的结构,但命名和细节依赖 libstdc++、libc++ 或 MSVC STL 版本。

这张图说明 std::function 的构造成本有两个层级。第一层是所有目标都需要的擦除成本:建立调用入口和管理入口。第二层是大目标或特殊目标才触发的存储成本:动态分配、指针保存和释放。小对象优化只能降低第二层成本,无法消除间接调用和 erased manager。

函数指针和 std::reference_wrapper 在标准语义上有更强的异常边界。拷贝一个保存函数指针或 reference wrapper 的 std::function 时,标准规定这些情况不抛出动态分配异常;普通函数对象则可能因为目标对象复制或内存分配而抛出异常。事件系统如果频繁复制 handler,目标对象的可复制性和复制成本会进入整体异常安全设计。

std::function 默认保存目标对象的值语义副本。把捕获 lambda 赋给 std::function 时,闭包对象被构造进 wrapper;复制 std::function 时,闭包对象也按目标类型的拷贝构造复制。捕获的是 std::shared_ptr<State> 时,复制增加引用计数;捕获的是大数组时,复制数组;捕获的是引用时,复制引用关系,底层对象生命周期仍由外部承担。

struct State {
int limit{};
};

void setup(Dispatcher& dispatcher, State& state) {
dispatcher.add([&state](const Event& event) {
if (event.value >= state.limit) {
print_event(event);
}
});
}

这段代码展示引用捕获的边界。std::function 保存的是闭包对象,闭包对象内部保存 state 的引用关系;它不延长 state 的生命周期。setup 返回后,只要 state 仍然存活,handler 调用有效;一旦 state 先销毁,handler 内部引用悬垂。这个问题属于目标对象内部状态的生命周期问题,type erasure 不会把它修复。

把大对象改成共享状态,通常比把大对象直接复制进闭包更稳定。共享状态会带来引用计数和间接访问成本,但能控制 std::function 的对象体积,使复制 handler 的成本更可预测。

#include <memory>
#include <vector>

struct LookupTable {
std::vector<int> values;
};

void setup_with_shared_table(Dispatcher& dispatcher,
std::shared_ptr<const LookupTable> table) {
dispatcher.add([table = std::move(table)](const Event& event) {
if (event.value < static_cast<int>(table->values.size())) {
print_event(event);
}
});
}

这段代码的目标是把大状态从闭包值中移到独立对象中。闭包只保存一个 std::shared_ptr,许多实现中这类闭包更容易落入小对象优化范围。工程上要继续检查两个成本:shared_ptr 复制时的引用计数更新,以及调用时多一层指针访问。这个选择适合 handler 被复制次数多、状态对象体积大、状态生命周期需要跨越注册函数的场景。

本节的判断顺序是:先估计目标闭包大小,再看 handler 是否频繁构造或复制,再看是否跨线程共享状态,最后再根据测量决定是否调整捕获形式。小对象优化能改善常见小 callable 的存储成本,但它不改变 std::function 的语义:目标仍然被复制,调用仍然经过擦除入口。

35.3 调用开销与性能边界

std::function 的调用开销主要来自间接调用、优化边界和目标对象访问。模板直接接收 lambda 时,编译器在实例化后知道目标类型,常能内联 operator()std::function 调用时,调用点只知道 erased wrapper,具体目标通过 invoker 间接进入,编译器通常难以把目标逻辑完全展开到调用点。

在事件分发器中,单次 handler(event) 的逻辑路径包含三步:检查 wrapper 中的调用入口,取出目标对象地址,把参数转交给目标对象。空对象处理还涉及 bad_function_call 路径。实际实现可以把检查和分支压缩到很短,但它仍然是一层运行时派发。对 UI 事件、任务完成回调、插件通知这类低频路径,这个成本通常被业务处理覆盖;对每个元素、每个像素、每个交易订单都调用的热循环,它会成为可观察开销。

下面的例子把同一个谓词分别放在模板参数和 std::function 参数中。代码目的在于暴露优化边界的差异,性能数字需要结合编译器、优化级别和数据规模实测。

#include <functional>
#include <vector>

template <class Predicate>
int count_template(const std::vector<int>& values, Predicate pred) {
int count = 0;
for (int value : values) {
if (pred(value)) {
++count;
}
}
return count;
}

int count_erased(const std::vector<int>& values,
const std::function<bool(int)>& pred) {
int count = 0;
for (int value : values) {
if (pred(value)) {
++count;
}
}
return count;
}

count_template 在每个 Predicate 类型上生成一个函数实例。调用点传入 lambda 时,循环体内的 pred(value) 有机会内联,捕获状态也能按具体类型优化。count_erased 只有一个函数体,参数类型固定为 std::function<bool(int)>;它能减少接口膨胀,却把谓词调用放到运行时间接入口之后。

性能边界还出现在构造和复制阶段。下面这个注册函数每次调用都构造一个临时 std::function,再放入容器。如果注册发生在初始化阶段,成本通常可以接受;如果注册和注销发生在高频数据路径中,分配、复制和释放就会成为分析对象。

void register_many(Dispatcher& dispatcher, int base) {
for (int i = 0; i < 1000; ++i) {
dispatcher.add([base, i](const Event& event) {
if (event.value == base + i) {
print_event(event);
}
});
}
}

这段代码中,每个 handler 闭包保存两个 int,通常属于小目标;但容器扩容会移动或复制 wrapper,std::function 自身也要维护每个目标的 manager。性能排查时应把“闭包是否小”与“wrapper 是否大量搬迁”分开。handlers_.reserve(n) 可以减少 vector 扩容次数;把 handler 集合稳定下来再进入热路径,也能把构造成本移出关键阶段。

调用开销的另一个边界是分支预测和目标多样性。一个容器中如果大量 handler 的目标类型不同,invoker 地址和目标对象布局也会分散;CPU 看到的是一组间接调用目标。目标数量少且重复性高时,预测较稳定;目标数量多且顺序变化时,间接调用路径更难预测。这个判断属于运行时数据形态问题,单看复杂度符号无法说明。

std::function 的异常行为也会影响性能设计。空调用会抛异常,因此热路径中不应把“尝试调用再处理异常”当成常规控制流。稳定做法是在注册阶段保证必填回调非空,或者在调用前用 operator bool() 分支处理可选回调。异常路径应表达错误或罕见状态,常态判断应使用显式状态。

返回引用的 std::function 需要额外边界。早期标准下,把未显式写返回类型的 lambda 放进 std::function<const T&()> 可能让引用绑定到临时结果;C++23 对这类场景有更严格处理。工程代码中,返回引用的 callable 应显式写 trailing return type,并确保返回对象生命周期覆盖调用结果使用期。

#include <functional>
#include <string>

const std::string& global_name() {
static const std::string name = "dispatcher";
return name;
}

std::function<const std::string&()> make_name_reader() {
return []() -> const std::string& {
return global_name();
};
}

这段代码的关键是 -> const std::string&。它把返回值类别写进 lambda 的调用运算符,减少 std::function 签名和实际返回行为之间的误配。只要涉及引用返回,判断顺序应先确认返回对象的所有权和生命周期,再确认 std::function 的签名能表达这个关系。

本节的工程结论是:std::function 的成本由一组可定位来源组成。构造成本来自目标构造和可能分配;复制成本来自目标复制和可能分配;调用成本来自间接派发和优化边界;生命周期风险来自目标内部捕获和引用返回。性能分析应先定位操作频率,再测量构造、复制、调用分别占比。

35.4 与模板参数、函数指针和 lambda 的对比

std::function 的选择应和模板参数、函数指针、lambda 放在同一组维度比较:是否需要存储、是否需要运行时替换、是否保留具体类型、是否支持状态、是否要求可复制、是否位于热路径。只用“灵活”或“慢”判断会丢失前提。

形式类型信息可保存状态运行时替换典型成本适合位置
模板参数 F调用点保留具体类型支持通过重新实例化表达代码实例化增多,调用可内联泛型算法、热循环、局部策略
函数指针固定函数签名和地址只保存函数地址支持换地址调用间接,状态需要额外参数C 接口、无状态回调、ABI 边界
lambda 具体类型编译器生成闭包类型支持变量类型固定后替换受限小对象直接调用易优化局部算法、短生命周期策略
std::function擦除目标类型,只保留签名支持可复制目标支持统一槽位替换可能分配,调用间接回调存储、事件系统、插件接口

模板参数适合调用方和被调用方在同一个编译期上下文中协作。std::sort 接收比较器时使用模板,是因为算法希望保留比较器具体类型,让比较调用进入内联和优化路径。事件分发器的 handler 集合则需要把多个具体类型放进同一个 std::vector,模板参数无法直接提供同构存储类型。

template <class Handler>
void emit_once(const Event& event, Handler handler) {
handler(event);
}

void use_template() {
int threshold = 10;
emit_once(Event{"load", 12}, [threshold](const Event& event) {
if (event.value >= threshold) {
print_event(event);
}
});
}

这段代码展示模板参数的优势:Handler 保留闭包类型,handler(event) 可按具体类型优化。它的边界也清楚:emit_once 只处理一次调用,不需要把 handler 存入长期容器。需要“注册后稍后调用”时,模板函数无法单独表达延迟存储,需要外层对象继续决定存储类型。

函数指针适合无状态目标或状态由外部参数显式传递的场景。它的对象模型简单,通常就是一个代码地址;它不携带闭包状态。需要把 threshold、表格、对象指针这类状态和调用逻辑绑在一起时,函数指针需要额外上下文参数,接口会变成 C 风格的 void* 加回调函数组合。std::function 把这组状态放进目标对象,接口更接近 C++ 对象语义。

using CCallback = void (*)(void* context, const Event& event);

struct CSlot {
void* context{};
CCallback callback{};
};

这段简化结构说明函数指针和上下文指针的典型组合。它在 ABI 稳定和 C 接口中常见,但类型安全由调用方自行维护。std::function<void(const Event&)> 把目标对象类型擦除在标准库内部,用户无需在业务代码中写 void* 转型;代价是 wrapper 需要承担构造、复制、销毁和间接调用。

lambda 具体类型适合局部使用。捕获 lambda 是一个闭包对象,可以保存状态,直接传给模板算法时效率很好。它的类型无法由源代码直接命名,且每个 lambda 表达式都有独立类型;要把多个不同 lambda 放进同一个容器,就需要 std::function、自定义类型擦除、继承多态或其他统一包装方式。

std::function 还有一个常被忽略的约束:目标必须可复制构造。保存只移动资源的 callable,例如捕获 std::unique_ptr 的 lambda,普通 std::function 无法满足复制语义。C++23 引入 std::move_only_function 用来表达只移动 callable wrapper;项目仍处于 C++11 到 C++20 范围时,可以用自定义 move-only wrapper、显式对象协议或任务队列专用类型承载这类目标。

#include <memory>

void move_only_capture() {
auto resource = std::make_unique<int>(42);

auto task = [resource = std::move(resource)](const Event& event) {
if (event.value == *resource) {
print_event(event);
}
};

// std::function<void(const Event&)> handler = std::move(task);
// 普通 std::function 要求目标可复制,捕获 unique_ptr 的闭包不满足该要求。
}

这段代码把复制语义边界暴露出来。task 的闭包拥有 std::unique_ptr,闭包可移动但不可复制;std::function 的接口承诺自身可复制,因此这类目标不满足普通 std::function 的保存条件。工程上看到这类错误时,应先判断回调集合是否真的需要复制;若只需要移动所有权,选择 move-only 设计比强行共享资源更准确。

对 API 设计来说,参数类型也应区分“立即调用”和“保存起来”。函数只在当前调用栈内调用一次 callable,优先使用模板参数或 auto&& 形式,让调用方保留具体类型;函数要把 callable 存入对象或容器,使用 std::function 能把存储语义写进接口。这个区别直接影响生命周期责任。

template <class Handler>
void visit_event_now(const Event& event, Handler&& handler) {
std::forward<Handler>(handler)(event);
}

class EventSource {
public:
void set_handler(std::function<void(const Event&)> handler) {
handler_ = std::move(handler);
}

void notify(const Event& event) const {
if (handler_) {
handler_(event);
}
}

private:
std::function<void(const Event&)> handler_;
};

visit_event_now 表达即时调用,EventSource::set_handler 表达保存。前者不需要 type erasure;后者需要一个稳定成员类型。这个接口边界是 std::function 选型中最实用的一条:只要 API 承诺保存回调,参数或成员中出现 std::function 就有清晰语义;只要 API 只消费一次 callable,保留模板类型通常更合适。

35.5 工程使用场景

std::function 最适合的场景是“回调身份由注册点决定,调用点只关心签名,并且回调需要被保存或替换”。事件系统、任务完成通知、状态机转移 hook、插件扩展点、GUI 按钮回调、异步结果处理都属于这类模型。它们的共同点是调用频率有限,接口稳定性和对象生命周期管理比极限内联更重要。

事件分发器进入工程代码后,通常不只保存裸 handler,还会保存订阅 ID、优先级、一次性调用标记或调试名称。稳定结构可以写成 Subscription,其中 std::function 只承担可调用目标。

#include <cstddef>
#include <functional>
#include <string>
#include <vector>

struct Subscription {
std::size_t id{};
int priority{};
std::string name;
std::function<void(const Event&)> handler;
};

class EventBus {
public:
void subscribe(Subscription subscription) {
subscriptions_.push_back(std::move(subscription));
}

void publish(const Event& event) const {
for (const auto& subscription : subscriptions_) {
if (subscription.handler) {
subscription.handler(event);
}
}
}

private:
std::vector<Subscription> subscriptions_;
};

这段代码给出一个更接近工程使用的边界:std::function 不负责订阅管理,Subscription 负责描述回调记录。这样做可以让回调的身份、排序和注销策略留在外层,std::function 只处理“如何调用”。职责拆开后,后续替换 handler 存储策略也更容易,例如把少数热路径 handler 改成模板静态组合,或把异步任务 handler 改成 move-only wrapper。

API 边界中使用 std::function 时,参数传递方式应表达所有权。void set_handler(std::function<void(Event)> handler) 表示函数接收一个 wrapper 副本或移动来的 wrapper,并保存它;调用方可以传 lambda,内部统一移动到成员。void set_handler(const std::function<void(Event)>& handler) 表示借用现有 wrapper 再复制到内部时,容易让接口语义变模糊。成员保存场景优先按值接收,再 std::move 到成员。

class Worker {
public:
using Done = std::function<void(int)>;

explicit Worker(Done done)
: done_(std::move(done)) {}

void finish(int code) const {
if (done_) {
done_(code);
}
}

private:
Done done_;
};

这段构造函数表达清楚的所有权路径:调用方交出一个可复制 callable wrapper,Worker 保存它,finish 在稍后调用它。按值接收允许调用方传临时 lambda、已有 std::functionstd::move 后的 wrapper。内部移动到成员可以减少一次额外复制。

策略对象也是常见使用点。一个解析器可能需要可替换的错误处理、日志处理或字段过滤策略。若策略在对象生命周期内被保存,并且不同模块会在运行时注入不同实现,std::function 能保持头文件接口小而稳定。若策略在模板库内部高频执行,模板参数能保留类型信息和内联机会。

#include <functional>
#include <string_view>

class Parser {
public:
using ErrorHandler = std::function<void(std::string_view message)>;

explicit Parser(ErrorHandler on_error)
: on_error_(std::move(on_error)) {}

void report(std::string_view message) const {
if (on_error_) {
on_error_(message);
}
}

private:
ErrorHandler on_error_;
};

这段代码中,Parser 不需要知道错误处理目标是日志系统、异常转换器还是测试桩。std::function 的封装价值来自模块边界稳定。性能判断则看 report 是否处于高频错误路径;如果错误只在失败路径出现,间接调用成本通常低于接口复杂度成本。

生命周期检查是 std::function 使用中的核心工程动作。进入 wrapper 的 callable 可以值捕获、引用捕获、捕获智能指针、保存裸指针、保存成员函数绑定结果。std::function 只管理目标对象本身,不自动管理目标对象内部引用指向的资源。保存到长期对象中的 handler,应优先使用值捕获或智能指针表达所有权;引用捕获适合调用范围明确短于被引用对象生命周期的场景。

线程边界也应显式处理。std::function 自身的复制和调用不等于目标对象线程安全。多个线程同时读同一个已构造且未修改的 std::function,目标对象内部仍可能修改共享状态;多个线程同时替换同一个 std::function 成员需要外部同步。事件系统中,订阅修改和事件发布如果并发发生,应在外层容器上设计锁、快照或消息队列策略。

最终可迁移判断顺序如下。第一步,确认 callable 是否需要被长期保存或放入同构容器;需要时进入 std::function 或其他 wrapper 设计。第二步,确认调用签名是否稳定,返回值和引用生命周期是否能被签名准确表达。第三步,确认目标是否可复制;不可复制目标进入 move-only wrapper 设计。第四步,检查捕获状态大小和生命周期;大状态用外部对象或智能指针承载。第五步,定位调用频率;热路径优先模板、具体 lambda 或函数指针,低频边界优先接口稳定性。第六步,用测量验证构造、复制和调用各自占比。

最小自检任务

阅读下面代码,判断 set_handler 保存的回调在对象生命周期、复制语义和性能边界上分别有什么结论。要求给出修改建议,使这个回调适合保存到长期存在的 EventSource 中。

#include <functional>
#include <string>
#include <vector>

struct Event {
int value{};
};

class EventSource {
public:
void set_handler(std::function<void(const Event&)> handler) {
handler_ = std::move(handler);
}

void notify(const Event& event) const {
if (handler_) {
handler_(event);
}
}

private:
std::function<void(const Event&)> handler_;
};

void configure(EventSource& source) {
std::vector<int> table(1024, 7);
int limit = 10;

source.set_handler([&table, limit](const Event& event) {
if (event.value >= limit && event.value < static_cast<int>(table.size())) {
// 使用 table[event.value]
}
});
}

答案要点

limit 是值捕获,闭包内部保存一个 int 副本,configure 返回后仍有效。table 是引用捕获,configure 返回后局部 std::vector<int> 被销毁,长期保存的 handler 内部引用悬垂;std::function 管理的是闭包对象生命周期,无法延长被引用 vector 的生命周期。

复制语义上,std::function 要求目标可复制。当前 lambda 闭包保存引用和 int,可复制;如果改成捕获 std::unique_ptr,普通 std::function 会因目标不可复制而不成立。若 handler 需要长期保存并可复制,可以使用 std::shared_ptr<const std::vector<int>> 作为捕获状态;若系统只需要移动任务,应选择 move-only wrapper 设计。

性能边界上,直接值捕获整个 std::vector 会让闭包变大,构造和复制 handler 时复制整张表。捕获 std::shared_ptr<const std::vector<int>> 能让闭包体积稳定,复制 handler 时主要更新引用计数。调用时会增加一次指针间接访问,这个成本通常适合低频事件分发;热路径应继续测量调用频率和状态访问成本。

一种稳定修改如下:

#include <memory>

void configure(EventSource& source) {
auto table = std::make_shared<const std::vector<int>>(1024, 7);
int limit = 10;

source.set_handler([table, limit](const Event& event) {
if (event.value >= limit && event.value < static_cast<int>(table->size())) {
// 使用 (*table)[event.value]
}
});
}

这个版本让 handler 自己持有表的共享所有权,EventSource 长期保存回调时不会留下悬垂引用。判断完成后还要看事件调用是否高频、handler 是否大量复制、共享所有权是否符合系统资源模型。

本章知识点总结

  • 擦除类型std::function<R(Args...)> 保留调用签名,隐藏具体 callable 类型,并通过统一 wrapper 完成运行时调用。
  • 调用签名:进入 wrapper 的目标必须能以 Args... 调用,返回结果必须能用于 R
  • 目标对象std::function 保存目标对象或引用包装,复制 wrapper 时会按目标语义复制目标。
  • 空状态:默认构造或赋值 nullptr 会得到空 wrapper,空对象调用会抛出 std::bad_function_call
  • 实现形状:常见实现包含目标存储、invoker 调用入口和 manager 管理入口,具体命名依赖标准库实现。
  • 小对象优化:小 callable 常能放入 wrapper 内部存储,动态分配减少,但内联容量和触发条件属于实现细节。
  • 分配成本:大闭包、复杂状态和频繁复制会放大构造、复制和释放成本,应根据状态体积和操作频率判断。
  • 调用成本std::function 调用经过运行时间接入口,通常弱化内联机会,热循环中应优先保留具体 callable 类型。
  • 生命周期std::function 管理闭包对象本身,不管理闭包内部引用指向的外部资源。
  • 复制约束:普通 std::function 需要可复制目标,只移动 callable 应进入 move-only wrapper 或专用任务模型。
  • 模板对比:模板参数保留具体类型和优化机会,适合即时调用和泛型算法。
  • 函数指针对比:函数指针适合无状态或显式上下文回调,状态管理需要额外接口承载。
  • API 边界:需要保存回调时按值接收 std::function 并移动到成员,能清楚表达所有权转移。
  • 判断顺序:先看是否需要运行时存储,再看签名和生命周期,再看复制语义,最后分析分配、复制和调用成本。