news 2026/10/20 1:47:37

C++中function的实现原理详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++中function的实现原理详解

前言


std::function是 C++11 引入的通用可调用对象包装器。它能把普通函数、函数指针、lambda、成员函数指针、重载了operator()的类对象,统统装进同一个类型里,于是容器、回调注册表、任务队列都可以用统一的方式保存和调用。它解决的问题是“类型擦除”(type erasure):调用方只关心“能按这个签名调用”,不关心底层到底是什么类型。


最常见的误解是把std::function当成“零成本的 lambda 容器”。实际上它通常会带来一层间接调用,很多实现还会为较大的可调用对象在堆上分配内存。另一个误解是认为它可以装下任何可调用对象——标准要求目标类型满足 CopyConstructible,捕获了std::unique_ptr的 lambda 不能直接存进去;C++23 才提供std::move_only_function来解决这个问题。


本文先说明类型擦除的必要性,再给出一个基于“概念接口 + 模板模型”的迷你实现,最后讨论真实标准库实现中的小对象优化、空调用异常和复制语义。本文的迷你实现用于理解原理,不追求与标准库完全等价;具体实现的细节以标准草案与各标准库文档为准。


一、为什么需要类型擦除


假设我们要实现一个按钮回调。用户可能传函数指针,也可能传捕获了上下文的 lambda:


// 适用于 C++17
void on_click(void (*cb)(int));

这个接口只能接受无捕获的 lambda,因为无捕获 lambda 可以转换成函数指针;一旦捕获了变量,它的类型就是编译器生成的独一无二的闭包类型,无法转成函数指针。模板可以解决一半问题:


// 适用于 C++17
template <class F>
void on_click(F cb);

但模板会把F编码进on_click的类型,导致每个不同的 lambda 都实例化一份on_click,而且无法把回调存进同一个std::vector里——因为元素类型必须相同。


类型擦除的思路是:定义一个统一的抽象接口,把“按签名调用”变成虚函数;再用一个模板派生类把具体的可调用对象包起来,并通过基类指针持有它。这样外部只看到统一的包装类型,内部通过虚调用分派到真实对象。std::function的核心就是这个结构,业界常称为“概念/模型”(concept/model)模式或多态包装。


二、迷你实现的骨架


整个实现只有三个组成部分:




组件作用



Concept抽象基类,声明invoke与clone两个纯虚函数

Model<F>模板派生类,持有具体可调用对象F,实现两个虚函数

Function外部包装类,用std::unique_ptr<Concept>持有模型,提供构造、拷贝、调用



clone()是为了支持拷贝语义:std::function可拷贝,但基类指针不能直接拷贝,否则两个包装对象会共享同一个模型并重复释放。让模型自己克隆一份,拷贝构造就变成了“调用clone得到新的模型”。


invoke的参数写成Args&&...,配合std::forward保留调用方的值类别。包装器内部不需要知道参数原来是什么,只要把收到的实参原样转发给目标对象即可。


三、完整示例代码


下面的代码实现了一个简化版Function,支持函数指针、lambda 和函数对象,支持拷贝、移动、判空与空调用异常。第一行标注了适用标准。


// 适用于 C++17
#include <cstddef>
#include <functional>
#include <iostream>
#include <memory>
#include <type_traits>
#include <utility>

template <class Signature>
class Function;

