C++构造自动推导模板类型的function adaptor(STL Function Adaptor)
继续前一篇文章http://deltamaster.is-programmer.com/posts/41833.html关于构造function adaptor的话题。
template <typename operation> operation derivative_functor(operation f, typename operation::argument_type dx) { typedef typename operation::argument_type T; return [=](T x)->T { return (f(x + dx) - f(x)) / dx; }; }
这是前一篇文章最后构造的function adaptor。回忆一下,尽管我们不再需要在调用时指定lambda表达式的参数类型T,但是我们仍然需要指定operation的类型。从C++语法上讲,函数模板的调用应该是支持类型推导的,也就是说,如果调用derivative_functor的时候,没有指定operation的类型,C++编译时也会自动推导。可惜这种情况下自动推导的结果是错误的,以至于无法通过编译。
再看一下调用语句。
typedef function<double (double)> operation; cout << derivative_functor<operation>(my_sqr_functor<double>(), 0.000001)(5) << endl;
在这里我们给出的参数是my_sqr_functor<double>(),那么经过自动推导得到的operation类型就是my_sqr_functor<double>,尽管这个lambda表达式和my_sqr_functor<double>都分别兼容于类型function<double (double)>,但是它们两者之间没有继承关系,因此难以完成转换。
为了解决这个问题,我们就要避免出现这种不兼容的类型转换,只管的解决办法就是,令这个adaptor返回另一种类型的对象(而不是lambda表达式),这种对象的类型与operation指定的类型类似,并且同样可以像函数一样使用,兼容function<T (T)>类型。到这里我们必须要放弃lambda表达式,而用functor来模拟这个lambda表达式的行为了。
首先刚才说到,operation的类型和adaptor的返回类型有共性,下面让我们来构造这个共性的部分。
template<typename Arg, typename Result> class my_unary_function { public: typedef Arg argument_type; typedef Result result_type; private: virtual Result operator()(Arg arg) = 0; };
这两者都可以当做一元函数来使用,并且我们用typedef定义了argument_type和result_type了,关于这个typedef的使用前一篇已经有一些说明。
template<typename T> class my_sqr_functor_derived : public my_unary_function<T, T> { public: T operator()(T x) { return (x * x); } };
这个求平方的functor应该继承自这个my_unary_function,因为它“是”一个一元函数,这个继承关系会将父类的typedef一并继承下来。子类的T的实际类型会被代入父类my_unary_function的Arg和Result,成为自己的argument_type和result_type。
接下来就是重点的adaptor本身的改造了。
template <typename operation> class derivative_functor_derived : public my_unary_function<typename operation::argument_type, typename operation::result_type> { private: operation op; typename operation::argument_type dx; public: derivative_functor_derived(operation f, typename operation::argument_type x) : op(f), dx(x) {} typename operation::result_type operator()(typename operation::argument_type x) { return (op(x + dx) - op(x)) / dx; } };
首先这个adaptor现在是一个对象,它也“是”一个一元函数,因此同理,也继承自my_unary_function,并且指定恰当的模板参数。两个private对象,op和dx,就是先前lambda表达式中,从函数scope中传递下来的f和dx,我们在构造这个对象的时候,通过构造函数,将我们指定的op和dx保存到本对象的数据成员中,在括号运算符重载函数中,就可以使用op和dx了。至此,我们的adaptor是一个具有一元函数性质的对象,可以直接使用,而避免了不兼容类型的转换。
等一下!C++的语法不支持类模板的类型推导,这意味着我们调用这个构造函数的时候还是必须要指定的,像下面这样。
cout << derivative_functor_derived<my_sqr_functor_derived<double> >(my_sqr_functor_derived<double>(), 0.000001)(5) << endl;
我们刚才消除不兼容的类型转换,是为了省去模板参数的指定!别担心,注意到现在模板参数和传递的参数类型已经一致了吗?那现在我们只需要一个wrapper函数来帮助我们完成心愿。
template <typename operation> derivative_functor_derived<operation> derivative_functor_derived_wrapper(operation f, typename operation::argument_type dx) { return derivative_functor_derived<operation>(f, dx); }
这个函数所做的唯一的事情,就是调用了derivative_functor_derived的构造函数,不过因为这是一个函数模板,根据我们传递的f参数类型,就可以自动推导正确的operation类型了!
cout << derivative_functor_derived_wrapper(my_sqr_functor_derived<double>(), 0.000001)(5) << endl;
这里就是最终形式了。回想上一篇开头的STL Function Adaptor调用,看看是不是有些类似呢?
cout << count_if(v.begin(), v.end(), bind2nd(less<int>(), 12)) << endl;
我们的derivative_functor_derived_wrapper和STL函数模板bind2nd的行为完全一致。这就是STL Function Adaptor的实现原理。
至此,我们了解了STL Function Adaptor的设计思想和实现,并且能够使用functor来模拟一些lambda表达式的行为。
C++使用Lambda表达式做function adaptor
我们往往有许多现成的函数(或者是具有函数行为的对象),有些时候,我们希望通过改造这些函数来生成新的函数来为我们所用。
int myArr[] = {1, 1, 2, 3, 5, 8, 13, 21}; vector<int> v(myArr, myArr + sizeof(myArr) / sizeof(int)); cout << count_if(v.begin(), v.end(), bind2nd(less<int>(), 12)) << endl;
这段代码中less<int>()是临时functor对象,而bind2nd负责将这个接受两个参数比较他们大小的函数,将第二个参数绑定为12,改造成接受一个参数的functor,就可以被count_if所使用了。这段忙代码帮忙数出vector中小于12的项的数量。
我曾经在博客中写过利用Javascript闭包来实现的改造器。
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);
这个derivative是一个改造器,接受参数是一个函数f,返回其导函数。具体解释请参考http://deltamaster.is-programmer.com/posts/28768.html。
下面我们要用C++的Lambda表达式来实现类似功能。
现有一个非常简单的函数,求x的平方,和上面Javascript中的例子相同。
double my_sqr(double x) { return (x * x); }
下面我们写一个改造器。
function<double (double)> derivative_simple(function<double (double)> f, double dx) { return [=](double x)->double { return (f(x + dx) - f(x)) / dx; }; }
这个改造器是一个函数,接受的第一个参数是一个函数指针,或者其他具有函数行为的对象,这种对象作为函数使用的时候,接受一个double类型的参数,返回double类型。这个改造器的返回类型和它的第一个参数类型相同。
在我们返回的Lambda表达式中,我们从改造器的scope把f和dx都以值传递方式传递进来。这个Lambda表达式根据输入参数x,求出这一点的导数值。
然后我们调用它,并接着直接调用其返回的Lambda表达式,得到x=5这一点的导数值。如我们所愿,我们看到了10(微积分还没有忘光吧)。
cout << derivative_simple(my_sqr, 0.000001)(5) << endl;
不过有一点不够理想的是,这个改造器的适用范围太狭窄了,如果我需要求导的函数接受的不是double类型的参数,而是Stone类型(假设这种神奇的Stone能进行数值计算),我们就要再写另一个差不多的改造器。这里你应该差不多想到了,我们可以用模板来实现。
template <typename operation, typename T> operation derivative_any(operation f, T dx) { return [=](T x)->T { return (f(x + dx) - f(x)) / dx; }; }
大家可以比较这里的operation和T出现的位置,和上面的例子比较一下。然后我们调用它。
typedef function<double (double)> operation; cout << derivative_any<operation, double>(my_sqr, 0.000001)(5) << endl;
我们在调用之前用typedef做了一个类型声明,简化下面调用语句的书写。这次调用的效果还是一样的。但是如果将来真的要把Stone传进来,我们只要将operation指定为function<Stone (Stone)>,将T指定为Stone就可以了。
到这里就有一个小问题了,因为其实operation里的参数和返回类型,应该和T指定的类型总是相同的。按照上面的写法,淘气的程序猿完全可以指定不同类型,而造成不必要的错误。在编译时期发现错误还好,如果是发生了非预期的隐式类型转换而造成运行时错误,就很难发现了。
这样就要求我们能够在operation里携带一些类型信息,再将这个类型信息在改造器中取出,取代现在T的位置。不过一个普通的函数没有办法隐含这样的信息,于是我们需要把这个求平方的函数本身变成一个functor,以便保存额外的类型信息。
template<typename T> class my_sqr_functor { public: typedef T argument_type; T operator()(T x) { return (x * x); } };
在这里我们用typedef来把类模板参数T定义为argument_type类型。想象一下,当这个模板被展开的时候,T被各种不同的类型替换,比如double,这个时候我们用typedef,将double在类的范围内定义为了argument_type。这样,我们在这个类外也总是能够通过对象的argument_type来获得当初指定给T的类型。
下面修改我们的改造器。
template <typename operation> operation derivative_functor(operation f, typename operation::argument_type dx) { typedef typename operation::argument_type T; return [=](T x)->T { return (f(x + dx) - f(x)) / dx; }; }
改造器也相应升级了,我们在这里不需要再指定模板参数T了,而是用typename operation::argument_type来代替。下面再将它定义成T纯粹是为了书写方便。改造器的功能基本没有变化。然后我们调用它。
typedef function<double (double)> operation; cout << derivative_functor<operation>(my_sqr_functor<double>(), 0.000001)(5) << endl;
调用方法和上面稍有不同的,一方面我们不再能够指定那个模板参数T,另一方面my_sqr_functor<double>不再是一个函数,而是一个模板类,直接在后面加上括号调用其默认构造函数构造了一个临时对象,这种写法和最开头看到的less<int>()是同样的含义。
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"); }