1:为什么function是运行时的多态
要分清编译时和运行时,最简单的方法就是看决议(dispatch)发生在什么时候。
编译时多态(模板/重载):编译器在编译期就确定了要调用哪个函数,直接把函数地址写死在机器码里。
运行时多态(虚函数/std::function):编译器不知道具体调谁,需要在程序跑起来后,根据内存里的数据(虚表指针或函数指针)去跳转。
cpp
// 外部调用
m_invoker->invoke(x);
在这行代码里,m_invoker 的类型是 IInvoker*(基类指针)。编译器在编译这行代码时,根本不知道这个指针指向的是 InvokerImpl<Lambda> 还是 InvokerImpl<函数指针>。
程序运行到这一行,CPU 会读取 m_invoker 指向的内存地址,从该对象中取出虚函数表指针(vptr),再从虚表中找到 invoke 函数的实际地址,最后跳转过去。
这个"查表-跳转"的动作只能发生在程序运行期间,因为只有运行时,内存里才有了具体的对象实例。
所以,std::function 本质上就是通过虚函数机制(或等效的函数指针模拟)实现的运行时多态
2.:为什么 std::function 难以(甚至无法)被内联?
我加了 inline 关键字,或者编译器开了 O2,为什么还是内联不了?
核心原因一句话:内联(Inlining)是编译期的行为,而 std::function 的调用目标在编译期是未知的。
内联要求编译器在编译当前 .cpp 文件时,必须清清楚楚地看到被调用函数的函数体,并且确认调用的地址是确定的。
我们在代码里分析一下编译器的视角:
场景 A:直接调用 Lambda(可以内联)
cpp
auto lambda = [](int x){ return x + 1; };
lambda(10);
编译器看到 lambda(10),在编译期就知道要调用 Lambda::operator()。它直接把 return x + 1 这行代码插入到调用处,根本没有"函数调用"的机器码。零开销。
场景 B:通过 MyFunction 调用(难以内联)
cpp
MyFunction f = lambda;
f(10); // 这里发生了什么?
编译器在编译 f(10) 这行时,只能看到 MyFunction::operator() 的定义:
cpp
void operator()(int x) const {
if (m_invoker) {
m_invoker->invoke(x); // 这里是一个虚函数调用!
}
}
编译器视角的分析:
它看到了 m_invoker->invoke(x),这是一个通过指针调用的虚函数。
编译器并不知道 m_invoker 指向的是谁。它可能指向 InvokerImpl<Lambda>,也可能指向 InvokerImpl<Functor>。
编译器不可能把"所有可能的子类函数体"都塞进这一个调用点,因为如果塞了 Lamba 的函数体,万一运行时传进来的是函数指针,程序就崩了。
因此,编译器只能放弃内联,生成一条标准的汇编指令:call rax + 8(从寄存器读取地址跳转)
在现代编译器中(如 GCC 的 -flto 链接时优化),有一种极其特殊的情况叫去虚拟化,想了解的可以下去了解一下