概述
C++ 11标准库引入了许多创新功能,其中之一便是std::function。作为函数对象容器,std::function允许开发者在编译时不知道具体类型的情况下,存储和传递任意可调用对象,极大地提升了代码的灵活性和可复用性。本文将详细探讨std::function的工作原理、使用场景及其在现代C++编程中的重要地位。
std::function是C++ 11中<functional>头文件中定义的一个类模板,它能够存储并调用任何具有匹配签名的可调用对象,包括:普通函数、成员函数、Lambda表达式、仿函数等,而且可以给函数添加状态。
function的构成
function是一组函数对象包装类的模板,实现了一个泛型的回调机制。function与函数指针比较相似,优点在于它允许用户在目标的实现上拥有更大的弹性。
声明一个function时,需要给出所包装的函数对象的返回值类型和各个参数的类型。其基本声明形式如下:
std::function<返回类型(参数列表)>
比如:声明一个function,它返回一个bool类型并接受一个int类型和一个float类型的参数,可以像下面这样:
function<bool(int, float)> f;
function的接口
function<bool(int, float)> f; function的接口
1、function();
缺省构造函数,创建一个空的函数对象。如果一个空的function被调用,将会抛出一个类型为bad_function_call的异常。
2、template <typename F> function(F g);
这个泛型的构造函数接受一个兼容的函数对象,即这样一个函数或函数对象,它的返回类型与被构造的function的返回类型或者一样,或者可以隐式转换,并且它的参数也要与被构造的function的参数类型或者一样,或者可以隐式转换。注意,也可以使用另外一个function实例来进行构造。如果这样做,并且function g为空,则被构造的function也为空。使用空的函数指针和空的成员函数指针也会产生空的function。
3、template <typename F> function(reference_wrapper<F> g);
这个构造函数与前一个类似,但它接受的函数对象包装在一个reference_wrapper中,用以避免通过值来传递而产生函数或函数对象的一份拷贝。这同样要求函数对象兼容于function的签名。
4、function& operator=(const function& g);
赋值操作符保存g中的函数或函数对象的一份拷贝;如果g为空,被赋值的函数也将为空。
5、template<typename F> function& operator=(F g);
这个泛型赋值操作符接受一个兼容的函数指针或函数对象。注意,也可以用另一个 function 实例(带有不同但兼容的签名)来赋值。这同样意味着,如果g是另一个function实例且为空,则赋值后的函数也为空。赋值一个空的函数指针或空的成员函数指针也会使function为空。
6、bool empty() const;
这个成员函数返回一个布尔值,表示该function是否含有一个函数或函数对象。如果有一个目标函数或函数对象可被调用,它返回false 。因为一个function可以在一个布尔上下文中测试,或者与0进行比较,因此这个成员函数可能会在未来版本的库中被取消,你应该避免使用它。
7、void clear();
这个成员函数清除 function, 即它不再关联到一个函数或函数对象。如果function已经是空的,这个调用没有影响。在调用后,function肯定为空。令一个function为空的首选方法是赋0给它;clear可能在未来版本的库中被取消。
8、result_type operator()(Arg1 a1, Arg2 a2, ..., ArgN aN) const;
调用操作符是调用function的方法。你不能调用一个空的 function ,那样会抛出一个bad_function_call的异常。调用操作符的执行会调用function中的函数或函数对象,并返回它的结果。
绑定与赋值
std::function可以接受各种类型的可调用对象,并通过调用其operator=进行赋值。比如:我们可以将一个Lambda表达式或全局函数绑定到std::function实例。
addFunction = [](int a, int b) { return a + b; }; // 或者是一个全局函数 int add(int a, int b) { return a + b; } addFunction = &add;
std::function的使用场景主要包括如下几处。
事件处理:在设计事件驱动程序时,可以使用std::function来表示事件处理器,方便用户自定义处理逻辑。
策略模式:在实现策略模式时,不同的算法策略可以通过std::function封装,使得在运行时动态切换策略变得简单。
回调函数:许多异步操作需要提供一个完成后的回调函数,此时std::function可以作为统一的回调接口。
泛型编程:编写通用组件时,如果需要接受一个可调用对象作为参数,std::function可以帮助我们写出更加灵活的API。
function的使用
下面分别给出使用function来包装普通函数,函数对象和类的成员函数的示例代码。
1、普通函数
int Add(int x, int y) { return x+y; } function<int (int,int)> f = Add; int z = f(2, 3);
2、函数对象
using namespace std; class CStudent { public: void operator() (string strName, int nAge) { cout << strName << " : " << nAge << endl; } }; int main() { CStudent stu; function<void (string, int)> f = stu; f("Mike", 12); return 0; }
3、类的成员函数
struct TAdd { int Add(int x,int y) { return x + y; } }; function<int (TAdd *, int, int)> f = TAdd::Add; TAdd tAdd; // 如果前面的模板参数为传值或引用,直接传入tAdd即可 f(&tAdd, 2, 3);
接下来,我们看看使用function来保存函数对象状态的情况。考虑下面的示例代码:
class CAdd { public: CAdd():m_nSum(0) { NULL; } int operator()(int i) { m_nSum += i; return m_nSum; } int Sum() const { return m_nSum; } private: int m_nSum; }; int main() { CAdd add; function<int (int)> f1 = add; function<int (int)> f2 = add; cout << f1(10) << "," << f2(10) << "," << add.Sum() << endl; return 0; }
可能与大家想象的结果不一样,上面程序的输出是:10,10,0。我们将同一个函数对象赋值给了两个function,然后分别调用了这两个function,但函数对象中m_nSum的状态并没有被保持,问题出在哪儿呢?这是因为function的缺省行为是拷贝一份传递给它的函数对象,于是f1和f2中保存的都是add对象的拷贝,调用f1和f2后,add对象中的值并没有被修改。
C++ 11中提供了ref和cref函数,来提供对象的引用和常引用的包装。要使function能够正确地保存函数对象的状态,我们可以这样来修改上面的示例代码。
CAdd add; function<int(int)> f1 = ref(add); function<int(int)> f2 = ref(add);
另外,在两个function之间赋值时,如果源function保存的是函数对象的拷贝,则目标function保存的也是函数对象的拷贝。如果源function保存的是函数对象的引用,则目标function保存的也是函数对象的引用。
总结
std::function虽然提供了极大的便利性和灵活性,但需要注意的是,它内部会封装一个类型擦除机制,这可能会带来额外的内存开销和间接调用的成本。因此,在对性能要求极高的场合,直接使用原始的可调用对象或者定制特定的函数指针类型可能更为高效。
std::function作为C++ 11中的一个重要工具,为程序员提供了强大的可调用对象封装能力,简化了代码设计,提高了代码的可读性和可维护性。然而,任何技术选择都需要权衡利弊,在追求灵活性的同时,适时地评估性能影响,以确保在实际项目中做出最优决策。