template <class R, class... Args>
class Function<R(Args...)> {
public:
Function() noexcept = default;
Function(std::nullptr_t) noexcept {}

template <class F,
class = std::enable_if_t<
!std::is_same<std::decay_t<F>, Function>::value>>
Function(F&& f)
: impl_(std::make_unique<Model<std::decay_t<F>>>(std::forward<F>(f))) {}

Function(const Function& other)
: impl_(other.impl_ ? other.impl_->clone() : nullptr) {}

Function(Function&& other) noexcept = default;

Function& operator=(const Function& other) {
Function tmp(other);
swap(tmp);
return *this;
}

Function& operator=(Function&& other) noexcept = default;

R operator()(Args... args) const {
if (!impl_) {
throw std::bad_function_call();
}
return impl_->invoke(std::forward<Args>(args)...);
}

explicit operator bool() const noexcept { return impl_ != nullptr; }

void swap(Function& other) noexcept { impl_.swap(other.impl_); }

private:
struct Concept {
virtual ~Concept() = default;
virtual R invoke(Args&&... args) = 0;
virtual std::unique_ptr<Concept> clone() const = 0;
};

template <class F>
struct Model final : Concept {
explicit Model(F f) : fn(std::move(f)) {}

R invoke(Args&&... args) override {
if constexpr (std::is_void<R>::value) {
fn(std::forward<Args>(args)...);
} else {
return fn(std::forward<Args>(args)...);
}
}

std::unique_ptr<Concept> clone() const override {
return std::make_unique<Model>(fn);
}

F fn;
};

std::unique_ptr<Concept> impl_;
};

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

int main() {
Function<int(int, int)> f = add;
std::cout << f(2, 3) << '\n';

int base = 10;
f = [base](int a, int b) { return base + a * b; };
std::cout << f(2, 3) << '\n';

Function<int(int, int)> g = f; // 拷贝:调用 clone
std::cout << g(4, 5) << '\n';

Function<void(int)> log = [](int v) {
std::cout << "value=" << v << '\n';
};
log(7);

Function<int(int, int)> empty;
if (!empty) {
std::cout << "empty\n";
}
try {
empty(1, 2);
} catch (const std::bad_function_call&) {
std::cout << "caught bad_function_call\n";
}
return 0;
}

按标准语义推演:f = add时,模板构造函数把函数指针类型int(*)(int,int)作为F,Model保存该指针;f(2, 3)通过虚函数invoke转发,返回 5。随后赋值一个捕获base的 lambda,包装器内部的旧模型被销毁,新模型持有闭包副本。g = f触发拷贝构造,clone()生成一个持有同一闭包副本的新模型,二者互不影响。空的Function调用operator()时抛出std::bad_function_call。


这段代码有两点刻意的简化。第一,Model::clone要求F可拷贝,因此捕获了unique_ptr的 lambda 无法存入;这与标准std::function的约束一致。第二,没有实现小对象优化,所有目标都放在堆上;标准库实现通常会把小到能塞进内部缓冲区的对象直接存进去。


四、真实实现中的几个关键细节


小对象优化(small buffer optimization, SBO)。标准并没有规定std::function必须如何存储目标,但主流实现会在包装器内部预留一小块缓冲区,目标对象足够小且满足对齐要求时就地构造,否则才堆分配。这解释了为什么std::function的大小通常远大于一个指针,也解释了为什么“存一个小 lambda 不分配内存”只是实现层面的常见行为,而不是标准保证。


拷贝与移动。std::function的拷贝构造会复制目标对象;移动构造之后,源对象处于有效但未指定的状态,标准只要求它仍可安全析构和重新赋值,不要求它一定为空。因此不要假设“移动后源对象必然为空”,需要判空时重新赋值或检查operator bool(但移动后的值本身未指定)。


空调用。默认构造或由nullptr构造的std::function不含目标,调用operator()会抛出std::bad_function_call(定义在<functional>)。它不会返回默认值,也不会静默失败。


类型查询。target_type()返回目标的std::type_info,target<T>()在目标类型正好是T时返回指针,否则返回nullptr。这两者适合调试和少量需要知道底层类型的场景,不应作为常规分派手段。


只可移动的可调用对象。C++23 增加了std::move_only_function(需要 C++23 及以上的标准库支持),它放宽了 CopyConstructible 要求,适合包装捕获unique_ptr的 lambda 或只能移动的函数对象。在 C++17 下如果确实需要,可以自己写一个只移动的包装器,或改用std::unique_ptr加自定义接口。


常见坑点



  1. ❌ 认为std::function保存的是 lambda 的引用。


✅ 它保存的是目标的副本;lambda 捕获的变量在构造时被复制,之后修改原变量不会影响包装器内的副本。



  1. ❌ 把捕获了std::unique_ptr的 lambda 赋给std::function。


