std::invoke与std::function:从调用原语到类型擦除的工程实践
2026/9/15 17:11:35 网站建设 项目流程

先说一个很多人容易搞混的点:刚接触这两个东西时,会以为std::invokestd::function是功能相近的“兄弟”,甚至觉得std::invokestd::function的简化版。实际上它们完全不在一个维度上。std::function是一个容器,用来存放、复制、传递任何可调用对象;而std::invoke只是一个调用原语,它定义了一套统一的规则,让你用同样的代码去调用函数指针、函数对象、成员函数指针、成员变量指针,甚至数据成员。一个是“存东西的盒子”,一个是“按按钮的动作”。

这篇内容我会从语言机制、内部实现、工程选型三个层面把这两个东西彻底拆开,再结合我在实际项目里替换、取舍、踩坑的经验,给你一份可以直接落地参考的实践笔记。不管你是刚接触C++17的新手,还是已经在工程里大量使用std::function的老手,这篇都能帮你把这两个工具用得更明白。

1. 为什么C++需要一套统一的“调用语法”

在讨论差异之前,得先把背景讲清楚。C++里的“可调用对象”是个非常宽泛的概念,它包括函数指针、重载了operator()的类对象、lambda表达式、std::bind生成的绑定对象、以及成员函数指针。这里最麻烦的就是成员函数指针:它不能用普通的func(args)语法调用,必须使用obj.*ptr(args)ptr(obj, args)这样的特殊写法。

这就导致了一个很现实的问题:当你写一个通用工具(比如线程池、定时器、回调管理器)时,如果希望它能接受“任意可调用对象”,你就得面对这些不同的调用形态。在C++11时代,最常用的办法是写一个模板,内部用std::bind把成员函数转成普通函数对象,或者干脆让调用者自己封装。这些方案能工作,但代码很啰嗦,而且边界情况容易出现编译错误。

std::invoke是C++17标准库对INVOKE这个概念的直接实现。它做的事情非常单一,就是根据传入对象的不同形态,自动选择正确的调用方式。它本身不存储任何东西,不做类型擦除,不产生额外的运行时开销,它就是一次标准的函数调用,只是把“成员函数需要用特殊语法调用”这件事给统一了。

这里有个关键认知:std::invoke编译期多态的体现,它的行为完全由编译器的重载决议决定,不存在任何虚函数或运行时跳转。你写std::invoke(f, args...)和直接写f(args...)在汇编层面几乎没有任何区别,前提是f是普通函数对象或函数指针。这意味着,在你不需要“运行时动态选择调用目标”的场景下,std::invoke是比std::function更合适的选择,它的成本低到可以忽略不计。

std::function解决的则是另一个问题:它允许你把不同类型的可调用对象(lambda、函数指针、函数对象)统一存储成同一个类型,放进容器、作为参数传递、在运行时替换。这是运行期多态类型擦除的经典应用。它内部会通过虚函数或等效机制,在调用时动态地找到真正的可调用对象并执行。付出的代价是额外的存储开销、可能的堆分配、以及间接调用带来的性能损耗。

简单类比一下。std::invoke像是你直接打电话给某人,号码是你提前写死在代码里的,每次拨打的都是这个人;std::function像是前台总机,你把不同人的分机号告诉总机,别人打电话进来时总机负责帮你转接到当前登记的那个人,而且你随时可以换分机号。总机方便,但每一次通话都要先经过一道转接。

2.std::invoke的机制拆解与使用边界

2.1INVOKE标准定义的四种形态

C++标准对INVOKE(f, args...)的定义,基本上就是把编译器对可调用对象的处理规则用库的形式固定了下来。具体来说,它支持以下几种情况:

  • f是函数指针或函数对象(重载了operator()),直接调用f(args...)
  • f是指向成员函数的指针,且传入的第一个参数是对象本身或对象的引用,等价于(args.first).*f(args.rest...)
  • f是指向成员函数的指针,且传入的第一个参数是指针(或智能指针、std::reference_wrapper),等价于(*args.first).*f(args.rest...)
  • f是指向数据成员的指针,等价于args.first.*f(*args.first).*f,此时args里不应该再有其他参数。

这个规则看起来复杂,但实际使用起来非常顺手。我在工程里最常见的用法,就是把std::invoke作为一个适配层,让上层代码不用关心“我手里拿到的到底是一个普通函数还是一个成员函数指针”。

