静态多态(模板)在运行时表现出与非多态代码相同的性能水平,因为它在编译时就已经确定了可以使用的类型集合。这意味着使用模板时,编译器会针对具体的类型参数生成相应的函数或类实例,这样执行效率高,但缺点在于在运行时无法支持未预见到的新类型。换言之,若要在运行时改变处理的类型,则必须重新编译代码。
相反,动态多态(通过继承和虚函数)允许单一版本的多态函数能够与编译时未知的具体类型一起工作,这是因为在运行时通过指向基类的指针或引用调用虚函数,可以根据实际对象类型调用对应的派生类实现。然而,这种灵活性的代价在于,所有参与动态多态的对象必须是从一个共同的基类派生出来的,而且通常会带来一定的性能开销,例如虚函数调用需要经过查找表来进行分派,且会导致代码体积增大。
函数对象、指针和std::function<>
首先,实现一个简单的函数模板:
#include <iostream>
#include <vector>
template<typename F>
void forUpTo(int n, F f) {
for(int i = 0; i < n; i++) {
f(i);
}
std::cout << std::endl;
}
void printInt(int i) {
std::cout << i << " ";
}
int main() {
forUpTo(5, printInt);
std::vector<int> nums;
// 传入一个lambda
forUpTo(5, [&nums](int i) {
nums.push_back(i);
});
std::cout<<nums.size()<<std::endl;
return 0;
}
每次使用forUpTo都可能会产生不同的实例化,这样会进一步增加编译代码的数量,印象可执行程序大小。
一种限制类似代码膨胀的方式,就是将函数模板转换成非函数模板,即使用std::function<> 来封装可调用对象:
void forUpTo(int n, const std::function<void(int)>& f) {
for(int i = 0; i < n; i++) {
f(i);
}
std::cout << std::endl;
}
std::function<>是一个模板类,其模板参数是一个函数类型,这个类型描述了函数对象接收的参数类型和应产生的返回类型,就像函数指针一样指示参数和返回值的类型。std::function<>可以存储任何具有兼容签名的可调用对象,包括但不限于函数指针、lambda表达式以及带有适当operator()重载方法的任意类实例。
这种能力的实现得益于类型擦除(type erasure)技术。类型擦除是一种编程技术,它隐藏了函数对象的实际类型细节,只保留其接口约定(即参数类型和返回类型)。这样,forUpTo()函数在编译期只需处理一个统一的std::function<void(int)>类型,而在运行时能够正确调用传递给它的不同类型的函数对象。这样就巧妙地在静态多态(编译时类型绑定和多态)和动态多态(通过虚函数在运行时决定类型)之间建立了联系,使得forUpTo()函数既可以享受静态多态的高效性和简洁性,又获得了类似动态多态的灵活性(只需要设计一个接口,就能实现多态)。
广义函数指针
std::function<>
类型是普通C++函数指针的一种泛化形式,它不仅提供函数指针的基础操作,还拓展了更多的功能:
-
使用
std::function<>
类型的对象可以在不知道其所包裹的具体函数详情的情况下调用该函数,如同使用函数指针调用函数一样。 -
std::function<>
对象支持复制、移动和赋值操作,这意味着你可以复制一个函数对象,将其传递给其他函数,或者在不同位置间共享同一个可调用实体。 -
std::function<>
对象可以从另一个具有兼容签名的函数初始化或赋值,无论这个函数是普通函数、函数指针还是其他形式的可调用对象。 -
std::function<>
有一个特殊的“空”状态,当没有函数与其绑定时,可以通过检查该状态来判断是否有有效的函数关联到对象上。
然而,与常规C++函数指针不同的是,std::function<>
还能存储lambda表达式或者其他具有合适operator()
重载的任意类对象(即函数对象),这些对象可能具有不同的具体类型。这意味着std::function<>
不仅能包装原始的函数指针,还可以容纳更广泛的可调用实体,增强了其灵活性和适用范围。