✅ 标准要求目标可拷贝;改用std::shared_ptr捕获,或使用 C++23 的std::move_only_function。



  1. ❌ 调用空的std::function并期望得到默认值。


✅ 调用前用if (f)判断,或捕获std::bad_function_call。



  1. ❌ 在热路径上反复构造std::function,以为它和直接调用一样快。


✅ 类型擦除会引入间接调用;把包装器构造提到循环外,性能敏感处直接用模板或函数指针。



  1. ❌ 认为target<T>()可以用来判断“能否按某签名调用”。


✅ 它只比较精确类型;签名兼容性应通过std::is_invocable等 trait 在编译期判断。



  1. ❌ 移动std::function后继续读取源对象保存的目标。


✅ 移动后源对象处于有效但未指定状态,只能销毁或重新赋值。



  1. ❌ 在回调里保存std::function的引用参数,回调返回后继续使用。


✅ 需要长期保存时按值捕获或复制一份;引用参数只在调用期间有效。


总结




要点说明



核心机制类型擦除:抽象基类 + 模板模型 + 智能指针持有

调用方式通过虚函数间接调用,参数用完美转发保持值类别

拷贝语义模型实现clone,避免多个包装器共享同一目标

存储策略标准不规定;常见实现有小对象优化,也可能堆分配

约束目标必须可拷贝;空调用抛std::bad_function_call

现代替代C++23std::move_only_function支持只移动目标



理解std::function的实现,最重要的收获不是背下这段代码,而是认清它的成本边界:一次类型擦除换来接口统一,代价是一层间接调用和潜在的内存分配。凡是能接受模板的场合,模板往往更直接;只有确实需要“运行期统一类型”时,std::function才是合适的选择。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 1:47:37

STM32+云平台+Qt:从零搭建智能家居控制系统全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 1:47:27

3分钟跑通的TwitchDropsMiner自动挖矿

3分钟跑通的TwitchDropsMiner自动挖矿 【免费下载链接】TwitchDropsMiner An app that allows you to AFK mine timed Twitch drops, with automatic drop claiming and channel switching. 项目地址: https://gitcode.com/GitHub_Trending/tw/TwitchDropsMiner TwitchD…

作者头像 李华
网站建设 2026/10/20 1:46:52

TwitchDropsMiner 掉宝自动挖矿教程:配置一次就领满全部奖励

TwitchDropsMiner 掉宝自动挖矿教程&#xff1a;配置一次就领满全部奖励 【免费下载链接】TwitchDropsMiner An app that allows you to AFK mine timed Twitch drops, with automatic drop claiming and channel switching. 项目地址: https://gitcode.com/GitHub_Trending/…

作者头像 李华
网站建设 2026/10/20 1:46:46

Vorssaint:一个 macOS 菜单栏工具,70 多个功能按需装载

Vorssaint&#xff1a;一个 macOS 菜单栏工具&#xff0c;70 多个功能按需装载 【免费下载链接】vorssaint-utils Free and open-source macOS menu bar toolkit. 项目地址: https://gitcode.com/GitHub_Trending/vo/vorssaint-utils Mac 上工具类的真正成本&#xff0c…

作者头像 李华
网站建设 2026/10/20 1:46:45

wiliwili 安装与快速上手指南:Switch 上用手柄刷 B 站完整教程

wiliwili 安装与快速上手指南&#xff1a;Switch 上用手柄刷 B 站完整教程 【免费下载链接】wiliwili 第三方B站客户端&#xff0c;目前可以运行在PC全平台、PSVita、PS4 、Xbox 和 Nintendo Switch上 项目地址: https://gitcode.com/GitHub_Trending/wi/wiliwili wiliw…

作者头像 李华
网站建设 2026/10/20 1:44:41

Agent技能层设计:构建可测试、可审计、可降级的确定性执行单元

1. 项目概述&#xff1a;从“agent-skills”这个词组看懂当前技术落地的真实切口最近在多个技术社区、开源仓库和工程团队的内部分享里&#xff0c;频繁看到“agent-skills”这个组合词——它既不是标准术语&#xff0c;也不是某个框架的官方命名&#xff0c;而更像是一群一线开…

作者头像 李华