跳转到内容

更多 C++ 习语/表达式模板

来自维基教科书,开放的书籍,开放的世界

表达式模板

[编辑 | 编辑源代码]
  • 在 C++ 中创建一个领域特定嵌入语言 (DSEL)
  • 支持 C++ 表达式的延迟求值(例如,数学表达式),这些表达式可以在程序定义后的时间点执行。
  • 将表达式(而不是表达式的结果)作为参数传递给函数。

也称为

[编辑 | 编辑源代码]

领域特定语言 (DSL) 是一种开发程序的方式,其中要解决的问题是用更接近问题域的符号来表达,而不是过程语言提供的通常符号(循环、条件语句等)。领域特定嵌入语言 (DSEL) 是 DSL 的一种特殊情况,其中符号被嵌入到宿主语言(例如 C++)中。基于 C++ 的 DSL 的两个突出例子是Boost Spirit 解析器框架Blitz++ 科学计算库。Spirit 提供了一种将 EBNF 语法直接写入 C++ 程序的符号,而 Blitz++ 允许一种对矩阵执行数学运算的符号。显然,C++ 本身没有提供这种符号。使用这种符号的主要好处是,程序直观地捕捉了程序员的意图,使程序更易读。它极大地降低了开发和维护成本。

那么,这些库(Spirit 和 Blitz++)是如何实现这种抽象级别上的飞跃的呢?答案是——你猜对了——表达式模板

表达式模板背后的关键思想是表达式的延迟求值。C++ 本身不支持表达式的延迟求值。例如,在下面的代码片段中,加法表达式 (x+x+x) 在调用函数 foo 之前执行。

int x;
foo(x + x + x); // The addition expression does not exist beyond this line.

函数 foo 实际上并不知道它接收的参数是如何计算的。加法表达式在第一次也是唯一一次求值后就不再存在了。C++ 的这种默认行为对于绝大多数现实世界程序来说是必要且足够的。但是,一些程序需要稍后再次求值表达式。例如,将数组中的每个整数都乘以 3。

int expression (int x)
{
  return x + x + x; // Note the same expression.
}
// .... Lot of other code here
const int N = 5;
double A[N] = { 1, 2, 3, 4, 5};

std::transform(A, A+N, A, std::ptr_fun(expression)); // Triples every integer.

这是在 C/C++ 中支持数学表达式延迟求值的传统方法。表达式被包装在一个函数中,该函数被作为参数传递。这种技术存在函数调用和创建临时变量的开销,而且通常,表达式在源代码中的位置与调用位置相差甚远,这会对可读性和可维护性产生负面影响。表达式模板通过内联表达式来解决这个问题,从而消除了对函数指针的需要,并将表达式和调用位置整合在一起。

解决方案和示例代码

[编辑 | 编辑源代码]

表达式模板使用递归类型组合 习语。递归类型组合使用包含其他相同模板实例作为成员变量的类模板的实例。相同模板的多次重复实例化产生了类型的抽象语法树 (AST)。递归类型组合已被用来创建线性类型列表以及以下示例中使用的二元表达式树。

#include <iostream>
#include <vector>

struct Var {
  double operator () (double v) { return v; }
};

struct Constant {
  double c;
  Constant (double d) : c (d) {}
  double operator () (double) { return c; }
};

template < class L, class H, class OP >
struct DBinaryExpression {
  L l_;
  H h_;
  DBinaryExpression (L l, H h) : l_ (l), h_ (h) {}
  double operator () (double d) { return OP::apply (l_ (d), h_(d)); }
};

struct Add {
  static double apply (double l, double h) { return l + h; }
};

template < class E >
struct DExpression {
  E expr_;
  DExpression (E e) : expr_ (e) {}
  double operator() (double d) { return expr_(d);  }
};

template < class Itr, class Func >
void evaluate (Itr begin, Itr end, Func func) 
{
  for (Itr i = begin; i != end; ++i)
    std::cout << func (*i) << std::endl;
}

int main (void)
{
  typedef DExpression <Var> Variable;
  typedef DExpression <Constant> Literal;
  typedef DBinaryExpression <Variable , Literal, Add> VarLitAdder;
  typedef DExpression <VarLitAdder> MyAdder;

  Variable x ((Var()));
  Literal l (Constant (50.00));
  VarLitAdder vl_adder(x, l);
  MyAdder expr (vl_adder);

  std::vector <double> a;
  a.push_back (10);
  a.push_back (20);

  // It is (50.00 + x) but does not look like it.
  evaluate (a.begin(), a.end(), expr);

  return 0;
}

这里,与组合模式的类比非常有用。模板 DExpression 可以被认为是组合模式中的抽象基类。它捕获接口中的共性。在表达式模板中,公共接口是重载的函数调用运算符。DBinaryExpression 是一个真实的组合体,也是一个适配器,它将 Add 的接口适配为 DExpression 的接口。常量和 Var 是两种不同类型的叶节点。它们也坚持 DExpression 的接口。DExpression 将 DBinaryExpression、Constant 和 Var 的复杂性隐藏在一个统一的接口后面,使它们能够协同工作。任何二元运算符都可以代替 Add,例如 Divide、Multiply 等。

上面的示例没有显示递归类型是如何在编译时生成的。此外,expr 看起来并不像数学表达式,但它确实是。下面的代码展示了如何使用以下重载的 + 运算符的重复实例化来递归地组合类型。

template< class A, class B >
DExpression<DBinaryExpression<DExpression<A>, DExpression<B>, Add> >
operator + (DExpression<A> a, DExpression<B> b)
{
  typedef DBinaryExpression <DExpression<A>, DExpression<B>, Add> ExprT;
  return DExpression<ExprT>(ExprT(a,b));
}

上面的重载运算符 + 做了两件事——它添加了语法糖并启用了递归类型组合,受编译器限制的约束。因此,它可以用来替换对 evaluate 的调用,如下所示

evaluate (a.begin(), a.end(), x + l + x); 
/// It is (2*x + 50.00), which does look like a mathematical expression.

已知用途

[编辑 | 编辑源代码]
[编辑 | 编辑源代码]

参考资料

[编辑 | 编辑源代码]
华夏公益教科书