C++中的函数对象与Lambda表达式
函数对象是C++中以参数形式传递函数的一个很好的方法,我们将函数包装成类,并且利用()运算符重载实现。
typedef class hello { public: void operator()(double x) { cout << x << endl; } } hello;
这时候hello是一个类,我们可以实例化一个对象hello h;,然后通过h(3.14)的方式来调用这个类的成员函数,如果某个函数需要这个函数作为回调函数,则可以将这个hello类的对象传入即可。
因为这是一个类的定义,因此我们完全可以在其中定义一些包含额外信息的成员和一些构造函数,让这个函数对象可以做更多不同的可定制的任务,最终的行为实际上只是调用了这个()运算符重载函数。这种做法比C++函数指针要容易理解得多,也不容易写错。
而Lambda表达式则是C++中的新语法,实现了许多程序员渴望的部分闭包特性。C++中Lambda表达式可以被视为一种匿名函数,这样,对于一些非常短,而且不太可能被其他地方的复用的小函数,可以通过Lambda表达式提高代码的可读性。
在Lambda表达式中对于变量生命期的控制还是与完全支持闭包的JavaScript非常不同,总而言之,C++对于变量声明期的控制在新标准中完全向前兼容,也就是局部变量一定在退出代码块时被销毁,而不是观察其是否被引用。因此,尽管C++的Lambda表达式中允许引用其代码上下文中的值,但是实际上并不能够保证引用的对象一定没有被销毁。
Lambda表达式对于上下文变量的引用有值传递和引用传递两种方式,实际上,无论是哪种方式,在产生Lambda表达式对象时,这些上下文值就已经从属于Lambda表达式对象了,也就是说,代码运行至定义Lambda表达式处时,通过值传递方式访问的上下文变量值已经被写入Lambda表达式的栈中,而引用方式传递的上下文变量地址被写入Lambda表达式的栈中。因此,调用Lambda表达式时得到的上下文变量值就是定义Lambda表达式时这些变量的值,而引用的上下文变量,如果已经被销毁,则会出现运行时异常。
Lambda表达式的基本语法是:
[上下文变量说明](Lambda表达式参数表) -> 返回类型 { 语句块 }
上下文变量说明部分就是说明对于上下文变量的引用方式,=表示值传递,&表示引用传递,例如,&s就表示s变量采用引用传递,不同的说明项之间用逗号分隔,可以为空,但是方括号不能够省略。第一项可以是单独的一个=或者&,表示,所有上下文变量若无特殊说明一律采用值传递/引用传递,什么都不写默认为值传递。
Lambda表达式和TR1标准对应的function<返回类型 (参数表)>对象是可以互相类型转换的,这样,我们也可以将Lambda表达式作为参数进行传递,也可以作为返回值返回。
下面看一个Lambda表达式各种使用方法的完整例子:
// compile with: /EHsc #include <iostream> #include <string> #include <functional> //这是TR1的头文件,定义了function类模板 using namespace std; typedef class hello { public: void operator()(double x) { cout << x << endl; } } hello; //函数对象的定义,也是非常常用的回调函数实现方法 void callhello(string s, hello func) { cout << s; func(3.14); } //一个普通的函数 void callhello(string s, const function<void (double x)>& func) { cout << s; func(3.14); } //这个函数会接受一个字符串和一个Lambda表达式作为参数 void callhello(string s, double d) { [=] (double x) { cout << s << x << endl; }(d); } //这个函数体内定义了一个Lambda表达式并立即调用 function<void (double x)> returnLambda(string s) { cout << s << endl; function<void (double x)> f = ([=/*这里必须使用值传递,因为s变量在returnLambda返回后就被销毁*/] (double x) { cout << s << x << endl; }); s = "changed"; //这里对s的修改Lambda表达式是无法感知的,调用这句语句前s在Lambda表达式中的值已经确定了 return f; } //这个函数接受了一个值传递的字符串变量s,我们将Lambda表达式作为返回值返回 function<void (double x)> returnLambda2(string& s) { cout << s << endl; function<void (double x)> f = ([&s/*这里可以使用引用传递,因为s是引用方式传入的,不随函数返回而消亡*/] (double x) { cout << s << x << endl; }); s = "changed"; //这里对s的修改Lambda表达式是可以感知的,因为s以引用方式参与到Lambda表达式上下文中 return f; } //这个函数接受了一个引用传递的字符串变量s,将Lambda表达式作为返回值返回 int main() { hello h; callhello("hello:", h); //用函数对象的方式实现功能 callhello("hello lambda:", -3.14); //这个函数体内定义了一个Lambda表达式并立即调用 int temp = 6; callhello("hello lambda2:", [&] (double x) -> void { cout << x << endl; cout << temp++ << endl; }); //这个函数会接受一个字符串和一个Lambda表达式作为参数 cout << temp << endl; function<void (double x)> f = returnLambda("lambda string"); //这个函数接受了一个值传递的字符串变量s,我们将Lambda表达式作为返回值返回 f(3.3); string lambdastring2 = "lambda string2"; //这个变量在main函数返回时才被销毁 f = returnLambda2(lambdastring2); //这个函数接受了一个引用传递的字符串变量s,将Lambda表达式作为返回值返回 f(6.6); system("pause"); }
关于JavaScript匿名函数与闭包的一些解释
JavaScript作为一种非常奇特的动态脚本语言,具有一些特有的语法特性,其中匿名函数和闭包特性可以说很大程度上区别了JavaScript程序设计与其他传统语言程序设计的方法。
匿名函数:
var x = function() { document.write('hello world!'); }; x();
上面这个例子是一个最简单的匿名函数,显而易见的事实有三点:第一,函数没有被命名;第二,函数以对象的形式赋值给变量x;第三,我们也可以通过变量名x来调用这个函数。这使得函数能够非常方便地以参数的形式进行传递。
闭包:
关于闭包特性有许多复杂的解释,但是为了便于理解,简单来说就是,一个变量的作用域或者生命期,与其所在的源代码中的位置有关,而不是与运行时的上下文相关。下面是一个关于闭包的小例子(来自维基百科):
function derivative(f, dx) { return function (x) { return (f(x + dx) - f(x)) / dx; }; } var result = derivative(function(x) { return x * x; }, 0.000001)(5); document.write(result + '<br />');
上面的代码实际上是在求函数某一点上的近似的导数值。
derivative这个函数接受两个参数,f应该是接受一个数值型变量的函数,dx应该是一个很接近0的浮点数。然而derivative实际上并没有返回一个确切的导数值,因为并没有得到需要求解的确切的横坐标的值,因此返回了原函数f的导函数。请注意,有一个只接受一个参数x的匿名函数作为返回值被derivative函数返回了。
下面定义了result变量,首先调用derivative求导函数,需要传入两个参数,f传入了一个匿名函数x平方,dx按照要求传入一个很小的值0.000001(你也可以取得更大一些或更小一些)。接着应该就得到了一个导函数,再为导函数传入常量参数5,得到了x平方在x=5这一点上的近似导数值,输出的结果非常接近10,也是我们期待的结果。
接下来反过来看一看函数执行的实际过程。
- derivative函数被定义。
- derivative被调用,此时derivative函数得到的实参分别是f=function(x) {return x * x;}和dx=0.000001。
- 函数返回一个函数function (x) { return (f(x + dx) - f(x)) / dx; }。
- 随后调用这个刚刚被返回的函数,实参是x=5。请注意,按照传统语言的变量作用域,此时derivative函数应当早已退出堆栈,其参数f和dx的值应当早已不在内存,而在JavaScript中,此时f和dx的值却仍然保留为derivative函数被调用时的值,仅因为这个匿名函数被写在了derivative函数的内部。