注意:其实std::function也是模板,也会产生多个实例,但对于下面的例子来说,减少了实例的范围。
https://mp.weixin.qq.com/s/okDFMAUHUBWunTObYkluDQ
C++模板的一个很大的缺点是会较大拖慢编译的速度,有的时候有时让人难以接受了。
问题示例:多次实例化
假设有一个模板函数 use_f,它接受一个可调用对象 f 和一个 double 值,然后调用 f 并返回结果:
cpp
template <typename F>
double use_f(F f, double x) {
return f(x);
}
现在我们有三种不同类型的可调用对象(函数、函数对象、lambda),但它们的调用特征标都是 double(double):
cpp
double cube(double x) { return x * x * x; }
struct Square {
double operator()(double x) const { return x * x; }
};
auto neg = [](double x) { return -x; };
如果我们分别调用 use_f(cube, 5.0)、use_f(Square{}, 5.0)、use_f(neg, 5.0),编译器会生成三个不同的 use_f 实例,因为 cube、Square 和 neg 的类型完全不同(函数指针类型、类类型、闭包类型)。这不仅造成代码冗余,而且当可调用对象数量增多时问题会更加明显。
解决方案:使用 std::function
std::function 定义在头文件 <functional> 中,它的模板参数是调用特征标。例如 std::function<double(double)> 可以包装任何接受一个 double 并返回 double 的可调用对象。
我们修改 use_f,使其参数类型不再是模板参数,而是固定的 std::function<double(double)>:
cpp
double use_f(std::function<double(double)> f, double x) {
return f(x);
}
此时,无论我们传入函数指针、函数对象还是 lambda,use_f 的参数类型都是相同的 std::function<double(double)>,因此整个程序只存在一个 use_f 函数实例(普通函数,而非模板)。当然,也可以保留模板形式,但显式指定 F 为 std::function<double(double)>,不过更自然的做法是直接使用普通函数。
完整代码示例
下面是一个完整的示例,展示了如何使用 std::function 包装器统一处理多个调用特征标相同的可调用对象,并且只实例化一次 use_f。
cpp
#include <iostream>
#include <functional> // for std::function
// 普通函数
double cube(double x) {
return x * x * x;
}
// 函数对象(仿函数)
struct Square {
double operator()(double x) const {
return x * x;
}
};
int main() {
// 几个调用特征标均为 double(double) 的可调用对象
double (*func_ptr)(double) = cube; // 函数指针
Square func_obj; // 函数对象
auto lambda = [](double x) { return -x; }; // lambda 表达式
// 使用 std::function 包装器统一类型
std::function<double(double)> wrapper1 = func_ptr;
std::function<double(double)> wrapper2 = func_obj;
std::function<double(double)> wrapper3 = lambda;
// 也可以直接传入可调用对象,因为 std::function 的构造函数是隐式的
// 定义一个接受 std::function 参数的普通函数
// 这个函数只会被实例化一次,因为参数类型固定
auto use_f = [](std::function<double(double)> f, double x) -> double {
std::cout << "调用 use_f,函数地址: " << &f << std::endl; // 演示只存在一个实例
return f(x);
};
double x = 5.0;
std::cout << "cube(5) = " << use_f(wrapper1, x) << std::endl;
std::cout << "square(5) = " << use_f(wrapper2, x) << std::endl;
std::cout << "negate(5) = " << use_f(wrapper3, x) << std::endl;
// 也可以直接传递可调用对象,std::function 会隐式构造
std::cout << "直接传递 lambda: " << use_f([](double x){ return x + 10; }, 3.0) << std::endl;
return 0;
}
输出示例(具体地址可能不同):
调用 use_f,函数地址: 0x7ffc1234
cube(5) = 125
调用 use_f,函数地址: 0x7ffc1234
square(5) = 25
调用 use_f,函数地址: 0x7ffc1234
negate(5) = -5
调用 use_f,函数地址: 0x7ffc1234
直接传递 lambda: 13
可以看到,每次调用 use_f 时,f 的地址都相同,说明 use_f 只有一个实例(lambda 表达式的地址相同进一步证明函数体只有一个)。
进一步说明
-
性能考虑 :
std::function会引入一定的运行时开销(类型擦除、可能的内存分配),但通常可以忽略不计。如果性能极其敏感,且可调用对象类型在编译期已知,模板方案更优。但在需要统一接口或减少代码膨胀的场景下,std::function是很好的选择。 -
与模板对比 :模板方案为每种类型生成独立代码,没有运行时开销,但会导致代码膨胀。
std::function方案只生成一份代码,但调用时通过虚函数或函数指针间接跳转,略有开销。两者各有适用场景。 -
C++23 补充 :C++23 引入了
std::move_only_function,适用于只移类型的可调用对象;C++26 可能有更多改进,但std::function依然是日常最通用的工具。
总结
当多个可调用对象具有相同的调用特征标时,利用 std::function 包装器可以将它们统一为同一个类型,从而避免模板函数或模板类的多次实例化,简化代码并减少编译产物体积。上述示例清晰地展示了如何重写程序,使得原本需要多次实例化的 use_f 只存在一个实例,同时保留了调用不同类型可调用对象的灵活性。