举个例子,我写过一个简单的任务队列,它需要支持任意可调用对象。代码长这样:

#include <functional> #include <vector> class TaskQueue { public: template <typename F, typename... Args> void emplace(F&& f, Args&&... args) { tasks_.emplace_back([f = std::forward<F>(f), args = std::make_tuple(std::forward<Args>(args)...)]() mutable { std::apply([&](auto&&... a) { std::invoke(f, std::forward<decltype(a)>(a)...); }, std::move(args)); }); } void run_all() { for (auto& task : tasks_) { task(); } tasks_.clear(); } private: std::vector<std::function<void()>> tasks_; };

这里的关键在于std::invoke,它让emplace函数既能接受普通函数,也能接受成员函数指针,还能接受lambda,使用者的代码不需要做任何特殊处理。如果没有std::invoke,这段代码就得写好几个重载来区分成员函数和非成员函数,非常麻烦。

2.2std::invoke返回值的完美转发

另一个很容易忽略的细节是std::invoke的返回类型。它的定义大概是:

template <typename F, typename... Args> constexpr decltype(auto) invoke(F&& f, Args&&... args) noexcept(/* 取决于内部调用是否可能抛异常 */) { return std::__invoke(std::forward<F>(f), std::forward<Args>(args)...); }

注意这个decltype(auto),它可以保证返回值类型被完整保留。如果被调用的函数返回一个引用,std::invoke也会返回一个引用;如果返回值是const类型,也会原样保留。这一点在泛型代码里非常重要,因为如果你用auto接收返回值,auto会丢弃引用性,导致不必要的拷贝,甚至在某些情况下产生悬垂引用。

我自己曾经在一个属性访问器框架里踩过这个坑。当时用包装了类成员变量指针的对象实现了类似“读写属性的通用接口”,一开始返回类型用了auto,在对某个字符串字段做修改时,发现修改没有生效。排查下来发现是auto把返回值拷贝了一份,改的是副本。换成std::invoke配合decltype(auto)之后,问题立刻解决。这也体现出,std::invoke考虑到了引用语义和值语义的区分,不是你随便实现的模板能代替的。

2.3 使用std::invoke时常用的几个技巧

工程中用std::invoke,有几个配合使用的工具需要熟记。

第一个是std::reference_wrapper。当你想把对象以引用的方式存储进容器时,std::invoke能够自动识别std::reference_wrapper并解引用。这意味着你可以配合std::ref写出这样的代码:

struct Foo { void bar() const { /* ... */ } }; std::vector<std::reference_wrapper<Foo>> foos; Foo foo; foos.push_back(std::ref(foo)); // 对每个元素调用 bar() for (auto f : foos) { std::invoke(&Foo::bar, f); // 自动解引用 reference_wrapper }

这里fstd::reference_wrapper<Foo>,但std::invoke不需要你手动f.get(),它会自动对reference_wrapper解引用。这个特性在泛型容器里特别好用,能让代码简洁很多。

第二个技巧是成员变量的访问。std::invoke不仅支持成员函数,还支持数据成员指针。你可以用它来做一些非常灵活的属性推导:

struct Config { int timeout_ms = 1000; std::string name; }; int get_timeout(const Config& c) { return std::invoke(&Config::timeout_ms, c); }

这个用法在反射类的框架里很常见。虽然std::invoke不能实现真正的运行时反射,但它已经把编译期的成员访问统一到了同一种语法下,配合if constexpr和可变参数模板,能写出非常优雅的序列化或日志代码。

不过这里也要提醒一下:std::invoke的成员函数指针形式,对对象的类型要求是很严格的。你不能传一个派生类对象去调用一个基类的私有成员函数(涉及访问控制),也无法让std::invoke去调用静态成员函数时把第一个参数当成对象传进去。静态成员函数本质上不是成员指针,它是普通函数指针,要用&ClassName::static_func来取地址,然后按普通函数指针的方式调用。

3.std::function的实现逻辑与成本分析

3.1 一次拷贝到底发生了什么

std::function的使用很简单,我直接讲它内部发生的事情。当你写下这段代码:

std::function<void(int)> f = [data = std::make_shared<BigStruct>()](int x) { // ... };

编译器和标准库会协作完成以下几步:

  • std::function的模板构造函数接受任何满足Callable且可拷贝构造的类型,它会实例化一个内部的模板类(通常叫_Function_handler)。
  • 这个内部类持有目标可调用对象,同时通过重写一组虚函数(或等在虚表中注册的函数指针),把目标的拷贝、销毁、调用等操作统一暴露出来。
  • std::function需要拷贝时,它会调用内部保存的“拷贝器”虚函数,把存储的可调用对象复制一份。这个拷贝可能发生在栈上(如果对象足够小,且标准库实现了小对象优化),也可能发生在堆上(对象很大或不可移动时)。
  • 调用f(42)时,std::function通过虚表找到真正的调用器,然后执行目标对象的operator()

关键在于,所有这些细节对用户是隐藏的。你手里始终握着一个std::function<void(int)>,不管它背后是lambda、函数对象还是函数指针,对调用者而言完全无感。

这就是类型擦除的全部意义:std::function的多态不是通过继承和虚函数暴露给用户的,而是在内部完成的。你不需要让你的所有回调类都继承自同一个抽象基类,也不需要手动管理函数指针加上下文void*那套繁琐的东西。

3.2 小对象优化与堆分配时机

关于std::function的性能,很多人关心它究竟什么时候会触发堆分配。这个问题的答案与标准库实现有关,通常不是语言标准强制规定的。以libstdc++(GCC)为例,std::function对象本身的大小是48字节(64位系统下),其中有16字节用于存储函数指针和被擦除对象的指针。剩余空间可以用来做小对象优化,但它能容纳的内容有限。

实际上,libstdc++的std::function内部使用了一个_M_invoker函数指针指向调用器,加上一个union用来在小对象情况下直接内嵌存储目标对象,或者在大对象情况下存储指向堆上对象的指针。小对象优化的边界,在不同编译器实现里差异很大。如果你非常在意这个问题,可以用sizeof(std::function<T>)alignof(std::function<T>)去实际测试一下,再根据你的目标对象大小做推断。

我在一个服务端项目里就吃过这个亏。当时在写一个基于锁的并发队列,队列里存的是一批std::function<void()>任务,其中有些任务捕获了相当大的上下文(一个几十字节的结构体加上若干个容器的迭代器)。因为对象比较大,没法走小对象优化,每个任务入队时都伴随着一次堆分配。在高QPS压测下,内存分配器和锁竞争成了瓶颈。后来我改成了将大对象用std::shared_ptr捕获,std::function里只存一个指向共享对象的轻量lambda,这才把性能拉回来。

那段核心改写前后大概是这样:

// 改写前:每个任务捕获整个上下文对象,对象大导致堆分配 struct BigTaskContext { /* 几十个成员,占用大 */ }; std::function<void()> task = [ctx = BigTaskContext{...}]() { ... }; // 改写后:用 shared_ptr 持有大对象,lambda 只捕获一个指针 auto ctx_ptr = std::make_shared<BigTaskContext>(...); std::function<void()> task = [ctx_ptr]() { // 通过 ctx_ptr 使用大对象 };

这种优化思路不是万能的,但它很实用:与其让std::function去复制目标并可能触发堆分配,不如显式管理大对象的生命周期,让std::function只保存一个很轻量的捕获器。这样一来,std::function的拷贝成本极低,而且不会有额外的堆分配。

3.3targettarget_type的用途和限制

std::function提供了两个不常用的成员函数:target_type()target<T>()。它们的作用是,在你知道内部存储的可调用对象的具体类型时,把它取回来直接使用,从而绕开虚函数调用。

std::function<void()> f = []() { std::cout << "hello" << std::endl; }; // 尝试获取原始 lambda 类型 if (auto* ptr = f.target<decltype(f)>()) { (*ptr)(); }

等等,上面这个写法是错的。因为f的类型是std::function<void()>,不是lambda的类型。target<T>()T应该是你存入的那个可调用对象的原始类型,而不是std::function本身的类型。正确的做法是:

auto lambda = []() { std::cout << "hello" << std::endl; }; std::function<void()> f = lambda; if (auto* ptr = f.target<decltype(lambda)>()) { (*ptr)(); // 直接调用原始 lambda,跳过 std::function 的间接层 }

只有T与内部存储类型完全匹配时,target<T>()才会返回非空指针。类型不匹配会返回nullptr,不会抛出异常。

这个特性在实际工程中的价值在于性能优化。如果你有一个热点路径,它接收了一个std::function,但你确切地知道在某个调用点上传进来的是某个特定函数对象,你可以先用target<T>()把它取出来直接调用,省掉虚函数间接调用。但这种用法破坏了通用性,除非必须做极致优化,我不建议在正常业务代码里这么干。

target_type()返回的是std::type_info,用来做运行时类型判断。但注意,使用typeid本身也有开销,而且如果你在代码里写下大量if (f.target_type() == typeid(SomeType)),基本上说明你的设计有问题。std::function的价值在于统一类型,而不是让你把类型再区分回去。真需要区分类型时,应该考虑用std::variant或者自己定义一个抽象接口。

3.4 空函数与调用异常

std::function有一个容易被忽略的状态:它可以为空。默认构造的std::function不持有任何可调用对象,这时对它调用operator()会抛出std::bad_function_call异常。

std::function<void()> f; // 空函数 try { f(); // 抛出 std::bad_function_call } catch (const std::bad_function_call& e) { std::cout << "caught: " << e.what() << std::endl; }

工程上,空函数可以作为“可选回调”的语义来用。比如一个组件允许调用者传入完成回调,但回调是可选的,那么初始化时用空函数,调用前检查if (callback)即可。这个bool转换也是std::function内置的,很方便。

不过要小心:检查if (callback)只能判断它是否为空,不能保证回调内部不会抛出异常。回调是否允许抛异常,还得靠业务约定或者外层try-catch兜底。

4. 工程选型:什么时候用std::invoke,什么时候用std::function

4.1 一个设计判断清单

很多刚接触这两个工具的开发者会纠结“到底该用哪个”。其实答案很简单:如果你需要在运行时保存和传递一个可调用对象,用std::function;如果你只是要立刻调用一次,并且类型在编译期就确定,用std::invoke但工程上的判断往往比这更细,我整理了一张选型清单:

判断维度std::invokestd::function
调用时机拿到可调用对象后立即调用可能延迟调用,甚至跨模块、跨线程
存储需求无,纯粹转发调用需要保存、复制、放入容器
类型信息编译期保留,模板可推导类型擦除,运行时不可见原始类型
性能要求零开销,内联友好有间接调用和可能的堆分配开销
灵活性需要区分成员函数/普通函数对所有可调用对象统一看待
多态需求不需要运行时切换实现需要运行时可替换,策略模式

注意,这里不是二选一的关系,实际工程中两者经常配合使用。比如你写一个通用接口,签名里暴露std::function方便调用方传任何东西;而接口内部对传进来的函数立即执行时,又可以把std::function对象作为参数传给一个std::invoke助手,确保调用逻辑统一。

4.2 通用回调接口设计时的替换实践

在库的API设计中,std::function几乎是最常见的参数类型。它简单、使用方便,对调用方非常友好。但如果你在写一个性能敏感的模板库,直接接收任意类型F&&并用std::invoke调用,会是更优的选择。

举个例子,我写过一个轻量的异步日志库,其中异步线程的入口函数是固定的,但每条日志的处理方式可能不同。最初的接口是:

void set_log_processor(std::function<void(std::string_view)> processor);

调用方需要把lambda包装成std::function传给这个库。这在大多数场景下没问题,但有一个调用方,他们每秒要处理数十万条日志,而且每条日志的处理逻辑只是一个简单的格式判断和一个原子计数器累加。经过性能分析,std::function的间接调用成了热点之一。

后来我把接口改成模板:

template <typename F> void set_log_processor(F&& processor) { static_assert(std::is_invocable_v<F&, std::string_view>); processor_ = std::forward<F>(processor); }

同时在内部保存可调用对象时,也尽量用类型明确的成员变量或std::function做回退。这样做的结果是:调用方传lambda时,lambda类型被完整保留,编译器可以直接内联调用;只有那些确实需要类型擦除的场景才走std::function。改造后,这部分的热点损耗明显下降。

不过这种模板接口也有代价:它会增加编译时间,而且如果processor_在类里就用std::function存储,那模板接口带来的收益会大打折扣。所以设计时需要想清楚:你究竟是需要“接收任何可调用对象但只立即调用”,还是需要“接收并保存,以后随时调用”。前者用模板加std::invoke,后者用std::function

4.3std::invoke在泛型代码中的不可替代性

在某些场景中,std::invoke是几乎没有替代方案的。最典型的就是写泛型工具,支持成员函数指针和普通函数指针的统一处理。

我举一个具体项目里的例子:一个简单的RPC框架,它需要把服务端的方法调用分发到不同的处理函数上。分发表是一个std::map,key是方法名,value是std::function<std::string(const std::string&)>。而每个具体的服务方法可能是普通函数,也可能是类的成员函数。为了让两者都能存进同一个std::function,我使用了lambda加std::invoke作为适配层:

class Server { public: template <typename F> void register_handler(std::string name, F&& f) { handlers_[name] = [f = std::forward<F>(f)](const std::string& req) { // 统一用 std::invoke 处理不同类型的 handler return std::invoke(f, req); }; } private: std::map<std::string, std::function<std::string(const std::string&)>> handlers_; };

这里的register_handler既可以接受一个普通函数,也可以接受一个类的成员函数指针。如果是成员函数,需要额外绑定对象,这个由调用方决定是通过lambda捕获还是通过std::bind处理。但关键是,用std::invoke统一了调用的最后一步,避免为每种可调用形态写一遍调用逻辑。

这种模式在教程里可能看起来有点绕,但在真实项目中非常常见:外部接口用std::function做类型擦除,内部实现用std::invoke保持调用的通用性和效率。两者是配合关系,而非对立关系。

5. 常见问题与排查技巧实录

5.1 成员函数指针的正确写法

一个非常高频的错误是,在std::invokestd::function里传成员函数时,忘记取地址符号。注意成员函数指针的语法必须是&ClassName::member_function,而不是ClassName::member_function。后者在C++里并不是一个有效的表达式。

struct Foo { void func() {} }; std::invoke(&Foo::func, Foo{}); // 正确 std::invoke(Foo::func, Foo{}); // 编译错误

另一个容易出错的是,成员函数本身有重载。此时你需要通过显式指定函数指针类型来消除歧义:

struct Foo { void func() {} void func(int) {} }; using FuncType = void (Foo::*)(); std::invoke(static_cast<FuncType>(&Foo::func), Foo{});

如果不做转换,编译器无法确定你指的是哪个重载。这类错误在泛型代码里尤其隐蔽,因为你往往看不到直接的调用点,只能看到一堆模板错误信息。我的经验是,遇到这种编译错误时,先把成员函数指针赋值给一个具体类型的变量,再用这个变量去调用std::invoke,能显著降低排错难度。

5.2std::function捕获引用时的生命周期陷阱

std::function保存一个捕获了引用变量的lambda时,lambda捕获的引用并不会延长原变量的生命周期。这是一个极其常见的坑。

std::function<void()> make_callback(int& value) { return [&value]() { std::cout << value << std::endl; }; } int main() { std::function<void()> cb; { int local = 42; cb = make_callback(local); } // local 在这里销毁 cb(); // 未定义行为:访问已销毁的局部变量 }

这类问题在VS调试器中可能“碰巧”能运行,看起来一切正常,但在Release版本或开启优化的编译下,随时可能崩溃或输出乱码。排查思路很简单:直接看std::function对应的lambda捕获列表,如果捕获了引用,就要确保被引用对象的生命周期覆盖std::function的使用范围。

我自己的习惯是,凡是需要把lambda存入std::function并可能延迟执行的场景,一律用std::shared_ptr捕获堆对象,或者拷贝值捕获。除非我能非常确定生命周期没有问题。这是写异步代码时的基本素养。

5.3 递归lambda与std::function的冲突

想在lambda内部递归调用自己,最直接的想法是这样:

std::function<void(int)> f = [&](int n) { if (n <= 0) return; std::cout << n << std::endl; f(n - 1); // 引用捕获 f,但 f 还在初始化中 };

这段代码编译能过,但存在一个问题:lambda捕获的是f的引用,而f本身在lambda构造完成后才被赋值。如果lambda在f赋值完成之前被调用(比如在初始化完成后,但在同一条语句中被立即使用),行为可能是未定义的。实际上上面的写法在初始化完成后使用是安全的,因为它捕获的是f的引用,而f在lambda被创建后、调用前已经完成赋值。

但更隐蔽的坑是:你捕获的是std::function的引用,如果外层std::function被拷贝或移动,lambda内部持有的引用仍然指向原来的那个std::function对象,这会导致递归时用到了一个可能已经失效的对象。

更稳妥的做法是使用std::function时配合std::shared_ptr

std::shared_ptr<std::function<void(int)>> f_ptr = std::make_shared<std::function<void(int)>>(); *f_ptr = [f_ptr](int n) { if (n <= 0) return; std::cout << n << std::endl; (*f_ptr)(n - 1); };

或者直接用模板加std::invoke的方式。本质上,递归lambda的问题是lambda类型在定义时还是不完全的,它无法直接通过类型名称引用自己。std::function虽然提供了一种间接引用自己的方式,但生命周期管理需要非常小心。

5.4 性能排查:std::function调用开销到底在哪

很多开发者抱怨std::function比直接函数调用慢,但慢在哪里,经常说不清楚。我用perf做过几次分析,观察到的结论比较一致:

  • 如果可调用对象是lambda,且lambda不捕获任何状态(无捕获或只捕获平凡类型),std::function的间接调用开销可能只有几个纳秒。因为libstdc++可以将无捕获lambda以函数指针的方式存储,调用路径非常短。
  • 如果lambda捕获了大对象,std::function的构造和拷贝会涉及堆分配和对象拷贝,这才是真正的性能大头。
  • 如果在循环中反复调用同一个std::function,间接调用可能干扰CPU的分支预测和指令缓存,但这个影响在大部分业务场景中可以忽略。

所以在优化时,不要一上来就否定std::function,先看它内部存的是什么类型、有没有触发堆分配、调用频率有多高。我曾经在一个场景里,把std::function<void()>换成了void (*)()void*上下文的C风格写法,性能提升了大约10%,但代码可维护性大幅下降。后来发现那10%的性能提升根本没有实际意义,反而让代码变得容易出错。大多数情况下,std::function的性能问题不在调用本身,而在对它的错误使用方式。

5.5 自定义类型擦除的替代方案

最后提一个进阶话题。如果你确实需要类型擦除,但std::function的堆分配让你无法接受,或者你需要管理资源的释放时机,可以考虑自己写一个简单的类型擦除包装器。核心思路是:

template <typename Signature> class unique_function; template <typename R, typename... Args> class unique_function<R(Args...)> { public: template <typename F> unique_function(F&& f) : invoke_([](void* fun, Args... args) -> R { return (*static_cast<std::decay_t<F>*>(fun))(std::forward<Args>(args)...); }), holder_(new std::decay_t<F>(std::forward<F>(f))) {} unique_function(unique_function&& other) : invoke_(std::exchange(other.invoke_, nullptr)), holder_(std::exchange(other.holder_, nullptr)) {} unique_function& operator=(unique_function&& other) { if (this != &other) { delete holder_; invoke_ = std::exchange(other.invoke_, nullptr); holder_ = std::exchange(other.holder_, nullptr); } return *this; } ~unique_function() { delete holder_; } R operator()(Args... args) const { return invoke_(holder_, std::forward<Args>(args)...); } private: R (*invoke_)(void*, Args...); void* holder_; };

这个实现比std::function更精简,只能移动不能拷贝,避免了不必要的复制。但它不支持复制语义,API也不如std::function丰富,只适合在特定场景下使用。

如果你不想自己造轮子,可以看看function_ref(C++26标准化中,之前有第三方实现)或者std::copyable_function(C++23引入)。不过这些新工具目前编译器支持还不完整,可以作为关注方向。

6. 从实践出发的个人体会

我个人在实际项目中,std::invokestd::function是配合使用的,而不是竞争关系。接口对外尽量保持简单,能用模板就用模板,让std::invoke在编译期完成所有分发;当需要真正把回调存下来、传出去、甚至异步执行时,才把类型擦除交给std::function。这个次序一旦颠倒,就会出现“所有回调都是std::function<void()>,每次拷贝都担心堆分配,想优化又无从下手”的尴尬局面。

最后再分享一个小技巧:当你觉得std::function的性能不够用时,先别急着替换掉它。试着把它内部存储的对象换成更轻量的形态,比如用shared_ptr捕获、把多个参数打包成结构体、或者把std::function换成std::move_only_function(如果你能接受C++23)。很多时候性能瓶颈不在间接调用,而在你无意中让它复制了太大的目标对象。想清楚这一点,比盲目手写类型擦除要有效得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询