这篇文章是我学习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 namespace boost::phoenix,而要直接使用using boost::phoenix::val, ....,如

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;

为什么不要直接使用using namespace boost::phoenix呢?因为这样会带来不可预知的问题。这是我在实践中发现的。boost的宏BOOST_PHOENIX_ADAPT_FUNCTION_NULLARY以及类似的宏,会出现编译错误。真实的原因是什么,没有细致考究。

另外一个原因是,要防止不必要的命名污染,因为phoenix用了很多和boost库冲突的名称,这些在使用的时候,很容易造成问题。

基础函数对象

values

包含头文件:

#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的真正威力,它让求值推迟到需要的时候。在普通编程中,我们必须通过类和接口才能完成。

References

包含头文件

#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与val都是可以延迟求值的,但是,不同的是,ref相当于 int& 和 char const*& 的调用。

因此,上面 (++ref(i))()的返回值是4,而且,变量i的值也将变为4.

references是phoenix的函数对象和外部变量交换数据的桥梁。

Arguments

还记得boost中有_1, _2, _3, ...这些东西吗?在phoenix中有一种类似的 arg1, arg2, arg3, ...。他们有相似的作用,但是arg1 事实上是函数对象。

包含头文件

#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;

输出的结果是
3, "Hello world"。 

  • arg1接收1个以上的参数,然后返回第1个参数
  • arg2接受2个以上的参数,然后返回第2个参数
  • arg3接受3个以上的参数,然后返回第3个参数

依次类推。

那么,这样的东西有什么用呢?它实际上是用来提取参数的。arg1提取第一个参数,arg2提取第二个参数,....

比如,我们有一个函数

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"。

Lazy Operators

操作符也可以生成函数对象。

头文件

#include <boost/phoenix/operator.hpp>

无需命名空间

看个例子

std::find_if(vec.begin(), vec.end(), arg1 %2 == 1);

find_if的功能是查找第一个符合条件的对象,然后返回。它要求最后一个参数为一个函数或者函数对象。那么 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->member

arg1->*&A::member实现一个对A对象的访问操作。

Lazy Statements

懒惰语句。

头文件

#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_;
....

我们看看if_的例子

    std::for_each(vec.begin(), vec.end(),
if_(arg1 > 5)
[
std::cout << arg1 << ","
]
);

虽然看起来很奇怪,但是,它的确是C++的语法。这个里面也充斥了函数对象。我们可以这样看

if_.operator()(
operator > (arg1, 5)
) .operator[](
operator<<(
operator<<(std::cout, arg1)
, ",")
)

Lazy Statement还有很多类似的语法。它的目的是为了模拟C++的语法。
用C++模拟C++

它用逗号代替分号,模拟语句序列,如

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, New, Delete, Casts

可以重载类的这些实现:

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

BOOST_PHOENIX_ADAPT_FUNCTION

它的语法是

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;

myplus(arg1, arg2, 3) 生成一个函数对象,这个函数对象接收两个整数参数。

至于细节,了解不是很多,不管怎么样,用就是了。

针对函数对象的宏

BOOST_PHOENIX_ADAPT_CALLABLE_NULLARY

BOOST_PHOENIX_ADAPT_CALLABLE

在使用上,同FUNCTION对应的函数,但是,它是针对函数对象的。

namespace demo
{
struct foo2 {
typedef int result_type;
int operator()() const
{
return 42;
}
};
} BOOST_PHOENIX_ADAPT_CALLABLE_NULLARY(foo2, demo::foo2)

声明方法和BOOST_PHOENIX_ADAPT_FUNCTION_NULLARY 几乎是一样的,但是它不需要给出返回值。

值得注意的是,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_语句

我们开头看到的,就是一个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_对象,这个对象也是一个函数对象,可以接收任何函数对象。于是,这样就被层层包含起来,形成了上面的奇观。

switch_语句

看这个例子
    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_后面是不加","的。

case_和default_都是函数对象。

while_语句

例子:
    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)来作为临时变量。

这样的代码写起来,和对应的C++代码很相似。但是它实际上是一堆函数对象的组合。它是延迟加载的,这一点很重要。
与之相似的还有do_while_循环。


for_语句

    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")
)
);

无语了。


其他语句

另外还有,try_catch_和throw_语句,实现原理都差不多。

总结

以上的介绍是浅尝辄止,phoenix还有很多高级的东西未曾涉及,有兴趣的读者可以看boost相关内容。

phoenix让我重新认识了C++的模板。C++的模板是C++元编程的重要利器。它甚至一定程度上改变了C++语言的语法。

不过,个人觉得,phoenix也有些过度设计。其实,语句部分,可以通过编写专门的函数来实现。这对大多数人来说,也就是多敲几行代码的问题。

我觉得有价值的是phoenix对函数的包装,使得C++的函数具备了懒计算的能力。

懒计算避免了我们定义N多接口,以及和N多接口配合的N^N的类工厂和派生类。

使用FP编程,不必像OO编程那样,设计者为了保证接口的兼容性,绞尽脑汁的设计接口;使用者不必为了实现一个简单的功能,派生一大堆类,和一大堆工厂。

设计者根据需要,要求传递函数对象即可;使用者只需要包装一个自己的实现给它使用,一切都搞定了。

