这篇文章是我学习boost phoenix的总结。
Phoenix是一个C++的函数式编程(function programming)库。Phoenix的函数式编程是构建在函数对象上的。因此,了解Phoenix,必须先从它的基础函数对象上做起。
Phoenix能够提供令人惊艳的编码效果。我先撂一个出来,看看用Phoenix能写出什么样的代码:
std::for_each(vec.begin(), vec.end(), if_(arg1 > 5) [ std::cout << arg1 << ">5\n" ] .else_ [ if_(arg1 == 5) [ std::cout << arg1 << "== 5\n" ] .else_ [ std::cout << arg1 << "< 5\n" ] ] );这是C++代码?答案是肯定的!只需要C++编译器,不需要任何额外的工具,就能实现这样的效果。这是怎么回事?且看下面逐步分解。
在此之前,编译phoenix库必须
包含核心头文件
#include <boost/phoenix/core.hpp>
using boost::phoenix::val; using boost::phoenix::arg_names::arg1; using boost::phoenix::arg_names::arg2; using boost::phoenix::case_; using boost::phoenix::ref; using boost::phoenix::for_; using boost::phoenix::let; using boost::phoenix::lambda; using boost::phoenix::local_names::_a;
另外一个原因是,要防止不必要的命名污染,因为phoenix用了很多和boost库冲突的名称,这些在使用的时候,很容易造成问题。
包含头文件:
#include <boost/phoenix/core.hpp>使用命名空间:
using boost::phoenix::val;例子
val(3) val("Hello, World")val(3) 生成一个包含整数3的 函数对象。val("Hello, World")则是一个包含字符串的 函数对象。
他们是函数对象,因此,你可以象函数那样调用他们
std::cout << val(3)() << val("Hello World")()<<std::endl;val(3)() 将返回值3, val("Hello World")() 将返回值"Hello World"。
也许,你会觉得,这简直是多此一举。但是,事实上,你没有明白phoenix的真正用以。
val(3)和val("Hello World") 实际上实现了一个懒惰计算的功能,将对3和"Hello World"的求值,放在需要的时候。
上面的表达式,还可以写成这样
(std::cout << val(3) << val("Hello World")<<std::endl)();括号中std::cout << .. 这一长串,实际上生成了一个函数对象,因此我们才能在需要的时候,调用这个函数对象。
这是val的真正威力,它让求值推迟到需要的时候。在普通编程中,我们必须通过类和接口才能完成。
#include <boost/phoenix/core.hpp>使用命名空间
using boost::phoenix::ref;如果声明了如下变量:
int i = 3; char const* s = "Hello World"; std::cout << (++ref(i))() << std::endl; std::cout << ref(s)() << std::endl;
因此,上面 (++ref(i))()的返回值是4,而且,变量i的值也将变为4.
references是phoenix的函数对象和外部变量交换数据的桥梁。
包含头文件
#include <boost/phoenix/core.hpp>
使用命名空间
using boost::phoenix::arg_names::arg1; using boost::phoenix::arg_names::arg2; using boost::phoenix::arg_names::arg3; ....
看下面的例子
std::cout << arg1(3) << std::endl; std::cout << arg2(2, "hello world") << std::endl;
依次类推。
比如,我们有一个函数
void testArg(F f) { f(1,2,3); } ... int main() { testArg(std::cout<<arg1<<"-"<<arg2<<"-"<<arg3<<std::endl); }std::cout ... 这一长串生成了一个函数对象。arg1 ,arg2, arg3分别提取了testArg传递的参数1,2,3。因此,这个函数会返回"1-2-3"。如果你将arg1和arg3的位置兑换下,返回的结果将是"3-2-1"。
头文件
#include <boost/phoenix/operator.hpp>
看个例子
std::find_if(vec.begin(), vec.end(), arg1 %2 == 1);
答案是肯定的!。
它一共涉及两个操作符 %和 == 。 arg1 % 2 生成一个函数对象,新生成的函数对象在通过 == 操作符,又生成了新的对象。
它实际上就是
auto func1 = operator % (arg1, 2); auto func2 = operator == (func1, 1);最后的func2被传递给了find_if。
phoenix支持所有的操作符,包括一元操作符在内,
如:
1 << 3; // Immediately evaluated val(1) << 3; // Lazily evaluated
prefix: ~, !, -, +, ++, --, & (reference), * (dereference) postfix: ++, --
=, [], +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= +, -, *, /, %, &, |, ^, <<, >> ==, !=, <, >, <=, >= &&, ||, ->*
if_else(c, a, b)
struct A { int member; }; A* a = new A; ... (arg1->*&A::member)(a); // returns member a->memberarg1->*&A::member实现一个对A对象的访问操作。
头文件
#include <boost/phoenix/statement.hpp>
using boost::phoenix::if_; using boost::phoenix::switch_; using boost::phoenix::case_; using boost::phoenix::while_; using boost::phoenix::for_; ....
std::for_each(vec.begin(), vec.end(), if_(arg1 > 5) [ std::cout << arg1 << "," ] );
if_.operator()( operator > (arg1, 5) ) .operator[]( operator<<( operator<<(std::cout, arg1) , ",") )
它用逗号代替分号,模拟语句序列,如
statement, statement, .... statement主要,最后一条"语句"(实际上是函数对象),不能有“,”,如是这样
statement, statement, statement, // ERROR!就错了。
可以用括号来扩住一些语句(即函数对象)
statement, statement, ( statement, statement ), statement括号也可以用在最外层,将语句(即函数对象)进行分组,如
std::for_each(c.begin(), c.end(), ( do_this(arg1), do_that(arg1) ) );
construct<std::string>(arg1, arg2) // constructs a std::string from arg1, arg2 new_<std::string>(arg1, arg2) // makes a new std::string from arg1, arg2 delete_(arg1) // deletes arg1 (assumed to be a pointer) static_cast_<int*>(arg1) // static_cast's arg1 to an int*
头文件
#include <boost/phoenix/function.hpp>
boost::phoenix::function
考虑一个factorial函数
struct factorial_impl { template <typename Sig> struct result; template <typename This, typename Arg> struct result<This(Arg)> : result<This(Arg const &)> {}; template <typename This, typename Arg> struct result<This(Arg &)> { typedef Arg type; }; template <typename Arg> Arg operator()(Arg n) const { return (n <= 0) ? 1 : n * this->operator()(n-1); } };
factorial_impl的result声明是必须的,这是phoenix的模板要求的。result的声明使用了半实例化模板
template <typename Sig> struct result;这是声明一个主模板,当然,主模板没有任何用处,因此只声明不定义。
template <typename This, typename Arg> struct result<This(Arg &)> { typedef Arg type; };这是一个半实例化的模板。从 result<This(Arg&)>可以看出。 This(Arg&)声明一个返回对象为 This, 参数为Arg& 的函数。
后面,Arg operator()(Arg)就是函数对象的实现体了。
使用时,需要这样
int main() { using boost::phoenix::arg_names::arg1; boost::phoenix::function<factorial_impl> factorial; int i = 4; std::cout << factorial(i)() << std::endl; std::cout << factorial(arg1)(i) << std::endl; return 0; }
上面的代码,书写起来,还是比较麻烦的,因此,phoniex提供了几个宏,用于帮助实现函数对象的适配。
它的语法是
BOOST_PHOENIX_ADAPT_FUNCTION_NULLARY( RETURN_TYPE , LAZY_FUNCTION , FUNCTION )
BOOST_PHOENIX_ADAPT_FUNCTION( RETURN_TYPE , LAZY_FUNCTION , FUNCTION , FUNCTION_ARITY )NULLARY表明是没有参数的。
针对NULLARY的例子:
声明函数:
namespace demo { int foo() { return 42; } }生成函数对象
BOOST_PHOENIX_ADAPT_FUNCTION_NULLARY(int, foo, demo::foo)使用它:
std::cout << "foo()():"<<foo()() << std::endl;foo() 返回一个函数对象。 foo是一个函数,你可以认为是函数对象的工厂。
namespace demo { int plus(int a, int b) { return a+b; } template<typename T> T plus ( T a, T b, T c) { return a + b + c; } } BOOST_PHOENIX_ADAPT_FUNCTION(int, myplus, demo::plus, 2) BOOST_PHOENIX_ADAPT_FUNCTION( typename boost::remove_reference<A0>::type , myplus , demo::plus , 3 )
int a = 123; int b = 256; std::cout<<"myplus:"<<(myplus(arg1, arg2)(a, b)) << std::endl; std::cout<<"myplus<3>:"<<(myplus(arg1, arg2, 3)(a, b)) << std::endl;
至于细节,了解不是很多,不管怎么样,用就是了。
在使用上,同FUNCTION对应的函数,但是,它是针对函数对象的。
如
namespace demo { struct foo2 { typedef int result_type; int operator()() const { return 42; } }; } BOOST_PHOENIX_ADAPT_CALLABLE_NULLARY(foo2, demo::foo2)
值得注意的是,foo2中 typedef int result_type;的声明是必须的,因为,它是phonix模板要求的,一旦没有,就会出错。
带有重载的例子
namespace demo { struct plus { template<typename Sig> struct result; template<typename This, typename A0, typename A1> struct result<This(A0, A1)> :boost::remove_reference<A0> {}; template<typename This, typename A0, typename A1, typename A2> struct result<This(A0, A1,A2)> :boost::remove_reference<A0> {}; template<typename A0, typename A1> A0 operator()(A0 const& a0, A1 const &a1) const { return a0 + a1; } template<typename A0, typename A1, typename A2> A0 operator()(A0 const& a0, A1 const &a1, A2 const &a2) const { return a0 + a1 + a2; } }; } BOOST_PHOENIX_ADAPT_CALLABLE(plus, demo::plus, 2) BOOST_PHOENIX_ADAPT_CALLABLE(plus, demo::plus, 3)struct result的声明也是使用了半实例化的技巧。需要给出参数个数,这个是很重要的。
语句在上面提到过,这里介绍更多的语句
我们开头看到的,就是一个if_else_语句
std::for_each(vec.begin(), vec.end(), if_(arg1 > 5) [ std::cout << arg1 << ">5\n" ] .else_ [ if_(arg1 == 5) [ std::cout << arg1 << "== 5\n" ] .else_ [ std::cout << arg1 << "< 5\n" ] ] );
if_最终生成了一个函数对象,它还有一个.else_对象,这个对象也是一个函数对象,可以接收任何函数对象。于是,这样就被层层包含起来,形成了上面的奇观。
std::for_each(vec.begin(), vec.end(), switch_(arg1) [ case_<1>(std::cout<<arg1<<":"<<val("one") << "\n"), case_<2>(std::cout<<arg1<<":"<<val("two") << "\n"), default_(std::cout<<arg1<<":"<<val("other value") << "\n") ] );注意default_后面是不加","的。
int value; std::for_each(vec.begin(), vec.end(), ( ref(value) = arg1, while_(ref(value)--) [ std::cout<<ref(value)<<"," ], std::cout << val("\n") ) );我用了ref(value)来作为临时变量。
int iii; std::for_each(vec.begin(), vec.end(), ( for_(ref(iii) = 0, ref(iii) < arg1, ++ ref(iii)) [ std::cout << arg1 << ", " ], std::cout << val("\n") ) );
以上的介绍是浅尝辄止,phoenix还有很多高级的东西未曾涉及,有兴趣的读者可以看boost相关内容。
phoenix让我重新认识了C++的模板。C++的模板是C++元编程的重要利器。它甚至一定程度上改变了C++语言的语法。
不过,个人觉得,phoenix也有些过度设计。其实,语句部分,可以通过编写专门的函数来实现。这对大多数人来说,也就是多敲几行代码的问题。
我觉得有价值的是phoenix对函数的包装,使得C++的函数具备了懒计算的能力。
懒计算避免了我们定义N多接口,以及和N多接口配合的N^N的类工厂和派生类。
使用FP编程,不必像OO编程那样,设计者为了保证接口的兼容性,绞尽脑汁的设计接口;使用者不必为了实现一个简单的功能,派生一大堆类,和一大堆工厂。
设计者根据需要,要求传递函数对象即可;使用者只需要包装一个自己的实现给它使用,一切都搞定了。