proto 再探

前面简单的看了一些 proto 的资料,也自己重新学习了一下利用 CRTP 写 expression template 的要点。这里准备对 boost.proto 进行一些更深入的了解。设计一个 DSL 的第一步就是把语言里面涉及到的 terminal 抓出来。

terminal

通过 proto 声明一个 terminal 非常简单。默认情况下,proto 为我们生成了对应的各种 operator,这些 operator 通常返回的仍然是 expression 对象,这时我们可以容易的表述出各种神奇的东西,但是完全没有意义。我们可以将任意的 expression 通过 boost::proto::display_expr 显示出来。

template <int> struct arg {} ;
boost::proto::teriminal<arg<0> >::type const _1 = {{}} ;
boost::proto::teriminal<arg<1> >::type const _2 = {{}} ;

这里的 arg 仅仅起到一个 tag 的作用(即表示是两个不同的类名),这样我们获得的两个 terminal 就可以用来进行各种表达(只要合乎 C++ 语法就行)。比较常见的做法都是类似,比如定义一个 xxx_ 的空 struct,然后定义一个 terminal 叫 xxx。对常数也可以使用 literal 模板,如

boost::proto::literal<int> zero = 0 ;

除了 operator 的重载,proto 也提供了(lazy)函数的重载。这时传递给 terminal 模板的是一个函数指针,如

boost::proto::terminal<double (*) (double)>::type sin = {&std::sin} ;

这样我们甚至可以将三角函数组织成一个 expression tree,从而进行优化,比如

boost::proto::terminal<double (*) (double)>::type const sin = {&std::sin} ;
boost::proto::terminal<double (*) (double)>::type const cos = {&std::cos} ;
boost::proto::display_expr (2 * sin(_1) * cos (_1)) ;
boost::proto::display_expr (sin(2 * _1)) ;

但这种仅仅对函数指针有效,更多的时候我们需要处理 functor,比如我们将 std::pow 进行简单的封装获得的 functor,这时我们需要通过 boost::proto::function 计算返回值,并且注意为了让其参数(可能传入是一个 terminal 也可能不是)能够变成 terminal,我们使用了 as_child 这个模板(类似还有 as_expr,前者传引用,后者传值),

template <int I>
struct pow_fun {
  typedef double result_type ;
  result_type
  operator() (double a) {
    return std::pow (a, I) ;
  }
} ;

template<int I, class Expr>
typename boost::proto::function<
  typename boost::proto::terminal<pow_fun<I> >::type,
  typename boost::proto::result_of::as_child<Expr const>::type
>::type const
pow (Expr const& e) {
  typedef typename boost::proto::function<
    typename boost::proto::terminal<pow_fun<I> >::type,
    typename boost::proto::result_of::as_child<Expr const>::type
  >::type const result_type ;
  result_type result = {{{}}, boost::proto::as_child (e)} ;
  return result ;
} ;

更简单的策略是使用 make_expr 这个函数,

template<int I, class Expr>
typename boost::proto::result_of::make_expr<
  boost::proto::tag::function, pow_fun<I>, Expr const&
>::type const
pow (Expr const& e) {
  return boost::proto::make_expr<bp::tag::function> (
    pow_fun<I> (), boost::ref (e)
  ) ;
} ;

domain

我们学会了声明 terminal 之后,就需要将一个 expression 能在一定的环境中具体的表述对应的意义,这就是 DSL 中所想表达的 domain 的含义。通常每个 domain 也会声明一个 struct,它继承于 boost::proto::domain 模板,这个模板通过 generator 和可缺省的 grammar 描述了什么样的 expression 在此 domain 中合法。generator 也是一个模板,它可以对这个 domain 中的 expression 对象处理。

因此我们先确定这个 domain 中 expression 是什么样子的(即公用的 interface 是什么),这里很明显使用了 CRTP,我们从 boost::proto::extends 模板继承(通过 terminal 等产生的虽然是 boost::proto::expr 模板,但是我们不能直接继承它),我们可在这个 expression 中也通过宏定义一些常见的 operator,方便使用。继承会导致丧失 POD,更好地方式是直接使用 BOOST_PROTO_EXTENDS 宏(包括 BASIC_EXTENDS,_ASSIGN,_SUBSCRIPT,_FUNCTION)。

// for terminal types
template <int I> struct arg {} ;
// context used in eval ()
struct calculator_context
  : boost::proto::callable_context<calculator_context const> {
  std::vector<double> args ;
  typedef double result_type ;

  template <int I> inline double
  operator () (boost::proto::tag::terminal, arg<I>) const {
    return args [I] ;
  }
} ;
// forward declaration of the expression
template <class Expr> struct calculator ;
// the definition of the domain
struct calculator_domain : boost::proto::domain<boost::proto::pod_generator<calculator> > {} ;
// what is a calculator expression
template <class Expr>
struct calculator {
  typedef double result_type ;

  BOOST_PROTO_BASIC_EXTENDS(Expr, calculator<Expr>, calculator_domain)
  BOOST_PROTO_EXTENDS_SUBSCRIPT()
  BOOST_PROTO_EXTENDS_ASSIGN()

  inline double
  operator () (double a = 0, double b = 0) const {
    calculator_context ctx ;
    ctx.args.push_back (a) ;
    ctx.args.push_back (b) ;
    return boost::proto::eval (*this, ctx) ;
  }
} ;

calculator<boost::proto::terminal<arg<0> >::type> const _1 = {{{}}};
calculator<boost::proto::terminal<arg<1> >::type> const _2 = {{{}}};

在每个 domain 里面可以覆盖对应的 as_child 和 as_expr 方法。另外在 domain 的定义部分,我们可以为 domain 模板加上第三个参数表示其父 domain,这样就能将 domain 组合起来了。

grammar

Grammar 的作用就是用来规定哪些是合法的 DSL 语法,通常使用如下的方式定义,

struct caculator_grammar : boost::proto::or_<
  boost::proto::plus<caculator_grammar, caculator_grammar>,
  boost::proto::minus<caculator_grammar, caculator_grammar>,
  boost::proto::multiplies<caculator_grammar, caculator_grammar>,
  boost::proto::divides<caculator_grammar, caculator_grammar>,
  boost::proto::terminal<boost::proto::_>
> {} ;

将现有类 protofy

这个是一个很有意思的事情,有时候写到一半发现其实自己需要的是 expression template,那么我们可以 leverage boost.proto 的能力。首先我们可以利用 traits 注入到原来已有的容器、类上,让他们称为 terminal,这只需要简单在对应的 namespace 里面通过 template 特化,将对应类型弄成 true_type,

namespace some_name {
  template <class T>
  struct is_terminal : boost::mpl::false_ {} ;

  template <>
  struct is_terminal<vector> : boost::mpl::true_ {} ;

  // ... many more

  BOOST_PROTO_DEFINE_OPERATORS(is_terminal, boost::proto::default_domain)
}

这一部分基本上对应于 boost.proto 文档的 FE 部分,另外参考了这个系列的前 5 篇 blog(第一篇见这里)。下面我们将讨论一些更高级的内容。

——————
Now Sarai Abram’s wife bore him no children: and she had an handmaid, an Egyptian, whose name was Hagar.

Advertisements
proto 再探

一个有关“proto 再探”的想法

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s