boost 的函数式编程库 Phoenix入门学习的更多相关文章

  1. 函数式编程/lambda表达式入门

    函数式编程/lambda表达式入门 本篇主要讲解 lambda表达式的入门,涉及为什么使用函数式编程,以及jdk8提供的函数式接口 和 接口的默认方法 等等 1.什么是命令式编程 命令式编程就是我们去 ...

  2. Python函数式编程:从入门到走火入魔

    一行代码显示"爱心" >>> print]+(y*-)**-(x**(y*<= ,)]),-,-)]) Python函数式编程:从入门到走火入魔 # @fi ...

  3. Reactive UI -- 反应式编程UI框架入门学习(二)

    前文Reactive UI -- 反应式编程UI框架入门学习(一)  介绍了反应式编程的概念和跨平台ReactiveUI框架的简单应用. 本文通过一个简单的小应用更进一步学习ReactiveUI框架的 ...

  4. python函数式编程-------python2.7教程学习【廖雪峰版】(五)

    2017年6月13日19:08:13 任务: 看完函数式编程 笔记: 该看:函数式编程1.函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解 ...

  5. 【java8新特性】01:函数式编程及Lambda入门

    我们首先需要先了解什么是函数式编程.函数式编程是一种结构化编程范式.类似于数学函数.它关注的重点在于数据操作.或者说它所提倡的思想是做什么,而不是如何去做. 自Jdk8中开始.它也支持函数式编程.函数 ...

  6. Reactive UI -- 反应式编程UI框架入门学习(一)

    推荐一个反应式编程的MVVM跨平台框架. 反应式编程 反应式编程是一种相对于命令式的编程范式,由函数式的组合声明来构建异步数据流.要理解这个概念,可以简单的借助Excel中的单元格函数. 上图中,A1 ...

  7. 类和对象:面向对象编程 - 零基础入门学习Python037

    类和对象:面向对象编程 让编程改变世界 Change the world by program 经过上节课的热身,相信大家对类和对象已经有了初步的认识,但似乎还是懵懵懂懂:好像面向对象编程很厉害,但不 ...

  8. 翻译连载 | 附录 C:函数式编程函数库-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

    原文地址:Functional-Light-JS 原文作者:Kyle Simpson-<You-Dont-Know-JS>作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTM ...

  9. 【大前端攻城狮之路】JavaScript函数式编程

    转眼之间已入五月,自己毕业也马上有三年了.大学计算机系的同学大多都在北京混迹,大家为了升职加薪,娶媳妇买房,熬夜加班跟上线,出差pk脑残客户.同学聚会时有不少兄弟已经体重飙升,开始关注13号地铁线上铺 ...

随机推荐

  1. EditText 软键盘

    EditText 软键盘 package brother.eighteen.demoedittext; import android.content.Context; import android.t ...

  2. Android项目实战手机安全卫士(02)

    目录 项目结构图 源代码 运行结果 项目源代码 项目结构图 源代码 清单 01.  SplashActivity.java package com.coderdream.mobilesafe.acti ...

  3. 初探 插头DP

    因为这题,气得我火冒三丈! 这数据是不是有问题啊!我用cin代替scanf后居然就AC了(本来一直卡在Test 18)!导致我调(对)试(排)了一个小时!! UPD:后来细细想想,会不会是因为scan ...

  4. 基于JSP+SERVLET的新闻发布系统(二)

    接下来讲解的是通过AJAX验证用户名是否已经添加 用户名: <input type="text" name="userName" id="use ...

  5. 『WPF』DataGrid的使用

    原文 『WPF』DataGrid的使用 几点说明 这里主要是参考了MSDN中关于DataGrid的说明 这里只会简单说明在WPF中,DataGird最简单的使用方法 对于MSDN中的翻译不会很详细,也 ...

  6. inner join、left join、right join中where和and的作用

    inner join.left join.right join中where和and的作用 .内连接(自然连接): 只有两个表相匹配的行才能在结果集中出现 2.外连接: 包括  (1)左外连接 (左边的 ...

  7. perl学习(10) 字符串处理函数和排序

    1.1.index Perl 查找子串第一次在大字符串中出现的地方,返回第一个字符的位置. . . my $stuff = “Howordy world!”; my $where3 = index($ ...

  8. [置顶] NS2中TCP拥塞控制仿真过程中盲点解析

    最近利用NS2做TCP拥塞控制协议的仿真,发现很多变量的方法含义都是解释的不清楚,给核心模块修改带来很多麻烦,所以决定用最准确的语言解释成员变量.方法,术语等的含义.限于个人水平,若有错误请留言指正! ...

  9. 数据库元数据MetaData

    本篇介绍数据库方面的元数据(MetaData)的有关知识.元数据在建立框架和架构方面是特别重要的知识,再下一篇我们仿造开源数据库工具类DbUtils就要使用数据库的元数据来创建自定义JDBC框架. 在 ...

  10. Eclipse用法和技巧十一:分栏显示

    在编码的时候,有时候需要同时看到两个文件的代码.或者在代码走读的时候,能同时看到两个文件的代码能加快我们对代码的理解.来看看如何在eclipse中同时显示两个文件的代码.        步骤一:拖住一 ...