用std::function减缓C++模板代码膨胀和编译压力的一个场景

注意:其实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 实例,因为 cubeSquareneg 的类型完全不同(函数指针类型、类类型、闭包类型)。这不仅造成代码冗余,而且当可调用对象数量增多时问题会更加明显。

解决方案:使用 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 函数实例(普通函数,而非模板)。当然,也可以保留模板形式,但显式指定 Fstd::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 只存在一个实例,同时保留了调用不同类型可调用对象的灵活性。

相关推荐
BT-BOX1 小时前
Matlab 2025B下载安装教程
开发语言·matlab
Hical612 小时前
C++17 实战心得:那些真正改变我写代码方式的特性
c++
programhelp_2 小时前
Pinterest OA 题库大公开|Programhelp 独家整理(最新高频)
java·开发语言
他是龙5512 小时前
71:Python安全 & 反序列化 & PYC反编译 & 格式化字符串安全
开发语言·python·安全
Hical613 小时前
实测:C++20 协程 vs Go Gin vs Rust Actix,谁的 Web 性能更强?
c++
wjs20243 小时前
Go 语言接口
开发语言
草莓熊Lotso3 小时前
《告别 “会用不会讲”:C++ string 底层原理拆解 + 手撕实现,面试 / 开发都适用》
开发语言·c++·面试
水木流年追梦3 小时前
【python因果库实战27】逆概率加权模型2
开发语言·python
会编程的土豆3 小时前
【数据结构与算法】空间复杂度从入门到面试:不仅会算,还要会解释
数据结构·c++·算法·面试·职场和发展