大多数编译器都有前端和后端。前端将输入程序的文本解析成某种中间形式,例如抽象语法树,后端则获取中间形式并从中生成可执行文件。
用 Proto 构建的库本质上是嵌入式领域特定语言 (EDSL) 的编译器。它也有前端、中间形式和后端。前端由符号(又名终结符)、成员、运算符和函数组成,这些构成了 EDSL 用户可见的方面。后端由评估上下文和转换组成,它们为前端生成的表达式模板赋予意义和行为。介于两者之间的是中间形式:表达式模板本身,它在非常真实的意义上是一棵抽象语法树。
要使用 Proto 构建库,您首先要决定您的接口是什么;也就是说,您将为您的领域设计一种编程语言,并使用 Proto 提供的工具构建前端。然后,您将通过编写评估上下文和/或转换来设计后端,这些上下文和/或转换接受表达式模板并使用它们执行有趣的操作。
本用户指南的组织结构如下。在入门指南之后,我们将介绍 Proto 提供的用于定义和操作编译器的三个主要部分的工具
之后,您可能有兴趣查看一些示例,以更好地了解所有部分如何组合在一起。
您可以通过下载 Boost(Proto 在 1.37 及更高版本中)或访问 SourceForge.net 上的 Boost SVN 存储库来获取 Proto。只需访问 http://svn.boost.org/trac/boost/wiki/BoostSubversion 并按照那里的说明进行匿名 SVN 访问。
Proto 是一个仅头文件的模板库,这意味着您无需更改构建脚本或链接到任何单独的 lib 文件即可使用它。您只需执行 #include <boost/proto/proto.hpp>
即可。或者,您可能决定只包含 Proto 的核心 (#include <boost/proto/core.hpp>
) 以及您恰好使用的任何上下文和转换。
Proto 依赖于 Boost。您必须使用 Boost 版本 1.34.1 或更高版本,或者 SVN trunk 中的版本。
目前,已知 Boost.Proto 在以下编译器上工作
![]() |
注意 |
---|---|
请将任何问题、意见和错误报告发送至 eric <at> boostpro <dot> com。 |
Proto 是一个大型库,可能与您以前使用过的任何库都非常不同。Proto 使用一些一致的命名约定,以便于导航,下面将对此进行描述。
Proto 的所有函数都在 boost::proto
命名空间中定义。例如,在 boost::proto
中定义了一个名为 value()
的函数,它接受一个终结符表达式并返回终结符的值。
Proto 定义了与 Proto 的每个自由函数相对应的元函数。元函数用于计算函数的返回类型。Proto 的所有元函数都位于 boost::proto::result_of
命名空间中,并且与它们对应的函数具有相同的名称。例如,有一个类模板 boost::proto::result_of::value<>
,您可以使用它来计算 boost::proto::value()
函数的返回类型。
Proto 定义了所有自由函数的函数对象等价物。(函数对象是定义 operator()
成员函数的类类型的实例。)Proto 的所有函数对象类型都在 boost::proto::functional
命名空间中定义,并且与其对应的自由函数具有相同的名称。例如,boost::proto::functional::value
是一个类,它定义了一个函数对象,该对象执行与 boost::proto::value()
自由函数相同的事情。
Proto 还定义了原始转换——可用于组合更大的转换以操作表达式树的类类型。Proto 的许多自由函数都有相应的原始转换。这些都位于 boost::proto
命名空间中,并且它们的名称带有前导下划线。例如,与 value()
函数对应的转换称为 boost::proto::_value
。
下表总结了以上讨论
表 30.1. Proto 命名约定
实体 |
示例 |
---|---|
自由函数 |
|
元函数 |
|
函数对象 |
|
转换 |
|
下面是一个非常简单的程序,它使用 Proto 构建表达式模板,然后执行它。
#include <iostream> #include <boost/proto/proto.hpp> #include <boost/typeof/std/ostream.hpp> using namespace boost; proto::terminal< std::ostream & >::type cout_ = { std::cout }; template< typename Expr > void evaluate( Expr const & expr ) { proto::default_context ctx; proto::eval(expr, ctx); } int main() { evaluate( cout_ << "hello" << ',' << " world" ); return 0; }
该程序输出以下内容
hello, world
该程序构建一个表示输出操作的对象,并将其传递给 evaluate()
函数,然后该函数执行它。
表达式模板的基本思想是重载所有运算符,以便它们不是立即评估表达式,而是构建表达式的树状表示,以便稍后可以对其进行评估。对于表达式中的每个运算符,必须至少对一个操作数进行 Proto 化,以便找到 Proto 的运算符重载。在表达式中...
cout_ << "hello" << ',' << " world"
... Proto 化的子表达式是 cout_
,它是 std::cout
的 Proto 化。 cout_
的存在“感染”了表达式,并将 Proto 的树构建运算符重载纳入考虑范围。表达式中的任何字面量都通过将它们包装在 Proto 终结符中进行 Proto 化,然后在将它们组合成更大的 Proto 表达式。
一旦 Proto 的运算符重载构建了表达式树,就可以通过遍历树来延迟评估表达式。这就是 proto::eval()
所做的事情。它是一个通用的树遍历表达式评估器,其行为可通过上下文参数自定义。proto::default_context
的使用为表达式中的运算符分配了标准含义。(通过使用不同的上下文,您可以为表达式中的运算符赋予不同的语义。默认情况下,Proto 不会对运算符的实际含义做任何假设。)
在我们继续之前,让我们使用上面的示例来说明 Proto 的一个重要的设计原则。在hello world示例中创建的表达式模板是完全通用和抽象的。它不以任何方式绑定到任何特定的领域或应用程序,并且它本身没有任何特定的含义或行为,除非它在上下文中进行评估。表达式模板实际上只是异构树,它们在一个领域中可能意味着某种东西,而在另一个领域中则完全意味着其他东西。
正如我们稍后将看到的,有一种方法可以创建不是纯粹抽象的 Proto 表达式树,并且这些树具有独立于任何上下文的含义和行为。还有一种方法可以控制为您的特定领域重载哪些运算符。但这不是默认行为。稍后我们将看到为什么默认通常是一件好事。
“Hello, world”很好,但它并没有让您走得太远。让我们使用 Proto 为延迟评估的计算器构建 EDSL(嵌入式领域特定语言)。我们将看到如何定义您的迷你语言中的终结符,如何将它们组合成更大的表达式,以及如何定义评估上下文,以便您的表达式可以执行有用的工作。完成之后,我们将拥有一个迷你语言,该语言将允许我们声明一个延迟评估的算术表达式,例如 (_2 - _1) / _2 * 100
,其中 _1
和 _2
是在评估表达式时要传入的值的占位符。
首要任务是定义占位符 _1
和 _2
。为此,我们将使用 proto::terminal<>
元函数。
// Define a placeholder type template<int I> struct placeholder {}; // Define the Protofied placeholder terminals proto::terminal<placeholder<0> >::type const _1 = {{}}; proto::terminal<placeholder<1> >::type const _2 = {{}};
初始化乍一看可能有点奇怪,但是这样做是有充分理由的。上面的对象 _1
和 _2
不需要运行时构造——它们是静态初始化的,这意味着它们本质上是在编译时初始化的。有关更多信息,请参见静态初始化部分中的原理附录。
现在我们有了终结符,我们可以使用 Proto 的运算符重载将这些终结符组合成更大的表达式。因此,例如,我们可以立即说出类似以下内容
// This builds an expression template (_2 - _1) / _2 * 100;
这将创建一个表达式树,其中每个运算符都有一个节点。结果对象的类型很大且复杂,但是我们现在对此并不太感兴趣。
到目前为止,该对象只是一棵表示表达式的树。它没有行为。特别是,它还不是计算器。下面我们将看到如何通过定义评估上下文使其成为计算器。
毫无疑问,您希望您的表达式模板实际做一些事情。一种方法是定义评估上下文。上下文就像一个函数对象,它将行为与表达式树中的节点类型相关联。以下示例应使其清晰明了。下面对其进行了解释。
struct calculator_context : proto::callable_context< calculator_context const > { // Values to replace the placeholders std::vector<double> args; // Define the result type of the calculator. // (This makes the calculator_context "callable".) typedef double result_type; // Handle the placeholders: template<int I> double operator()(proto::tag::terminal, placeholder<I>) const { return this->args[I]; } };
在 calculator_context
中,我们通过定义函数调用运算符的适当重载来指定 Proto 应如何评估占位符终结符。对于表达式树中的任何其他节点(例如,算术运算或非占位符终结符),Proto 将以“默认”方式评估表达式。例如,二进制加法节点的评估方式是首先评估左右操作数,然后将结果相加。Proto 的默认评估器使用 Boost.Typeof 库来计算返回类型。
现在我们有了计算器的评估上下文,我们可以使用它来评估我们的算术表达式,如下所示
calculator_context ctx; ctx.args.push_back(45); // the value of _1 is 45 ctx.args.push_back(50); // the value of _2 is 50 // Create an arithmetic expression and immediately evaluate it double d = proto::eval( (_2 - _1) / _2 * 100, ctx ); // This prints "10" std::cout << d << std::endl;
稍后,我们将看到如何定义更有趣的评估上下文和表达式转换,这些上下文和转换使您可以完全控制表达式的评估方式。
我们的计算器 EDSL 已经非常有用,对于许多 EDSL 场景,不再需要更多功能。但是让我们继续。想象一下,如果所有计算器表达式都重载了 operator()
,以便可以将它们用作函数对象,那将有多好。我们可以通过创建一个计算器域并告知 Proto 计算器域中的所有表达式都具有额外的成员来做到这一点。以下是如何定义计算器域
// Forward-declare an expression wrapper template<typename Expr> struct calculator; // Define a calculator domain. Expression within // the calculator domain will be wrapped in the // calculator<> expression wrapper. struct calculator_domain : proto::domain< proto::generator<calculator> > {};
calculator<>
类型将是一个表达式包装器。它的行为将与它包装的表达式完全相同,但是它将具有我们将定义的额外成员函数。 calculator_domain
是告知 Proto 关于我们的包装器的内容。它在下面的 calculator<>
的定义中使用。请继续阅读以获取描述。
// Define a calculator expression wrapper. It behaves just like // the expression it wraps, but with an extra operator() member // function that evaluates the expression. template<typename Expr> struct calculator : proto::extends<Expr, calculator<Expr>, calculator_domain> { typedef proto::extends<Expr, calculator<Expr>, calculator_domain> base_type; calculator(Expr const &expr = Expr()) : base_type(expr) {} typedef double result_type; // Overload operator() to invoke proto::eval() with // our calculator_context. double operator()(double a1 = 0, double a2 = 0) const { calculator_context ctx; ctx.args.push_back(a1); ctx.args.push_back(a2); return proto::eval(*this, ctx); } };
calculator<>
结构是一个表达式扩展。它使用 proto::extends<>
来有效地向表达式类型添加其他成员。当从较小的表达式组合成较大的表达式时,Proto 会记录较小的表达式所属的域。较大的表达式位于同一域中,并自动包装在域的扩展包装器中。
剩下要做的就是将我们的占位符放在计算器域中。我们通过将它们包装在我们的 calculator<>
包装器中来做到这一点,如下所示
// Define the Protofied placeholder terminals, in the // calculator domain. calculator<proto::terminal<placeholder<0> >::type> const _1; calculator<proto::terminal<placeholder<1> >::type> const _2;
任何包含这些占位符的较大表达式都将自动包装在 calculator<>
包装器中,并具有我们的 operator()
重载。这意味着我们可以像下面这样将它们用作函数对象。
double result = ((_2 - _1) / _2 * 100)(45.0, 50.0); assert(result == (50.0 - 45.0) / 50.0 * 100));
由于计算器表达式现在是有效的函数对象,因此我们可以将它们与标准算法一起使用,如下所示
double a1[4] = { 56, 84, 37, 69 }; double a2[4] = { 65, 120, 60, 70 }; double a3[4] = { 0 }; // Use std::transform() and a calculator expression // to calculate percentages given two input sequences: std::transform(a1, a1+4, a2, a3, (_2 - _1) / _2 * 100);
现在,让我们使用计算器示例来探索 Proto 的其他一些有用的功能。
您可能已经注意到,您不必定义重载的 operator-()
或 operator/()
—— Proto 为您定义了它们。实际上,Proto 为您重载了所有运算符,即使它们在您的领域特定语言中可能没有任何意义。这意味着可能会创建在您的域中无效的表达式。您可以使用 Proto 通过定义您的领域特定语言的语法来检测无效表达式。
为简单起见,假设我们的计算器 EDSL 仅应允许加法、减法、乘法和除法。涉及任何其他运算符的任何表达式均无效。使用 Proto,我们可以通过定义计算器 EDSL 的语法来说明此要求。它看起来如下
// Define the grammar of calculator expressions struct calculator_grammar : proto::or_< proto::plus< calculator_grammar, calculator_grammar > , proto::minus< calculator_grammar, calculator_grammar > , proto::multiplies< calculator_grammar, calculator_grammar > , proto::divides< calculator_grammar, calculator_grammar > , proto::terminal< proto::_ > > {};
您可以将上面的语法理解为:如果表达式树是二进制加法、减法、乘法或除法节点,其中两个子节点也符合计算器语法;或者,如果它是终结符,则表达式树符合计算器语法。在 Proto 语法中,proto::_
是一个通配符,它匹配任何类型,因此 proto::terminal< proto::_ >
匹配任何终结符,无论是占位符还是字面量。
![]() |
注意 |
---|---|
此语法实际上比我们希望的要宽松一些。只有可转换为 double 的占位符和字面量才是有效的终结符。稍后我们将看到如何在 Proto 语法中表达类似的内容。 |
定义 EDSL 的语法后,您可以使用 proto::matches<>
元函数来检查给定的表达式类型是否符合语法。例如,我们可能会将以下内容添加到我们的 calculator::operator()
重载中
template<typename Expr> struct calculator : proto::extends< /* ... as before ... */ > { /* ... */ double operator()(double a1 = 0, double a2 = 0) const { // Check here that the expression we are about to // evaluate actually conforms to the calculator grammar. BOOST_MPL_ASSERT((proto::matches<Expr, calculator_grammar>)); /* ... */ } };
BOOST_MPL_ASSERT()
行的添加在编译时强制执行,我们只评估符合计算器 EDSL 语法的表达式。使用 Proto 语法、 proto::matches<>
和 BOOST_MPL_ASSERT()
,当 EDSL 的用户不小心误用您的 EDSL 时,很容易为他们提供简短且可读的编译时错误。
![]() |
注意 |
---|---|
|
语法和 proto::matches<>
使检测用户何时创建了无效表达式并发出编译时错误成为可能。但是,如果您想从一开始就阻止用户创建无效表达式怎么办?通过将语法和域一起使用,您可以禁用任何会创建无效表达式的 Proto 运算符重载。这就像在定义域时指定 EDSL 的语法一样简单,如下所示
// Define a calculator domain. Expression within // the calculator domain will be wrapped in the // calculator<> expression wrapper. // NEW: Any operator overloads that would create an // expression that does not conform to the // calculator grammar is automatically disabled. struct calculator_domain : proto::domain< proto::generator<calculator>, calculator_grammar > {};
我们唯一更改的是,在定义 calculator_domain
时,我们将 calculator_grammar
添加为 proto::domain<>
模板的第二个模板参数。通过这个简单的添加,我们禁用了任何会创建无效计算器表达式的 Proto 运算符重载。
希望这能让您了解 Proto 可以为您做哪些事情。但这只是冰山一角。本用户指南的其余部分将更详细地描述所有这些功能和其他功能。
祝您元编程愉快!
这是有趣的部分:设计您自己的迷你编程语言。在本节中,我们将讨论使用 Proto 设计 EDSL 接口的细节。我们将介绍 EDSL 用户将用来编程的终结符和惰性函数的定义。我们还将讨论 Proto 的表达式模板构建运算符重载,以及在您的域中向表达式添加其他成员的方法。
正如我们在简介中的计算器示例中看到的那样,启动并运行 EDSL 的最简单方法是简单地定义一些终结符,如下所示。
// Define a literal integer Proto expression. proto::terminal<int>::type i = {0}; // This creates an expression template. i + 1;
借助一些终结符和 Proto 的运算符重载,您可以立即开始创建表达式模板。
定义终结符——使用聚合初始化——有时可能有点笨拙。Proto 为字面量提供了一个更易于使用的包装器,该包装器可用于构造 Proto 化的终结符表达式。它被称为 proto::literal<>
。
// Define a literal integer Proto expression. proto::literal<int> i = 0; // Proto literals are really just Proto terminal expressions. // For example, this builds a Proto expression template: i + 1;
还有一个 proto::lit()
函数,用于就地构造 proto::literal<>
。上面的表达式可以简单地写成
// proto::lit(0) creates an integer terminal expression proto::lit(0) + 1;
一旦我们有了一些 Proto 终结符,涉及这些终结符的表达式就会为我们构建表达式树。Proto 为 C++ 的每个可重载运算符在 boost::proto
命名空间中定义了重载。只要一个操作数是 Proto 表达式,运算的结果就是一个表示该运算的树节点。
![]() |
注意 |
---|---|
Proto 的运算符重载位于 |
由于 Proto 的运算符重载,我们可以说
-_1; // OK, build a unary-negate tree node _1 + 42; // OK, build a binary-plus tree node
在大多数情况下,这“开箱即用”,您无需考虑它,但少数运算符是特殊的,了解 Proto 如何处理它们可能会有所帮助。
Proto 也重载了 operator=
、 operator[]
和 operator()
,但这些运算符是表达式模板的成员函数,而不是 Proto 命名空间中的自由函数。以下是有效的 Proto 表达式
_1 = 5; // OK, builds a binary assign tree node _1[6]; // OK, builds a binary subscript tree node _1(); // OK, builds a unary function tree node _1(7); // OK, builds a binary function tree node _1(8,9); // OK, builds a ternary function tree node // ... etc.
对于前两行,赋值和下标,结果表达式节点应该是二元的,这应该不足为奇。毕竟,每个表达式中都有两个操作数。起初可能会令人惊讶的是,看起来像是没有参数的函数调用 _1()
实际上创建了一个带有一个子节点的表达式节点。子节点是 _1
本身。同样,表达式 _1(7)
有两个子节点:_1
和 7
。
由于这些运算符只能定义为成员函数,因此以下表达式无效
int i; i = _1; // ERROR: cannot assign _1 to an int int *p; p[_1]; // ERROR: cannot use _1 as an index std::sin(_1); // ERROR: cannot call std::sin() with _1
此外,C++ 对 operator->
的重载有特殊的规则,这使得它对于构建表达式模板毫无用处,因此 Proto 不会重载它。
Proto 为表达式类型重载了取地址运算符,因此以下代码创建了一个新的 unary address-of 树节点
&_1; // OK, creates a unary address-of tree node
它不返回 _1
对象的地址。但是,Proto 中有特殊的代码,使得 unary address-of 节点可以隐式转换为指向其子节点的指针。换句话说,以下代码可以工作,并且可以达到您可能期望的效果,但方式并非显而易见
typedef proto::terminal< placeholder<0> >::type _1_type; _1_type const _1 = {{}}; _1_type const * p = &_1; // OK, &_1 implicitly converted
如果我们将自己限制在仅使用终端和运算符重载,那么我们的嵌入式领域特定语言就不会很有表现力。想象一下,我们想用全套数学函数(如 sin()
和 pow()
)扩展我们的计算器 EDSL,我们可以像下面这样惰性地调用它们。
// A calculator expression that takes one argument // and takes the sine of it. sin(_1);
我们希望以上代码创建一个表示函数调用的表达式模板。当评估该表达式时,它应该导致函数被调用。(至少,这是我们希望计算器 EDSL 具有的函数调用的含义。)您可以非常简单地将 sin
定义如下。
// "sin" is a Proto terminal containing a function pointer proto::terminal< double(*)(double) >::type const sin = {&std::sin};
在上面,我们将 sin
定义为包含指向 std::sin()
函数的指针的 Proto 终端。现在我们可以将 sin
用作惰性函数。我们在导言中看到的 default_context
知道如何评估惰性函数。考虑以下代码
double pi = 3.1415926535; proto::default_context ctx; // Create a lazy "sin" invocation and immediately evaluate it std::cout << proto::eval( sin(pi/2), ctx ) << std::endl;
以上代码打印出
1
我不是三角学专家,但这在我看来是正确的。
我们可以写 sin(pi/2)
,因为 sin
对象(它是 Proto 终端)具有重载的 operator()()
,它构建一个表示函数调用的节点。sin(pi/2)
的实际类型实际上是这样的
// The type of the expression sin(pi/2): proto::function< proto::terminal< double(*)(double) >::type const & proto::result_of::as_child< double const >::type >::type
此类型进一步扩展为一个难看的节点类型,其 tag 类型为 proto::tag::function
,并且有两个子节点:第一个表示要调用的函数,第二个表示函数的参数。(节点标签类型描述了创建节点的操作。a + b
和 a - b
之间的区别在于前者具有标签类型 proto::tag::plus
,后者具有标签类型 proto::tag::minus
。标签类型是纯编译时信息。)
![]() |
注意 |
---|---|
在上面的类型计算中, |
重要的是要注意,包含函数指针的终端没有任何特别之处。任何 Proto 表达式都具有重载的函数调用运算符。考虑
// This compiles! proto::lit(1)(2)(3,4)(5,6,7,8);
乍一看可能很奇怪。它使用 proto::lit()
创建一个整数终端,然后像函数一样一遍又一遍地调用它。这是什么意思?谁知道呢?!您可以决定何时定义评估上下文或转换。但更多内容稍后介绍。
现在,如果我们想在我们的计算器 EDSL 中添加一个 pow()
函数,用户可以像下面这样调用它,该怎么办?
// A calculator expression that takes one argument // and raises it to the 2nd power pow< 2 >(_1);
上面描述的将 pow
作为包含函数指针的终端的简单技术在这里不起作用。如果 pow
是一个对象,那么表达式 pow< 2 >(_1)
不是有效的 C++。(好吧,从技术上讲它是;它的意思是,pow
小于 2,大于 (_1)
,这根本不是我们想要的。)pow
应该是一个真正的函数模板。但它必须是一个不寻常的函数:一个返回表达式模板的函数。
对于 sin
,我们依靠 Proto 提供重载的 operator()()
来为我们构建一个标签类型为 proto::tag::function
的表达式节点。现在我们需要自己这样做。和以前一样,该节点将有两个子节点:要调用的函数和函数的参数。
对于 sin
,要调用的函数是一个原始函数指针,它被包装在 Proto 终端中。在 pow
的情况下,我们希望它是一个包含 TR1 风格函数对象的终端。这将允许我们根据指数来参数化函数。下面是 std::pow
函数的简单 TR1 风格包装器的实现
// Define a pow_fun function object template< int Exp > struct pow_fun { typedef double result_type; double operator()(double d) const { return std::pow(d, Exp); } };
按照 sin
示例,我们希望 pow< 1 >( pi/2 )
具有这样的类型
// The type of the expression pow<1>(pi/2): proto::function< proto::terminal< pow_fun<1> >::type proto::result_of::as_child< double const >::type >::type
我们可以使用类似这样的代码编写 pow()
函数,但这很冗长且容易出错;很容易因为忘记在必要时调用 proto::as_child()
而引入细微的错误,从而导致代码看起来可以工作,但有时却不行。Proto 提供了一种更好的构建表达式节点的方法:proto::make_expr()
。
make_expr()
简化惰性函数Proto 提供了一个用于构建表达式模板的助手,称为 proto::make_expr()
。我们可以使用它简洁地定义 pow()
函数,如下所示。
// Define a lazy pow() function for the calculator EDSL. // Can be used as: pow< 2 >(_1) template< int Exp, typename Arg > typename proto::result_of::make_expr< proto::tag::function // Tag type , pow_fun< Exp > // First child (by value) , Arg const & // Second child (by reference) >::type const pow(Arg const &arg) { return proto::make_expr<proto::tag::function>( pow_fun<Exp>() // First child (by value) , boost::ref(arg) // Second child (by reference) ); }
关于上面的代码,需要注意一些事项。我们使用 proto::result_of::make_expr<>
来计算返回类型。第一个模板参数是我们正在构建的表达式节点的标签类型——在本例中为 proto::tag::function
。
proto::result_of::make_expr<>
的后续模板参数表示子节点。如果子类型还不是 Proto 表达式,它将自动使用 proto::as_child()
变为终端。诸如 pow_fun<Exp>
之类的类型会产生按值保存的终端,而诸如 Arg const &
之类的类型(请注意引用)表明结果应按引用保存。
在函数体中是 proto::make_expr()
的运行时调用。它紧密地反映了返回类型计算。proto::make_expr()
要求您将节点的标签类型指定为模板参数。函数的参数成为节点的子节点。当子节点应按值存储时,无需执行任何特殊操作。当子节点应按引用存储时,您必须使用 boost::ref()
函数包装参数。
就是这样!proto::make_expr()
是懒人创建惰性函数的方式。
在本节中,我们将全面学习域。特别是,我们将学习
在 Hello Calculator 部分,我们研究了如何使计算器表达式直接用作 STL 算法调用中的 lambda 表达式,如下所示
double data[] = {1., 2., 3., 4.}; // Use the calculator EDSL to square each element ... HOW? std::transform( data, data + 4, data, _1 * _1 );
如果您还记得,困难在于默认情况下 Proto 表达式本身没有有趣的特性。它们只是树。特别是,表达式 _1 * _1
不会有一个接受 double 并返回 double 的 operator()
,就像 std::transform()
所期望的那样——除非我们给它一个。为了使其工作,我们需要定义一个表达式包装器类型,该类型定义了 operator()
成员函数,并且我们需要将包装器与计算器域关联。
在 Proto 中,术语域指的是一种类型,该类型将该域中的表达式与表达式生成器关联。生成器只是一个函数对象,它接受一个表达式并对其执行某些操作,例如将其包装在表达式包装器中。
您还可以使用域将表达式与语法关联。当您指定域的语法时,Proto 确保它在该域中生成的所有表达式都符合域的语法。它通过禁用任何会创建无效表达式的运算符重载来实现这一点。
赋予您的计算器表达式额外行为的第一步是定义一个计算器域。计算器域中的所有表达式都将充满计算器特性,我们稍后会看到。
// A type to be used as a domain tag (to be defined below) struct calculator_domain;
当扩展 proto::expr<>
类型时,我们使用此域类型,我们使用 proto::extends<>
类模板来完成此操作。这是我们的表达式包装器,它为表达式注入了计算器特性。下面将对其进行描述。
// The calculator<> expression wrapper makes expressions // function objects. template< typename Expr > struct calculator : proto::extends< Expr, calculator< Expr >, calculator_domain > { typedef proto::extends< Expr, calculator< Expr >, calculator_domain > base_type; calculator( Expr const &expr = Expr() ) : base_type( expr ) {} // This is usually needed because by default, the compiler- // generated assignment operator hides extends<>::operator= BOOST_PROTO_EXTENDS_USING_ASSIGN(calculator) typedef double result_type; // Hide base_type::operator() by defining our own which // evaluates the calculator expression with a calculator context. result_type operator()( double d1 = 0.0, double d2 = 0.0 ) const { // As defined in the Hello Calculator section. calculator_context ctx; // ctx.args is a vector<double> that holds the values // with which we replace the placeholders (e.g., _1 and _2) // in the expression. ctx.args.push_back( d1 ); // _1 gets the value of d1 ctx.args.push_back( d2 ); // _2 gets the value of d2 return proto::eval(*this, ctx ); // evaluate the expression } };
我们希望计算器表达式是函数对象,因此我们必须定义一个接受并返回 doubles 的 operator()
。calculator<>
上面的包装器借助 proto::extends<>
模板来实现这一点。proto::extends<>
的第一个模板参数是我们正在扩展的表达式类型。第二个是包装表达式的类型。第三个参数是此包装器关联的域。像 calculator<>
这样的包装器类型,它继承自 proto::extends<>
,其行为就像它扩展的表达式类型一样,并带有您选择赋予它的任何其他行为。
![]() |
注意 |
---|---|
为什么不直接从 您可能会认为这种表达式扩展业务不必要地复杂化了。毕竟,这不正是 C++ 支持继承的原因吗?为什么 |
尽管在本例中不是绝对必要的,但我们使用 BOOST_PROTO_EXTENDS_USING_ASSIGN()
宏将 extends<>::operator=
带入作用域。只有当您希望像 _1 = 3
这样的表达式创建惰性评估的赋值时,这才是真正必要的。proto::extends<>
为您定义了合适的 operator=
,但编译器生成的 calculator<>::operator=
将隐藏它,除非您使用宏使其可用。
请注意,在 calculator<>::operator()
的实现中,我们使用我们之前定义的 calculator_context
评估表达式。正如我们之前看到的,上下文是赋予运算符含义的东西。对于计算器,上下文也是定义占位符终端含义的东西。
现在我们已经定义了 calculator<>
表达式包装器,我们需要包装占位符以赋予它们计算器特性
calculator< proto::terminal< placeholder<0> >::type > const _1; calculator< proto::terminal< placeholder<1> >::type > const _2;
BOOST_PROTO_EXTENDS()
保留 POD 特性要使用 proto::extends<>
,您的扩展类型必须派生自 proto::extends<>
。不幸的是,这意味着您的扩展类型不再是 POD,并且其实例不能静态初始化。(有关原因,请参阅 静态初始化 部分中的 原理 附录。)特别是,如上所述,全局占位符对象 _1
和 _2
将需要在运行时初始化,这可能会导致细微的初始化顺序错误。
还有另一种方法可以进行表达式扩展,而不会牺牲 POD 特性:
宏。您可以像使用 BOOST_PROTO_EXTENDS
()proto::extends<>
一样使用它。我们可以使用
使 BOOST_PROTO_EXTENDS
()calculator<>
保持 POD,并使我们的占位符静态初始化。
// The calculator<> expression wrapper makes expressions // function objects. template< typename Expr > struct calculator { // Use BOOST_PROTO_EXTENDS() instead of proto::extends<> to // make this type a Proto expression extension. BOOST_PROTO_EXTENDS(Expr, calculator<Expr>, calculator_domain) typedef double result_type; result_type operator()( double d1 = 0.0, double d2 = 0.0 ) const { /* ... as before ... */ } };
使用新的 calculator<>
类型,我们可以重新定义我们的占位符以进行静态初始化
calculator< proto::terminal< placeholder<0> >::type > const _1 = {{{}}}; calculator< proto::terminal< placeholder<1> >::type > const _2 = {{{}}};
我们需要进行一个额外的微小更改以适应表达式扩展的 POD 特性,我们将在下面关于表达式生成器的部分中进行描述。
做什么?它定义了正在扩展的表达式类型的数据成员;Proto 所需的一些嵌套 typedef;用于构建表达式模板的 BOOST_PROTO_EXTENDS
()operator=
、 operator[]
和 operator()
重载;以及用于计算 operator()
返回类型的嵌套 result<>
模板。但是,在这种情况下,不需要 operator()
重载和 result<>
模板,因为我们在 calculator<>
类型中定义了自己的 operator()
。Proto 提供了额外的宏,可以更精细地控制定义哪些成员函数。我们可以按如下方式改进我们的 calculator<>
类型
// The calculator<> expression wrapper makes expressions // function objects. template< typename Expr > struct calculator { // Use BOOST_PROTO_BASIC_EXTENDS() instead of proto::extends<> to // make this type a Proto expression extension: BOOST_PROTO_BASIC_EXTENDS(Expr, calculator<Expr>, calculator_domain) // Define operator[] to build expression templates: BOOST_PROTO_EXTENDS_SUBSCRIPT() // Define operator= to build expression templates: BOOST_PROTO_EXTENDS_ASSIGN() typedef double result_type; result_type operator()( double d1 = 0.0, double d2 = 0.0 ) const { /* ... as before ... */ } };
请注意,我们现在使用的是
而不是 BOOST_PROTO_BASIC_EXTENDS
()
。这仅添加数据成员和嵌套 typedef,但不添加任何重载运算符。这些运算符分别通过 BOOST_PROTO_EXTENDS
()
和 BOOST_PROTO_EXTENDS_ASSIGN
()
添加。我们省略了函数调用运算符和嵌套的 BOOST_PROTO_EXTENDS_SUBSCRIPT
()result<>
模板,这些模板可以使用 Proto 的
宏来定义。BOOST_PROTO_EXTENDS_FUNCTION
()
总而言之,以下是您可以用来定义表达式扩展的宏,以及每个宏的简要说明。
表 30.2. 表达式扩展宏
宏 |
目的 |
---|---|
|
定义类型为 |
定义 |
|
定义 |
|
定义 |
|
|
等效于
|
![]() |
警告 |
---|---|
Argument-Dependent Lookup 和 Proto 的运算符重载在
template<class T> struct my_complex { BOOST_PROTO_EXTENDS( typename proto::terminal<std::complex<T> >::type , my_complex<T> , proto::default_domain ) }; int main() { my_complex<int> c0, c1; c0 + c1; // ERROR: operator+ not found }
问题与 argument-dependent lookup 的工作方式有关。 那么我们能做什么呢?通过添加一个额外的虚拟模板参数,该参数默认为
template<class T, class Dummy = proto::is_proto_expr> struct my_complex { BOOST_PROTO_EXTENDS( typename proto::terminal<std::complex<T> >::type , my_complex<T> , proto::default_domain ) }; int main() { my_complex<int> c0, c1; c0 + c1; // OK, operator+ found now! }
类型 |
剩下的最后一件事是告诉 Proto 它需要将我们所有的计算器表达式包装在我们的 calculator<>
包装器中。我们已经包装了占位符,但是我们希望所有涉及计算器占位符的表达式都是计算器。我们可以通过在定义 calculator_domain
时指定表达式生成器来做到这一点,如下所示
// Define the calculator_domain we forward-declared above. // Specify that all expression in this domain should be wrapped // in the calculator<> expression wrapper. struct calculator_domain : proto::domain< proto::generator< calculator > > {};
proto::domain<>
的第一个模板参数是生成器。“生成器”只是一个花哨的名称,用于表示接受表达式并对其执行某些操作的函数对象。proto::generator<>
是一个非常简单的生成器——它将表达式包装在您指定的包装器中。proto::domain<>
从其生成器参数继承,因此所有域本身都是函数对象。
如果我们使用
来保持我们的表达式扩展类型为 POD,那么我们需要使用 BOOST_PROTO_EXTENDS
()proto::pod_generator<>
而不是 proto::generator<>
,如下所示
// If calculator<> uses BOOST_PROTO_EXTENDS() instead of // use proto::extends<>, use proto::pod_generator<> instead // of proto::generator<>. struct calculator_domain : proto::domain< proto::pod_generator< calculator > > {};
在 Proto 计算出新的表达式类型后,它会检查子表达式的域。它们必须匹配。假设它们匹配,Proto 会创建新的表达式并将其传递给
以进行任何额外的处理。如果我们不指定生成器,则新的表达式将保持不变地传递。但是由于我们在上面指定了生成器,Domain
::operator()calculator_domain::operator()
返回 calculator<>
对象。
现在我们可以将计算器表达式用作 STL 算法的函数对象,如下所示
double data[] = {1., 2., 3., 4.}; // Use the calculator EDSL to square each element ... WORKS! :-) std::transform( data, data + 4, data, _1 * _1 );
默认情况下,Proto 为 Proto 化表达式定义了所有可能的运算符重载。这使得可以轻松地组合 EDSL。但是,在某些情况下,Proto 的滥用重载的存在可能会导致混淆或更糟。当发生这种情况时,您必须禁用 Proto 的某些重载运算符。这可以通过为您的域定义语法并将其指定为 proto::domain<>
模板的第二个参数来完成。
在 Hello Calculator 部分,我们看到了 Proto 语法的示例,此处重复该示例
// Define the grammar of calculator expressions struct calculator_grammar : proto::or_< proto::plus< calculator_grammar, calculator_grammar > , proto::minus< calculator_grammar, calculator_grammar > , proto::multiplies< calculator_grammar, calculator_grammar > , proto::divides< calculator_grammar, calculator_grammar > , proto::terminal< proto::_ > > {};
在后续章节中,我们将对语法进行更多说明,但就目前而言,我们只想说 calculator_grammar
结构描述了所有表达式类型的子集——构成有效计算器表达式的子集。我们希望禁止 Proto 创建不符合此语法的计算器表达式。我们通过更改 calculator_domain
结构的定义来做到这一点。
// Define the calculator_domain. Expressions in the calculator // domain are wrapped in the calculator<> wrapper, and they must // conform to the calculator_grammar: struct calculator_domain : proto::domain< proto::generator< calculator >, calculator_grammar > {};
唯一的新增功能是将 calculator_grammar
作为第二个模板参数添加到 proto::domain<>
模板。 这样做会禁用 Proto 的任何运算符重载,否则这些重载会创建无效的计算器表达式。
此功能的另一个常见用途是禁用 Proto 的一元 operator&
重载。 对于您的 EDSL 用户来说,他们无法获取表达式的地址可能会感到惊讶! 您可以使用非常简单的语法非常轻松地为您的域禁用 Proto 的一元 operator&
重载,如下所示
// For expressions in my_domain, disable Proto's // unary address-of operator. struct my_domain : proto::domain< proto::generator< my_wrapper > // A simple grammar that matches any expression that // is not a unary address-of expression. , proto::not_< proto::address_of< _ > > > {};
类型 proto::not_< proto::address_of< _ > >
是一个非常简单的语法,它匹配除一元 address-of 表达式之外的所有表达式。 在描述 Proto 中间形式的部分中,我们将更多地讨论语法。
![]() |
注意 |
---|---|
这是一个高级主题。 如果您刚开始使用 Proto,请随意跳过此部分。 |
Proto 的运算符重载从子表达式构建表达式。 子表达式成为新表达式的子项。 默认情况下,子项通过引用存储在父项中。 本节介绍如何更改该默认设置。
as_child
与 as_expr
Proto 允许您独立自定义 proto::as_child()
和 proto::as_expr()
的行为。 两者都接受对象 x
,并通过在必要时将 x
转换为 Proto 终端来返回 Proto 表达式。 虽然相似,但这两种函数在不同的情况下使用,并且默认情况下具有细微不同的行为。 重要的是要了解差异,以便您知道要自定义哪个函数以实现您想要的行为。
例如:proto::as_expr()
通常由您使用,将对象转换为要保存在局部变量中的 Proto 表达式,如下所示
auto l = proto::as_expr(x); // Turn x into a Proto expression, hold the result in a local
无论 x
是否已经是 Proto 表达式,上述方法都有效。 对象 l
保证是有效的 Proto 表达式。 如果 x
是非 Proto 对象,则会将其转换为终端表达式,该表达式按值保存 x
。[28] 如果 x
已经是 Proto 对象,则 proto::as_expr()
按值返回它,且不进行修改。
相反,proto::as_child()
由 Proto 内部使用,用于在将对象作为另一个表达式的子项之前对其进行预处理。 由于它是 Proto 内部的,因此您不会显式看到它,但它在幕后存在于如下表达式中
x + y; // Consider that y is a Proto expression, but x may or may not be.
在这种情况下,Proto 从两个子项构建一个加法节点。 两者都在传递给 proto::as_child()
之后进行预处理,然后再将它们作为新节点的子项。 如果 x
不是 Proto 表达式,则它会通过包装在 Proto 终端中而成为 Proto 表达式,该终端通过引用保存它。 如果 x
已经是 Proto 表达式,则 proto::as_child()
通过引用返回它,且不进行修改。 将此与上面对 proto::as_expr()
的描述进行对比。
下表总结了上述描述。
表 30.3. proto::as_expr() 与 proto::as_child()
函数 |
当 |
当 |
---|---|---|
|
(按值)返回一个新的 Proto 终端,该终端 |
(按值)返回 |
|
(按值)返回一个新的 Proto 终端,该终端 |
(按引用)返回 |
![]() |
注意 |
---|---|
Proto 在一个重要的地方同时使用 |
现在您知道了 proto::as_child()
和 proto::as_expr()
是什么,它们在何处使用,以及它们的默认行为是什么,您可能会认为对于您的域,其中一个或两个函数应该具有不同的行为。 例如,给定上面对 proto::as_child()
的描述,以下代码始终是错误的
proto::literal<int> i(0); auto l = i + 42; // This is WRONG! Don't do this.
为什么这是错误的? 因为 proto::as_child()
会将整数文字 42 转换为 Proto 终端,该终端保存对使用 42 初始化的临时整数的引用。 该临时变量的生命周期在分号处结束,从而保证局部变量 l
留下对已死亡整数的悬空引用。 该怎么办? 一种答案是使用 proto::deep_copy()
。 另一种是为您的域自定义 proto::as_child()
的行为。 请继续阅读以了解详细信息。
as_child
要控制 Proto 如何在您的域中从子表达式构建表达式,请像往常一样定义您的域,然后在其中定义嵌套的 as_child<>
类模板,如下所示
class my_domain : proto::domain< my_generator, my_grammar > { // Here is where you define how Proto should handle // sub-expressions that are about to be glommed into // a larger expression. template< typename T > struct as_child { typedefunspecified-Proto-expr-type
result_type; result_type operator()( T & t ) const { returnunspecified-Proto-expr-object
; } }; };
有一件重要的事情需要注意:在上面的代码中,模板参数 T
可能是也可能不是 Proto 表达式类型,但结果必须是 Proto 表达式类型,或对其中一个的引用。 这意味着大多数用户定义的 as_child<>
模板将需要检查 T
是否是表达式(使用 proto::is_expr<>
),然后通过将非表达式包装为 proto::terminal< /* ... */ >::type
或等效项来将其转换为 Proto 终端。
as_expr
虽然不太常见,但 Proto 也允许您在每个域的基础上自定义 proto::as_expr()
的行为。 该技术与 as_child
的技术相同。 见下文
class my_domain : proto::domain< my_generator, my_grammar > { // Here is where you define how Proto should handle // objects that are to be turned into expressions // fit for storage in local variables. template< typename T > struct as_expr { typedefunspecified-Proto-expr-type
result_type; result_type operator()( T & t ) const { returnunspecified-Proto-expr-object
; } }; };
auto
安全让我们再次看看上面描述的问题,其中涉及 C++11 auto
关键字和 proto::as_child()
的默认行为。
proto::literal<int> i(0); auto l = i + 42; // This is WRONG! Don't do this.
回想一下,问题是为保存值 42 而创建的临时整数的生命周期。 局部变量 l
在其生命周期结束后将留下对其的悬空引用。 如果我们希望 Proto 使表达式可以安全地以这种方式存储在局部变量中怎么办? 我们可以通过使 proto::as_child()
的行为与 proto::as_expr()
完全一样来非常轻松地做到这一点。 以下代码实现了这一点
template< typename E > struct my_expr; struct my_generator : proto::pod_generator< my_expr > {}; struct my_domain : proto::domain< my_generator > { // Make as_child() behave like as_expr() in my_domain. // (proto_base_domain is a typedef for proto::domain< my_generator > // that is defined in proto::domain<>.) template< typename T > struct as_child : proto_base_domain::as_expr< T > {}; }; template< typename E > struct my_expr { BOOST_PROTO_EXTENDS( E, my_expr< E >, my_domain ) }; /* ... */ proto::literal< int, my_domain > i(0); auto l = i + 42; // OK! Everything is stored by value here.
请注意,my_domain::as_child<>
只是延迟到 proto::domain<>
中找到的 as_expr<>
的默认实现。 通过简单地交叉连接我们域的 as_child<>
到 as_expr<>
,我们保证所有可以按值保存的终端都按值保存,并且所有子表达式也都按值保存。 这会增加复制,并可能导致运行时性能成本,但它消除了任何生命周期管理问题的幽灵。
有关另一个示例,请参见 libs/proto/example/lambda.hpp
中 lldomain
的定义。 该示例是在 Boost.Proto 之上对 Boost Lambda Library (BLL) 的完整重新实现。 BLL 生成的函数对象可以安全地存储在局部变量中。 为了使用 Proto 模拟这一点,lldomain
将 as_child<>
交叉连接到 as_expr<>
,如上所述,但有一个额外的转折:数组类型的对象也按引用存储。 看看吧。
![]() |
注意 |
---|---|
这是一个高级主题。 如果您刚开始使用 Proto,请随意跳过此部分。 |
能够组合不同的 EDSL 是它们最令人兴奋的功能之一。 考虑一下如何使用 yacc 构建解析器。 您使用 yacc 的特定于域的语言编写语法规则。 然后,您将用 C 编写的语义动作嵌入到语法中。 Boost 的 Spirit 解析器生成器为您提供了相同的功能。 您使用 Spirit.Qi 编写语法规则,并使用 Phoenix 库嵌入语义动作。 Phoenix 和 Spirit 都是基于 Proto 的特定于域的语言,它们具有自己独特的语法和语义。 但是您可以自由地将 Phoenix 表达式嵌入到 Spirit 表达式中。 本节介绍 Proto 的子域功能,该功能使您可以定义可互操作的域族。
当您尝试从不同域中的两个子表达式创建表达式时,结果表达式的域是什么? 这是子域解决的基本问题。 考虑以下代码
#include <boost/proto/proto.hpp> namespace proto = boost::proto; // Forward-declare two expression wrappers template<typename E> struct spirit_expr; template<typename E> struct phoenix_expr; // Define two domains struct spirit_domain : proto::domain<proto::generator<spirit_expr> > {}; struct phoenix_domain : proto::domain<proto::generator<phoenix_expr> > {}; // Implement the two expression wrappers template<typename E> struct spirit_expr : proto::extends<E, spirit_expr<E>, spirit_domain> { spirit_expr(E const &e = E()) : spirit_expr::proto_extends(e) {} }; template<typename E> struct phoenix_expr : proto::extends<E, phoenix_expr<E>, phoenix_domain> { phoenix_expr(E const &e = E()) : phoenix_expr::proto_extends(e) {} }; int main() { proto::literal<int, spirit_domain> sp(0); proto::literal<int, phoenix_domain> phx(0); // Whoops! What does it mean to add two expressions in different domains? sp + phx; // ERROR }
上面,我们定义了两个名为 spirit_domain
和 phoenix_domain
的域,并在每个域中声明了两个 int 字面量。 然后,我们尝试使用 Proto 的二元加运算符将它们组合成更大的表达式,但失败了。 Proto 无法确定结果表达式应该在 Spirit 域还是 Phoenix 域中,因此无法确定它应该是 spirit_expr<>
还是 phoenix_expr<>
的实例。 我们必须告诉 Proto 如何解决冲突。 我们可以通过声明 Phoenix 是 Spirit 的子域来做到这一点,如下面的 phoenix_domain
定义所示
// Declare that phoenix_domain is a sub-domain of spirit_domain struct phoenix_domain : proto::domain<proto::generator<phoenix_expr>, proto::_, spirit_domain> {};
proto::domain<>
的第三个模板参数是超域。 通过如上定义 phoenix_domain
,我们表示 Phoenix 表达式可以与 Spirit 表达式组合,并且当这种情况发生时,结果表达式应该是 Spirit 表达式。
![]() |
注意 |
---|---|
如果您想知道上面 |
当给定表达式中存在多个域时,Proto 使用一些规则来确定哪个域“胜出”。 这些规则大致模仿 C++ 继承的规则。 Phoenix_domain
是 spirit_domain
的子域。 您可以将它比作派生/基类关系,这种关系使 Phoenix 表达式可以隐式转换为 Spirit 表达式。 并且由于 Phoenix 表达式可以“转换”为 Spirit 表达式,因此它们可以与 Spirit 表达式自由组合,结果是 Spirit 表达式。
![]() |
注意 |
---|---|
超域和子域实际上不是使用继承来实现的。 这只是一种有用的心理模型。 |
即使在三个域的情况下,当两个域是第三个域的子域时,与继承的类比也成立。 想象一下另一个名为 foobar_domain
的域,它也是 spirit_domain
的子域。 foobar_domain
中的表达式可以与 phoenix_domain
中的表达式组合,结果表达式将在 spirit_domain
中。 这是因为两个子域中的表达式都“转换”为超域,因此允许该操作,并且超域胜出。
当您不将 Proto 表达式分配给特定域时,Proto 会将其视为所谓的默认域 proto::default_domain
的成员。 甚至非 Proto 对象也被视为默认域中的终端。 考虑
int main() { proto::literal<int, spirit_domain> sp(0); // Add 1 to a spirit expression. Result is a spirit expression. sp + 1; }
默认域中的表达式(或像 1
这样的非表达式)可以隐式转换为每个其他域类型的表达式。 更重要的是,您可以将您的域定义为默认域的子域。 这样做,您可以使您域中的表达式转换为每个其他域中的表达式。 这就像一个“自由恋爱”域,因为它将与其他所有域自由混合。
让我们再次考虑 Phoenix EDSL。 由于它提供了通用的 lambda 功能,因此可以合理地假设除了 Spirit 之外的许多其他 EDSL 可能也希望能够嵌入 Phoenix 表达式。 换句话说,phoenix_domain
应该是 proto::default_domain
的子域,而不是 spirit_domain
// Declare that phoenix_domain is a sub-domain of proto::default_domain struct phoenix_domain : proto::domain<proto::generator<phoenix_expr>, proto::_, proto::default_domain> {};
这样就好多了。 Phoenix 表达式现在可以放在任何地方。
使用 Proto 子域使混合来自多个域的表达式成为可能。 当您希望您域中的表达式与所有表达式自由组合时,请将其设为 proto::default_domain
的子域。
前面关于定义 Proto 前端的讨论都做了一个很大的假设:您有从头开始定义一切的奢侈。 如果您有现有类型,例如矩阵类型和向量类型,并且您想将它们视为 Proto 终端,该怎么办? Proto 通常只处理其自己的表达式类型,但是使用
,它也可以容纳您的自定义终端类型。BOOST_PROTO_DEFINE_OPERATORS
()
例如,假设您有以下类型,并且您无法修改它们以使其成为“原生” Proto 终端类型。
namespace math { // A matrix type ... struct matrix { /*...*/ }; // A vector type ... struct vector { /*...*/ }; }
您可以使用
定义适当的运算符重载,以非侵入方式使这些类型的对象成为 Proto 终端。 基本步骤如下BOOST_PROTO_DEFINE_OPERATORS
()
BOOST_PROTO_DEFINE_OPERATORS
()
定义一组运算符重载,将 trait 的名称作为第一个宏参数传递,并将 Proto 域的名称(例如,proto::default_domain
)作为第二个宏参数传递。以下代码演示了它的工作原理。
namespace math { template<typename T> struct is_terminal : mpl::false_ {}; // OK, "matrix" is a custom terminal type template<> struct is_terminal<matrix> : mpl::true_ {}; // OK, "vector" is a custom terminal type template<> struct is_terminal<vector> : mpl::true_ {}; // Define all the operator overloads to construct Proto // expression templates, treating "matrix" and "vector" // objects as if they were Proto terminals. BOOST_PROTO_DEFINE_OPERATORS(is_terminal, proto::default_domain) }
调用
宏定义了一整套运算符重载,这些重载将 BOOST_PROTO_DEFINE_OPERATORS
()matrix
和 vector
对象视为 Proto 终端。 并且由于运算符是在与 matrix
和 vector
类型相同的命名空间中定义的,因此运算符将通过依赖于参数的查找找到。 使用上面的代码,我们现在可以使用矩阵和向量构造表达式模板,如下所示。
math::matrix m1; math::vector v1; proto::literal<int> i(0); m1 * 1; // custom terminal and literals are OK m1 * i; // custom terminal and Proto expressions are OK m1 * v1; // two custom terminals are OK, too.
有时,作为 EDSL 设计人员,为了让您的用户轻松生活,您必须让自己的生活变得艰难。 为用户提供自然而灵活的语法通常涉及编写大量重复的函数重载。 这足以给您带来重复性劳损! 在您伤害自己之前,请查看 Proto 提供的用于自动化许多重复代码生成工作的宏。
想象一下,我们正在编写一个 lambda EDSL,并且我们希望启用使用以下语法构造任何类型的临时对象的语法
// A lambda expression that takes two arguments and // uses them to construct a temporary std::complex<> construct< std::complex<int> >( _1, _2 )
为了便于讨论,假设我们已经有一个函数对象模板 construct_impl<>
,它接受参数并从中构造新对象。 我们希望上面的 lambda 表达式等效于以下内容
// The above lambda expression should be roughly equivalent // to the following: proto::make_expr<proto::tag::function>( construct_impl<std::complex<int> >() // The function to invoke lazily , boost::ref(_1) // The first argument to the function , boost::ref(_2) // The second argument to the function );
我们可以将我们的 construct()
函数模板定义如下
template<typename T, typename A0, typename A1> typename proto::result_of::make_expr< proto::tag::function , construct_impl<T> , A0 const & , A1 const & >::type const construct(A0 const &a0, A1 const &a1) { return proto::make_expr<proto::tag::function>( construct_impl<T>() , boost::ref(a0) , boost::ref(a1) ); }
这适用于两个参数,但我们希望它适用于任意数量的参数,最多为 (
- 1)。 (为什么是“- 1”? 因为一个子项被 BOOST_PROTO_MAX_ARITY
construct_impl<T>()
终端占用,只剩下 (
- 1) 个其他子项的空间。)BOOST_PROTO_MAX_ARITY
对于这种情况,Proto 提供了
和 BOOST_PROTO_REPEAT
()
宏。 要使用它,我们将上面的函数定义转换为宏,如下所示BOOST_PROTO_REPEAT_FROM_TO
()
#define M0(N, typename_A, A_const_ref, A_const_ref_a, ref_a) \ template<typename T, typename_A(N)> \ typename proto::result_of::make_expr< \ proto::tag::function \ , construct_impl<T> \ , A_const_ref(N) \ >::type const \ construct(A_const_ref_a(N)) \ { \ return proto::make_expr<proto::tag::function>( \ construct_impl<T>() \ , ref_a(N) \ ); \ }
请注意,我们将该函数转换为一个宏,该宏接受 5 个参数。 第一个是当前的迭代次数。 其余的是生成不同序列的其他宏的名称。 例如,Proto 将宏的名称作为第二个参数传递,该宏将扩展为 typename A0, typename A1, ...
。
现在我们已经将我们的函数转换为宏,我们可以将该宏传递给
。 Proto 将迭代调用它,为我们生成所有函数重载。BOOST_PROTO_REPEAT_FROM_TO
()
// Generate overloads of construct() that accept from // 1 to BOOST_PROTO_MAX_ARITY-1 arguments: BOOST_PROTO_REPEAT_FROM_TO(1, BOOST_PROTO_MAX_ARITY, M0) #undef M0
如上所述,Proto 将宏的名称作为最后 4 个参数传递给您的宏,这些宏生成各种序列。 宏
和 BOOST_PROTO_REPEAT
()
为这些参数选择默认值。 如果默认值不能满足您的需求,则可以使用 BOOST_PROTO_REPEAT_FROM_TO
()
和 BOOST_PROTO_REPEAT_EX
()
并传递生成不同序列的不同宏。 Proto 定义了许多此类宏,用作 BOOST_PROTO_REPEAT_FROM_TO_EX
()
和 BOOST_PROTO_REPEAT_EX
()
的参数。 查看 BOOST_PROTO_REPEAT_FROM_TO_EX
()boost/proto/repeat.hpp
的参考部分,以获取所有详细信息。
另外,请查看
。 它的工作方式类似于 BOOST_PROTO_LOCAL_ITERATE
()
及其友元,但是当您想要更改一个宏参数并接受其他参数的默认值时,它可能更容易使用。BOOST_PROTO_REPEAT
()
到目前为止,您已经了解了一些关于如何为您的 EDSL “编译器” 构建前端的知识——您可以定义终端和生成表达式模板的函数。 但是我们还没有讨论表达式模板本身。 它们看起来像什么? 您可以用它们做什么? 在本节中,我们将看到。
expr<>
类型所有 Proto 表达式都是名为 proto::expr<>
的模板的实例化(或此类实例化的包装器)。 当我们如下定义终端时,我们实际上是在初始化 proto::expr<>
模板的实例。
// Define a placeholder type template<int I> struct placeholder {}; // Define the Protofied placeholder terminal proto::terminal< placeholder<0> >::type const _1 = {{}};
_1
的实际类型如下所示
proto::expr< proto::tag::terminal, proto::term< placeholder<0> >, 0 >
proto::expr<>
模板是 Proto 中最重要的类型。 虽然您很少需要直接处理它,但它始终在幕后将您的表达式树连接在一起。 实际上,proto::expr<>
就是表达式树——分支、叶子和所有。
proto::expr<>
模板构成表达式树中的节点。 第一个模板参数是节点类型; 在这种情况下,是 proto::tag::terminal
。 这意味着 _1
是表达式树中的叶节点。 第二个模板参数是子类型列表,或者在终端的情况下,是终端的值类型。 终端在类型列表中始终只有一个类型。 最后一个参数是表达式的元数。 终端的元数为 0,一元表达式的元数为 1,依此类推。
proto::expr<>
结构定义如下
template< typename Tag, typename Args, long Arity = Args::arity > struct expr; template< typename Tag, typename Args > struct expr< Tag, Args, 1 > { typedef typename Args::child0 proto_child0; proto_child0 child0; // ... };
proto::expr<>
结构未定义构造函数,或任何其他会阻止静态初始化的内容。 所有 proto::expr<>
对象都使用聚合初始化(带花括号)进行初始化。 在我们的示例中,_1
使用初始化器 {{}}
初始化。 外面的花括号是 proto::expr<>
结构的初始化器,里面的花括号是成员 _1.child0
的初始化器,其类型为 placeholder<0>
。 请注意,我们使用花括号初始化 _1.child0
,因为 placeholder<0>
也是一个聚合。
_1
节点是 proto::expr<>
的实例化,并且包含 _1
的表达式也是 proto::expr<>
的实例化。 要有效地使用 Proto,您不必费心 Proto 生成的实际类型。 这些是细节,但是您很可能会在编译器错误消息中遇到这些类型,因此熟悉它们很有帮助。 这些类型看起来像这样
// The type of the expression -_1 typedef proto::expr< proto::tag::negate , proto::list1< proto::expr< proto::tag::terminal , proto::term< placeholder<0> > , 0 > const & > , 1 > negate_placeholder_type; negate_placeholder_type x = -_1; // The type of the expression _1 + 42 typedef proto::expr< proto::tag::plus , proto::list2< proto::expr< proto::tag::terminal , proto::term< placeholder<0> > , 0 > const & , proto::expr< proto::tag::terminal , proto::term< int const & > , 0 > > , 2 > placeholder_plus_int_type; placeholder_plus_int_type y = _1 + 42;
关于这些类型,有几点需要注意
expr<>
终端对象中,被转换为 Proto 表达式。这些新的包装器本身不通过引用持有,但被包装的对象是通过引用持有的。请注意,Proto 化后的 42
字面量的类型是 int const &
—— 通过引用持有。这些类型清楚地表明:Proto 表达式树中的所有内容都是通过引用持有的。这意味着构建表达式树的成本非常低。它完全不涉及复制。
![]() |
注意 |
---|---|
细心的读者会注意到,上面定义的 |
将表达式组装成树后,您自然会希望能够反过来操作,并访问节点的子节点。您甚至可能希望能够使用 Boost.Fusion 库中的算法迭代子节点。本节将展示如何实现。
表达式树中的每个节点都具有一个描述节点的标签类型,以及一个对应于其子节点数量的元数。您可以使用 proto::tag_of<>
和 proto::arity_of<>
元函数来获取它们。考虑以下示例
template<typename Expr> void check_plus_node(Expr const &) { // Assert that the tag type is proto::tag::plus BOOST_STATIC_ASSERT(( boost::is_same< typename proto::tag_of<Expr>::type , proto::tag::plus >::value )); // Assert that the arity is 2 BOOST_STATIC_ASSERT( proto::arity_of<Expr>::value == 2 ); } // Create a binary plus node and use check_plus_node() // to verify its tag type and arity: check_plus_node( proto::lit(1) + 2 );
对于给定的类型 Expr
,您可以直接访问标签和元数,分别为 Expr::proto_tag
和 Expr::proto_arity
,其中 Expr::proto_arity
是一个 MPL 整型常量。
没有比终端更简单的表达式,也没有比提取其值更基本的操作了。正如我们已经看到的,这就是 proto::value()
的用途。
proto::terminal< std::ostream & >::type cout_ = {std::cout}; // Get the value of the cout_ terminal: std::ostream & sout = proto::value( cout_ ); // Assert that we got back what we put in: assert( &sout == &std::cout );
要计算 proto::value()
函数的返回类型,您可以使用 proto::result_of::value<>
。当 proto::result_of::value<>
的参数是非引用类型时,元函数的结果类型是适合按值存储的值的类型;也就是说,顶层引用和限定符会被剥离。但是,当使用引用类型实例化时,结果类型会添加一个引用,从而产生适合按引用存储的类型。如果您想知道终端值的实际类型,包括它是按值存储还是按引用存储,您可以使用 fusion::result_of::value_at<Expr, 0>::type
。
下表总结了以上段落的内容。
表 30.4. 访问值类型
元函数调用 |
当值类型为 ... |
结果为 ... |
---|---|---|
|
|
typename boost::remove_const< typename boost::remove_reference<T>::type >::type [a]
|
|
|
typename boost::add_reference<T>::type
|
|
|
typename boost::add_reference< typename boost::add_const<T>::type >::type
|
|
|
|
[a] 如果 |
表达式树中的每个非终端节点都对应于表达式中的一个运算符,而子节点对应于操作数或运算符的参数。要访问它们,您可以使用 proto::child_c()
函数模板,如下所示
proto::terminal<int>::type i = {42}; // Get the 0-th operand of an addition operation: proto::terminal<int>::type &ri = proto::child_c<0>( i + 2 ); // Assert that we got back what we put in: assert( &i == &ri );
您可以使用 proto::result_of::child_c<>
元函数来获取表达式节点的第 N 个子节点的类型。通常,您不关心子节点是按值存储还是按引用存储,因此当您请求表达式 Expr
(其中 Expr
不是引用类型)的第 N 个子节点的类型时,您将获得从子节点的类型中剥离引用和 cv 限定符后的类型。
template<typename Expr> void test_result_of_child_c(Expr const &expr) { typedef typename proto::result_of::child_c<Expr, 0>::type type; // Since Expr is not a reference type, // result_of::child_c<Expr, 0>::type is a // non-cv qualified, non-reference type: BOOST_MPL_ASSERT(( boost::is_same< type, proto::terminal<int>::type > )); } // ... proto::terminal<int>::type i = {42}; test_result_of_child_c( i + 2 );
但是,如果您请求 Expr &
或 Expr const &
(注意引用)的第 N 个子节点的类型,则结果类型将是一个引用,无论子节点实际上是按引用存储还是按值存储。如果您需要确切地知道子节点是如何存储在节点中的,无论是按引用还是按值,您可以使用 fusion::result_of::value_at<Expr, N>::type
。下表总结了 proto::result_of::child_c<>
元函数的行为。
表 30.5. 访问子类型
元函数调用 |
当子类型为 ... |
结果为 ... |
---|---|---|
|
|
typename boost::remove_const< typename boost::remove_reference<T>::type >::type
|
|
|
typename boost::add_reference<T>::type
|
|
|
typename boost::add_reference< typename boost::add_const<T>::type >::type
|
|
|
|
C++ 中的大多数运算符都是一元或二元的,因此访问唯一的操作数,或左操作数和右操作数是非常常见的操作。因此,Proto 提供了 proto::child()
、proto::left()
和 proto::right()
函数。proto::child()
和 proto::left()
与 proto::child_c<0>()
同义,而 proto::right()
与 proto::child_c<1>()
同义。
还有 proto::result_of::child<>
、proto::result_of::left<>
和 proto::result_of::right<>
元函数,它们只是转发到其对应的 proto::result_of::child_c<>
。
当您使用 Proto 构建表达式模板时,所有中间子节点都通过引用持有。这避免了不必要的复制,如果您希望您的 EDSL 在运行时表现良好,这一点至关重要。当然,如果临时对象在您尝试评估表达式模板之前超出作用域,则存在危险。在 C++0x 中,使用新的 decltype
和 auto
关键字时,这个问题尤其突出。考虑以下情况
// OOPS: "ex" is left holding dangling references auto ex = proto::lit(1) + 2;
如果您使用 BOOST_TYPEOF()
或 BOOST_AUTO()
,或者如果您尝试将表达式模板传递到其组成部分的作用域之外,那么在今天的 C++ 中也可能发生这个问题。
在这些情况下,您需要深度复制您的表达式模板,以便所有中间节点和终端都按值持有。这样,您可以安全地将表达式模板分配给局部变量或从函数返回它,而无需担心悬空引用。您可以使用 proto::deep_copy()
来执行此操作,如下所示
// OK, "ex" has no dangling references auto ex = proto::deep_copy( proto::lit(1) + 2 );
如果您正在使用 Boost.Typeof,它看起来会像这样
// OK, use BOOST_AUTO() and proto::deep_copy() to // store an expression template in a local variable BOOST_AUTO( ex, proto::deep_copy( proto::lit(1) + 2 ) );
为了使上面的代码工作,您必须包含 boost/proto/proto_typeof.hpp
头文件,该文件还定义了
宏,该宏会自动深度复制其参数。使用 BOOST_PROTO_AUTO
()
,上面的代码可以写成BOOST_PROTO_AUTO
()
// OK, BOOST_PROTO_AUTO() automatically deep-copies // its argument: BOOST_PROTO_AUTO( ex, proto::lit(1) + 2 );
当深度复制表达式树时,所有中间节点和所有终端都按值存储。唯一的例外是函数引用终端,它们会被保留原样。
![]() |
注意 |
---|---|
|
Proto 提供了一个实用程序,用于美观地打印表达式树,当您尝试调试 EDSL 时,它非常方便。它被称为 proto::display_expr()
,您可以将要打印的表达式传递给它,并可选择传递一个 std::ostream
以将输出发送到该流。考虑以下示例
// Use display_expr() to pretty-print an expression tree proto::display_expr( proto::lit("hello") + 42 );
上面的代码将以下内容写入 std::cout
plus( terminal(hello) , terminal(42) )
为了调用 proto::display_expr()
,表达式中的所有终端都必须是可流化的(即可写入 std::ostream
)。此外,标签类型也必须都是可流化的。这是一个包含自定义终端类型和自定义标签的示例
// A custom tag type that is Streamable struct MyTag { friend std::ostream &operator<<(std::ostream &s, MyTag) { return s << "MyTag"; } }; // Some other Streamable type struct MyTerminal { friend std::ostream &operator<<(std::ostream &s, MyTerminal) { return s << "MyTerminal"; } }; int main() { // Display an expression tree that contains a custom // tag and a user-defined type in a terminal proto::display_expr( proto::make_expr<MyTag>(MyTerminal()) + 42 ); }
上面的代码打印以下内容
plus( MyTag( terminal(MyTerminal) ) , terminal(42) )
下表列出了可重载的 C++ 运算符、每个运算符的 Proto 标签类型以及用于生成相应 Proto 表达式类型的元函数的名称。正如我们稍后将看到的,元函数也可以用作匹配此类节点的语法,以及直通转换。
表 30.6. 运算符、标签和元函数
运算符 |
Proto 标签 |
Proto 元函数 |
---|---|---|
一元 |
|
|
一元 |
|
|
一元 |
|
|
一元 |
|
|
一元 |
|
|
一元 |
|
|
一元前缀 |
|
|
一元前缀 |
|
|
一元后缀 |
|
|
一元后缀 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元 |
|
|
二元下标 |
|
|
三元 |
|
|
n 元函数调用 |
|
|
Boost.Fusion 是一个用于操作异构序列的迭代器、算法、容器和适配器库。本质上,Proto 表达式只是其子表达式的异构序列,因此 Proto 表达式是有效的 Fusion 随机访问序列。这意味着您可以将 Fusion 算法应用于它们,转换它们,将 Fusion 过滤器和视图应用于它们,并使用 fusion::at()
访问它们的元素。Fusion 可以对异构序列执行的操作超出了本用户指南的范围,但下面是一个简单的示例。它接受一个惰性函数调用,如 fun(1,2,3,4)
,并使用 Fusion 按顺序打印函数参数。
struct display { template<typename T> void operator()(T const &t) const { std::cout << t << std::endl; } }; struct fun_t {}; proto::terminal<fun_t>::type const fun = {{}}; // ... fusion::for_each( fusion::transform( // pop_front() removes the "fun" child fusion::pop_front(fun(1,2,3,4)) // Extract the ints from the terminal nodes , proto::functional::value() ) , display() );
回顾一下引言,proto::functional
命名空间中的类型定义了与 Proto 的自由函数对应的函数对象。因此,proto::functional::value()
创建了一个等效于 proto::value()
函数的函数对象。上面对 fusion::for_each()
的调用显示以下内容
1 2 3 4
终端也是有效的 Fusion 序列。它们只包含一个元素:它们的值。
想象一下上面示例的一个轻微变体,我们不是迭代惰性函数调用的参数,而是希望迭代加法表达式中的终端
proto::terminal<int>::type const _1 = {1}; // ERROR: this doesn't work! Why? fusion::for_each( fusion::transform( _1 + 2 + 3 + 4 , proto::functional::value() ) , display() );
这不起作用的原因是表达式 _1 + 2 + 3 + 4
没有描述终端的扁平序列 --- 它描述的是二叉树。但是,我们可以使用 Proto 的 proto::flatten()
函数将其视为终端的扁平序列。proto::flatten()
返回一个视图,该视图使树看起来像一个扁平的 Fusion 序列。如果最顶层节点的标签类型为 T
,则扁平化序列的元素是不具有标签类型 T
的子节点。此过程是递归评估的。因此,以上代码可以正确地写成
proto::terminal<int>::type const _1 = {1}; // OK, iterate over a flattened view fusion::for_each( fusion::transform( proto::flatten(_1 + 2 + 3 + 4) , proto::functional::value() ) , display() );
上面对 fusion::for_each()
的调用显示以下内容
1 2 3 4
表达式树可以具有非常丰富和复杂的结构。通常,在处理表达式之前,您需要了解表达式结构的一些信息。本节介绍 Proto 提供的用于查看表达式树内部并发现其结构的工具。正如您将在后面的章节中看到的,所有可以使用 Proto 完成的真正有趣的事情都从这里开始。
想象一下,你的 EDSL 是一个微型的 I/O 设备,其 iostream 操作是延迟执行的。你可能希望代表输入操作的表达式由一个函数处理,而代表输出操作的表达式由另一个函数处理。你会如何做到这一点呢?
答案是编写模式(也称为 语法),以匹配输入和输出表达式的结构。Proto 提供了用于定义语法的实用工具,以及 proto::matches<>
模板,用于检查给定的表达式类型是否与语法匹配。
首先,让我们定义一些可以在延迟 I/O 表达式中使用的终结符
proto::terminal< std::istream & >::type cin_ = { std::cin }; proto::terminal< std::ostream & >::type cout_ = { std::cout };
现在,我们可以使用 cout_
代替 std::cout
,并获得可以稍后执行的 I/O 表达式树。为了定义与 cin_ >> i
和 cout_ << 1
形式的输入和输出表达式匹配的语法,我们这样做:
struct Input : proto::shift_right< proto::terminal< std::istream & >, proto::_ > {}; struct Output : proto::shift_left< proto::terminal< std::ostream & >, proto::_ > {};
我们之前已经见过模板 proto::terminal<>
,但在这里我们使用它时没有访问嵌套的 ::type
。像这样使用时,它是一个非常简单的语法,proto::shift_right<>
和 proto::shift_left<>
也是如此。这里的新成员是 _
在 proto
命名空间中。它是一个通配符,可以匹配任何内容。Input
结构体是一个语法,它匹配任何右移表达式,该表达式具有 std::istream
终结符作为其左操作数。
我们可以将这些语法与 proto::matches<>
模板一起使用,以便在编译时查询给定的 I/O 表达式类型是输入操作还是输出操作。考虑以下代码:
template< typename Expr > void input_output( Expr const & expr ) { if( proto::matches< Expr, Input >::value ) { std::cout << "Input!\n"; } if( proto::matches< Expr, Output >::value ) { std::cout << "Output!\n"; } } int main() { int i = 0; input_output( cout_ << 1 ); input_output( cin_ >> i ); return 0; }
此程序打印以下内容:
Output! Input!
如果我们想将 input_output()
函数分解为两个函数,一个处理输入表达式,另一个处理输出表达式,我们可以使用 boost::enable_if<>
,如下所示:
template< typename Expr > typename boost::enable_if< proto::matches< Expr, Input > >::type input_output( Expr const & expr ) { std::cout << "Input!\n"; } template< typename Expr > typename boost::enable_if< proto::matches< Expr, Output > >::type input_output( Expr const & expr ) { std::cout << "Output!\n"; }
这与之前的版本的工作方式相同。但是,以下代码根本无法编译:
input_output( cout_ << 1 << 2 ); // oops!
哪里出错了?问题在于这个表达式与我们的语法不匹配。该表达式的分组方式就像它被写成 (cout_ << 1) << 2
一样。它将不匹配 Output
语法,后者期望左操作数是一个终结符,而不是另一个左移操作。我们需要修复语法。
我们注意到,为了验证表达式是输入还是输出,我们需要递归下降到最左下角的叶子节点,并检查它是否是 std::istream
或 std::ostream
。当我们到达终结符时,我们必须停止递归。我们可以使用 proto::or_<>
在我们的语法中表达这一点。以下是正确的 Input
和 Output
语法:
struct Input : proto::or_< proto::shift_right< proto::terminal< std::istream & >, proto::_ > , proto::shift_right< Input, proto::_ > > {}; struct Output : proto::or_< proto::shift_left< proto::terminal< std::ostream & >, proto::_ > , proto::shift_left< Output, proto::_ > > {};
乍一看,这可能有点奇怪。我们似乎在用自身定义 Input
和 Output
类型。实际上,这完全没问题。在语法中使用 Input
和 Output
类型时,它们是不完整的,但是当我们实际使用 proto::matches<>
评估语法时,这些类型将是完整的。这些是递归语法,这样做是正确的,因为它们必须匹配递归数据结构!
将诸如 cout_ << 1 << 2
之类的表达式与 Output
语法匹配的过程如下:
proto::or_<>
的第一个备选项。它将失败,因为表达式 cout_ << 1 << 2
与语法 proto::shift_left< proto::terminal< std::ostream & >, proto::_ >
不匹配。proto::shift_left< Output, proto::_ >
匹配。表达式是左移,所以我们接下来尝试匹配操作数。2
与 proto::_
轻松匹配。cout_ << 1
是否与 Output
匹配,我们必须递归评估 Output
语法。这次我们成功了,因为 cout_ << 1
将与 proto::or_<>
的第一个备选项匹配。我们完成了 -- 语法匹配成功。
表达式树中的终结符可以是 const 或非 const 引用,或者它们可能根本不是引用。在编写语法时,您通常不必担心这一点,因为 proto::matches<>
在匹配终结符时会给您一些回旋余地。诸如 proto::terminal<int>
之类的语法将匹配类型为 int
、int &
或 int const &
的终结符。
您可以显式指定要匹配引用类型。如果这样做,则类型必须完全匹配。例如,诸如 proto::terminal<int &>
之类的语法将仅匹配 int &
。它将不匹配 int
或 int const &
。
下表显示了 Proto 如何匹配终结符。简单的规则是:如果您只想匹配引用类型,则必须在语法中指定引用。否则,请忽略它,Proto 将忽略 const 和引用。
表 30.7. proto::matches<> 和终结符的引用 / CV 限定符
终结符 |
语法 |
匹配? |
---|---|---|
T |
T |
是 |
T & |
T |
是 |
T const & |
T |
是 |
T |
T & |
否 |
T & |
T & |
是 |
T const & |
T & |
否 |
T |
T const & |
否 |
T & |
T const & |
否 |
T const & |
T const & |
是 |
这引出了一个问题:如果您想匹配 int
,但不匹配 int &
或 int const &
呢?为了强制精确匹配,Proto 提供了 proto::exact<>
模板。例如,proto::terminal< proto::exact<int> >
将仅匹配按值保存的 int
。
当匹配数组类型时,Proto 为您提供了额外的回旋余地。数组类型匹配它们自身或它们衰减成的指针类型。这对于字符数组尤其有用。proto::as_expr("hello")
返回的类型是 proto::terminal<char const[6]>::type
。这是一个包含 6 元素字符数组的终结符。自然地,您可以使用语法 proto::terminal<char const[6]>
匹配此终结符,但语法 proto::terminal<char const *>
也将匹配它,如下面的代码片段所示。
struct CharString : proto::terminal< char const * > {}; typedef proto::terminal< char const[6] >::type char_array; BOOST_MPL_ASSERT(( proto::matches< char_array, CharString > ));
如果我们只希望 CharString
匹配类型正好是 char const *
的终结符,该怎么办?您可以使用 proto::exact<>
在此处关闭终结符的模糊匹配,如下所示:
struct CharString : proto::terminal< proto::exact< char const * > > {}; typedef proto::terminal<char const[6]>::type char_array; typedef proto::terminal<char const *>::type char_string; BOOST_MPL_ASSERT(( proto::matches< char_string, CharString > )); BOOST_MPL_ASSERT_NOT(( proto::matches< char_array, CharString > ));
现在,CharString
不匹配数组类型,只匹配字符字符串指针。
相反的问题有点棘手:如果您想匹配所有字符数组,但不匹配字符指针,该怎么办?如上所述,表达式 as_expr("hello")
的类型为 proto::terminal< char const[ 6 ] >::type
。如果您想匹配任意大小的字符数组,可以使用 proto::N
,这是一个数组大小通配符。以下语法将匹配任何字符串字面量:proto::terminal< char const[ proto::N ] >
。
有时,您在匹配终结符时需要更大的回旋余地。例如,也许您正在构建一个计算器 EDSL,并且希望允许任何可转换为 double
的终结符。为此,Proto 提供了 proto::convertible_to<>
模板。您可以这样使用它:proto::terminal< proto::convertible_to< double > >
。
还有一种对终结符执行模糊匹配的方法。考虑尝试匹配 std::complex<>
终结符的问题。您可以轻松匹配 std::complex<float>
或 std::complex<double>
,但是您将如何匹配 std::complex<>
的任何实例化?您可以在此处使用 proto::_
来解决此问题。这是匹配任何 std::complex<>
实例化的语法:
struct StdComplex : proto::terminal< std::complex< proto::_ > > {};
当给定这样的语法时,Proto 将解构语法和与之匹配的终结符,并查看是否可以匹配所有组成部分。
我们已经看到了如何使用表达式生成器(如 proto::terminal<>
和 proto::shift_right<>
)作为语法。我们还看到了 proto::or_<>
,我们可以使用它来表达一组备用语法。还有一些其他感兴趣的;特别是 proto::if_<>
、proto::and_<>
和 proto::not_<>
。
proto::not_<>
模板是最简单的。它将一个语法作为模板参数并逻辑地否定它;not_<Grammar>
将匹配 Grammar
不 匹配的任何表达式。
proto::if_<>
模板与 Proto 转换一起使用,后者针对表达式类型进行评估以查找匹配项。(Proto 转换将在稍后描述。)
proto::and_<>
模板类似于 proto::or_<>
,不同之处在于,为了使 proto::and_<>
匹配,proto::and_<>
的每个参数都必须匹配。例如,考虑上面使用 proto::exact<>
的 CharString
的定义。它可以不使用 proto::exact<>
编写,如下所示:
struct CharString : proto::and_< proto::terminal< proto::_ > , proto::if_< boost::is_same< proto::_value, char const * >() > > {};
这表示 CharString
必须是一个终结符,并且 其值类型必须与 char const *
相同。请注意 proto::if_<>
的模板参数:boost::is_same< proto::_value, char const * >()
。这是一个 Proto 转换,它将终结符的值类型与 char const *
进行比较。
proto::if_<>
模板有几个变体。除了 if_<Condition>
之外,您还可以说 if_<Condition, ThenGrammar>
和 if_<Condition, ThenGrammar, ElseGrammar>
。这些可以让您根据 Condition
选择一个子语法或另一个子语法。
当您的 Proto 语法变得庞大时,您将开始遇到 proto::or_<>
的一些可伸缩性问题,proto::or_<>
是您用来指定备用子语法的构造。首先,由于 C++ 的限制,proto::or_<>
只能接受一定数量的子语法,这由 BOOST_PROTO_MAX_LOGICAL_ARITY
宏控制。此宏默认为 8,您可以将其设置得更高,但这会加剧另一个可伸缩性问题:编译时间过长。使用 proto::or_<>
,备用子语法按顺序尝试 -- 就像一系列级联的 if
语句 -- 导致大量不必要的模板实例化。您更希望得到类似于 switch
的东西,它可以避免级联 if
语句的开销。proto::switch_<>
的目的就是如此;虽然不如 proto::or_<>
方便,但它可以提高较大语法的编译时间,并且对子语法的数量没有任意的固定限制。
让我们通过首先使用 proto::or_<>
编写一个大型语法,然后使用 proto::switch_<>
将其转换为等效语法,来说明如何使用 proto::switch_<>
:
// Here is a big, inefficient grammar struct ABigGrammar : proto::or_< proto::terminal<int> , proto::terminal<double> , proto::unary_plus<ABigGrammar> , proto::negate<ABigGrammar> , proto::complement<ABigGrammar> , proto::plus<ABigGrammar, ABigGrammar> , proto::minus<ABigGrammar, ABigGrammar> , proto::or_< proto::multiplies<ABigGrammar, ABigGrammar> , proto::divides<ABigGrammar, ABigGrammar> , proto::modulus<ABigGrammar, ABigGrammar> > > {};
以上可能是更精细的计算器 EDSL 的语法。请注意,由于子语法超过八个,我们不得不使用嵌套的 proto::or_<>
链接子语法 -- 这不是很友好。
proto::switch_<>
背后的思想是基于表达式的标签类型分派到处理该类型表达式的子语法。要使用 proto::switch_<>
,您需要定义一个结构体,其中包含一个嵌套的 case_<>
模板,该模板针对标签类型进行了专门化。可以使用 proto::switch_<>
表达上述语法,如下所示。下面将对其进行描述。
// Redefine ABigGrammar more efficiently using proto::switch_<> struct ABigGrammar; struct ABigGrammarCases { // The primary template matches nothing: template<typename Tag> struct case_ : proto::not_<_> {}; }; // Terminal expressions are handled here template<> struct ABigGrammarCases::case_<proto::tag::terminal> : proto::or_< proto::terminal<int> , proto::terminal<double> > {}; // Non-terminals are handled similarly template<> struct ABigGrammarCases::case_<proto::tag::unary_plus> : proto::unary_plus<ABigGrammar> {}; template<> struct ABigGrammarCases::case_<proto::tag::negate> : proto::negate<ABigGrammar> {}; template<> struct ABigGrammarCases::case_<proto::tag::complement> : proto::complement<ABigGrammar> {}; template<> struct ABigGrammarCases::case_<proto::tag::plus> : proto::plus<ABigGrammar, ABigGrammar> {}; template<> struct ABigGrammarCases::case_<proto::tag::minus> : proto::minus<ABigGrammar, ABigGrammar> {}; template<> struct ABigGrammarCases::case_<proto::tag::multiplies> : proto::multiplies<ABigGrammar, ABigGrammar> {}; template<> struct ABigGrammarCases::case_<proto::tag::divides> : proto::divides<ABigGrammar, ABigGrammar> {}; template<> struct ABigGrammarCases::case_<proto::tag::modulus> : proto::modulus<ABigGrammar, ABigGrammar> {}; // Define ABigGrammar in terms of ABigGrammarCases // using proto::switch_<> struct ABigGrammar : proto::switch_<ABigGrammarCases> {};
将表达式类型 E
与 proto::switch_<C>
匹配等效于将其与 C::case_<E::proto_tag>
匹配。通过分派表达式的标签类型,我们可以跳转到处理该类型表达式的子语法,跳过所有其他可能不匹配的子语法。如果对于特定标签类型没有 case_<>
的特化,我们将选择主模板。在本例中,主模板继承自 proto::not_<_>
,它不匹配任何表达式。
请注意处理终结符的特化:
// Terminal expressions are handled here template<> struct ABigGrammarCases::case_<proto::tag::terminal> : proto::or_< proto::terminal<int> , proto::terminal<double> > {};
proto::tag::terminal
类型本身不足以选择合适的子语法,因此我们使用 proto::or_<>
列出与终结符匹配的备用子语法。
![]() |
注意 |
---|---|
您可能很想 就地 定义您的
struct ABigGrammarCases { template<typename Tag> struct case_ : proto::not_<_> {}; // ERROR: not legal C++ template<> struct case_<proto::tag::terminal> /* ... */ };
不幸的是,由于某些神秘的原因,像这样 就地 定义显式嵌套特化是不合法的。但是,就地 定义部分特化是完全合法的,因此您可以添加一个额外的虚拟模板参数,该参数具有默认值,如下所示:
struct ABigGrammarCases { // Note extra "Dummy" template parameter here: template<typename Tag, int Dummy = 0> struct case_ : proto::not_<_> {}; // OK: "Dummy" makes this a partial specialization // instead of an explicit specialization. template<int Dummy> struct case_<proto::tag::terminal, Dummy> /* ... */ };
您可能会发现这比在其封闭结构体外部定义显式 |
并非所有 C++ 的可重载运算符都是一元或二元的。有一个奇怪的 operator()
-- 函数调用运算符 -- 它可以有任意数量的参数。同样,使用 Proto,您可以定义自己的“运算符”,这些运算符也可以接受两个以上的参数。因此,您的 Proto 表达式树中可能存在具有任意数量子节点的节点(最多
,这是可配置的)。您如何编写语法来匹配这样的节点呢?BOOST_PROTO_MAX_ARITY
对于这种情况,Proto 提供了 proto::vararg<>
类模板。其模板参数是一个语法,proto::vararg<>
将匹配该语法零次或多次。考虑一个名为 fun()
的 Proto 延迟函数,它可以接受零个或多个字符作为参数,如下所示:
struct fun_tag {}; struct FunTag : proto::terminal< fun_tag > {}; FunTag::type const fun = {{}}; // example usage: fun(); fun('a'); fun('a', 'b'); ...
下面是匹配 fun()
的所有允许调用的语法:
struct FunCall : proto::function< FunTag, proto::vararg< proto::terminal< char > > > {};
FunCall
语法使用 proto::vararg<>
来匹配零个或多个字符字面量作为 fun()
函数的参数。
作为另一个示例,您能猜出以下语法匹配什么吗?
struct Foo : proto::or_< proto::terminal< proto::_ > , proto::nary_expr< proto::_, proto::vararg< Foo > > > {};
这里有一个提示:proto::nary_expr<>
的第一个模板参数表示节点类型,任何其他模板参数表示子节点。答案是这是一个退化的语法,它匹配每个可能的表达式树,从根到叶。
在本节中,我们将看到如何使用 Proto 为您的 EDSL 定义语法,并使用它来验证表达式模板,为无效表达式提供简短、可读的编译时错误。
![]() |
提示 |
---|---|
您可能会认为这是一种倒退的做法。“如果 Proto 让我选择要重载的运算符,我的用户一开始就不会创建无效表达式,我根本不需要语法!” 这可能是真的,但有一些原因更倾向于这样做。 首先,它可以让您快速开发 EDSL -- 所有运算符都已经为您准备好了! -- 然后再担心无效语法。 其次,某些运算符可能只允许在 EDSL 中的特定上下文中使用。这很容易用语法表达,但很难用直接运算符重载来完成。 第三,使用 EDSL 语法标记无效表达式通常可以产生比手动选择重载运算符更好的错误信息。 第四,语法不仅可以用于验证。您可以使用您的语法来定义 树转换,将表达式模板转换为其他更有用的对象。 如果以上都不能说服您,您实际上 可以 使用 Proto 来控制在您的域中重载哪些运算符。要做到这一点,您需要定义一个语法! |
在前面的章节中,我们使用 Proto 定义了一个延迟评估计算器的 EDSL,它允许占位符、浮点字面量、加法、减法、乘法、除法和分组的任意组合。如果我们要用 EBNF 编写此 EDSL 的语法,它可能看起来像这样:
group ::= '(' expression ')' factor ::= double | '_1' | '_2' | group term ::= factor (('*' factor) | ('/' factor))* expression ::= term (('+' term) | ('-' term))*
这捕获了计算器的语法、结合性和优先级规则。使用 Proto 为我们的计算器 EDSL 编写语法甚至更简单。由于我们使用 C++ 作为宿主语言,因此我们受 C++ 运算符的结合性和优先级规则的约束。我们的语法可以假定它们。此外,在 C++ 中,分组已经通过使用括号为我们处理,因此我们不必将其编码到我们的语法中。
让我们开始声明语法的 forward declaration:
struct CalculatorGrammar;
此时它是一个不完整类型,但我们仍然可以使用它来定义语法的规则。让我们定义终结符的语法规则:
struct Double : proto::terminal< proto::convertible_to< double > > {}; struct Placeholder1 : proto::terminal< placeholder<0> > {}; struct Placeholder2 : proto::terminal< placeholder<1> > {}; struct Terminal : proto::or_< Double, Placeholder1, Placeholder2 > {};
现在让我们定义加法、减法、乘法和除法的规则。在这里,我们可以忽略结合性和优先级问题 -- C++ 编译器将为我们强制执行这一点。我们只需要强制运算符的参数本身必须符合我们上面 forward declaration 的 CalculatorGrammar
。
struct Plus : proto::plus< CalculatorGrammar, CalculatorGrammar > {}; struct Minus : proto::minus< CalculatorGrammar, CalculatorGrammar > {}; struct Multiplies : proto::multiplies< CalculatorGrammar, CalculatorGrammar > {}; struct Divides : proto::divides< CalculatorGrammar, CalculatorGrammar > {};
现在我们已经定义了语法的所有部分,我们可以定义 CalculatorGrammar
:
struct CalculatorGrammar : proto::or_< Terminal , Plus , Minus , Multiplies , Divides > {};
就是这样!现在我们可以使用 CalculatorGrammar
来强制表达式模板符合我们的语法。我们可以使用 proto::matches<>
和 BOOST_MPL_ASSERT()
为无效表达式发出可读的编译时错误,如下所示:
template< typename Expr > void evaluate( Expr const & expr ) { BOOST_MPL_ASSERT(( proto::matches< Expr, CalculatorGrammar > )); // ... }
现在您已经为您的 EDSL 编译器编写了前端,并且您已经了解了一些它产生的中间形式,现在是时候考虑如何处理中间形式了。这是您放置特定于域的算法和优化的地方。Proto 为您提供了两种评估和操作表达式模板的方法:上下文和转换。
proto::eval()
函数。它将行为与节点类型相关联。proto::eval()
遍历表达式并在每个节点调用您的上下文。评估表达式的两种方法!如何选择?由于上下文在很大程度上是过程性的,因此它们更容易理解和调试,因此它们是一个很好的起点。但是,尽管转换更高级,但它们也更强大;由于它们与语法中的规则相关联,因此您可以根据子表达式的整个 结构 而不是仅仅根据其最顶层节点的类型来选择适当的转换。
此外,转换具有简洁而声明式的语法,乍一看可能会令人困惑,但一旦您习惯了它,就会变得非常富有表现力和可互换性。而且 -- 诚然,这非常主观 -- 作者发现使用 Proto 转换进行编程非常有趣! 您的体验可能会有所不同。
一旦你构建了一个 Proto 表达式树,无论是通过使用 Proto 的运算符重载还是使用 proto::make_expr()
及其友元函数,你可能想要真正地 用它做 一些事情。最简单的选择是使用 proto::eval()
,一个通用的表达式求值器。要使用 proto::eval()
,你需要定义一个 上下文,告诉 proto::eval()
每个节点应该如何求值。本节将详细介绍如何使用 proto::eval()
,定义求值上下文,以及使用 Proto 提供的上下文。
![]() |
注意 |
---|---|
|
概要
namespace proto { namespace result_of { // A metafunction for calculating the return // type of proto::eval() given certain Expr // and Context types. template<typename Expr, typename Context> struct eval { typedef typename Context::template eval<Expr>::result_type type; }; } namespace functional { // A callable function object type for evaluating // a Proto expression with a certain context. struct eval : callable { template<typename Sig> struct result; template<typename Expr, typename Context> typename proto::result_of::eval<Expr, Context>::type operator ()(Expr &expr, Context &context) const; template<typename Expr, typename Context> typename proto::result_of::eval<Expr, Context>::type operator ()(Expr &expr, Context const &context) const; }; } template<typename Expr, typename Context> typename proto::result_of::eval<Expr, Context>::type eval(Expr &expr, Context &context); template<typename Expr, typename Context> typename proto::result_of::eval<Expr, Context>::type eval(Expr &expr, Context const &context); }
给定一个表达式和一个求值上下文,使用 proto::eval()
非常简单。只需将表达式和上下文传递给 proto::eval()
,它就会完成剩下的工作并返回结果。你可以使用 eval<>
元函数在 proto::result_of
命名空间中计算 proto::eval()
的返回类型。以下演示了 proto::eval()
的用法
template<typename Expr> typename proto::result_of::eval<Expr const, MyContext>::type MyEvaluate(Expr const &expr) { // Some user-defined context type MyContext ctx; // Evaluate an expression with the context return proto::eval(expr, ctx); }
proto::eval()
所做的事情也非常简单。它将大部分工作委托给上下文本身。以下本质上是 proto::eval()
的实现
// eval() dispatches to a nested "eval<>" function // object within the Context: template<typename Expr, typename Context> typename Context::template eval<Expr>::result_type eval(Expr &expr, Context &ctx) { typename Context::template eval<Expr> eval_fun; return eval_fun(expr, ctx); }
实际上,proto::eval()
只不过是一个薄包装器,它分派到上下文类中的适当处理程序。在下一节中,我们将看到如何从头开始实现上下文类。
正如我们在上一节中看到的,proto::eval()
函数实际上没有太多内容。相反,所有有趣的表达式求值都发生在上下文类中。本节展示了如何从头开始实现一个上下文类。
所有上下文类都大致具有以下形式
// A prototypical user-defined context. struct MyContext { // A nested eval<> class template template< typename Expr , typename Tag = typename proto::tag_of<Expr>::type > struct eval; // Handle terminal nodes here... template<typename Expr> struct eval<Expr, proto::tag::terminal> { // Must have a nested result_type typedef. typedef ... result_type; // Must have a function call operator that takes // an expression and the context. result_type operator()(Expr &expr, MyContext &ctx) const { return ...; } }; // ... other specializations of struct eval<> ... };
上下文类只不过是一个嵌套的 eval<>
类模板的特化集合。每个特化处理不同的表达式类型。
在 Hello 计算器 章节中,我们看到了一个用户定义的上下文类的示例,用于求值计算器表达式。该上下文类是在 Proto 的 proto::callable_context<>
的帮助下实现的。如果我们从头开始实现它,它看起来会像这样
// The calculator_context from the "Hello Calculator" section, // implemented from scratch. struct calculator_context { // The values with which we'll replace the placeholders std::vector<double> args; template< typename Expr // defaulted template parameters, so we can // specialize on the expressions that need // special handling. , typename Tag = typename proto::tag_of<Expr>::type , typename Arg0 = typename proto::child_c<Expr, 0>::type > struct eval; // Handle placeholder terminals here... template<typename Expr, int I> struct eval<Expr, proto::tag::terminal, placeholder<I> > { typedef double result_type; result_type operator()(Expr &, MyContext &ctx) const { return ctx.args[I]; } }; // Handle other terminals here... template<typename Expr, typename Arg0> struct eval<Expr, proto::tag::terminal, Arg0> { typedef double result_type; result_type operator()(Expr &expr, MyContext &) const { return proto::child(expr); } }; // Handle addition here... template<typename Expr, typename Arg0> struct eval<Expr, proto::tag::plus, Arg0> { typedef double result_type; result_type operator()(Expr &expr, MyContext &ctx) const { return proto::eval(proto::left(expr), ctx) + proto::eval(proto::right(expr), ctx); } }; // ... other eval<> specializations for other node types ... };
现在我们可以将 proto::eval()
与上面的上下文类一起使用,以如下方式求值计算器表达式
// Evaluate an expression with a calculator_context calculator_context ctx; ctx.args.push_back(5); ctx.args.push_back(6); double d = proto::eval(_1 + _2, ctx); assert(11 == d);
以这种方式从头开始定义上下文是乏味且冗长的,但它可以让你完全控制表达式的求值方式。在 Hello 计算器 示例中的上下文类要简单得多。在下一节中,我们将看到 Proto 提供的辅助类,以简化实现上下文类的工作。
Proto 提供了一些现成的上下文类,你可以直接使用,或者可以在实现自己的上下文时使用它们作为辅助。它们是:
default_context
一个求值上下文,它为所有运算符分配通常的 C++ 含义。例如,加法节点通过求值左右子节点,然后将结果相加来处理。proto::default_context
使用 Boost.Typeof 推导出它求值的表达式的类型。
null_context
一个简单的上下文,它递归地求值子节点,但不以任何方式组合结果,并返回 void。
callable_context<>
一个辅助工具,简化了编写上下文类的工作。使用 proto::callable_context<>
,你无需编写模板特化,而是编写一个带有重载函数调用运算符的函数对象。任何未被重载处理的表达式都会自动分派到你可以指定的默认求值上下文。
proto::default_context
是一个求值上下文,它为所有运算符分配通常的 C++ 含义。例如,加法节点通过求值左右子节点,然后将结果相加来处理。proto::default_context
使用 Boost.Typeof 推导出它求值的表达式的类型。
例如,考虑以下 “Hello World” 示例
#include <iostream> #include <boost/proto/proto.hpp> #include <boost/proto/context.hpp> #include <boost/typeof/std/ostream.hpp> using namespace boost; proto::terminal< std::ostream & >::type cout_ = { std::cout }; template< typename Expr > void evaluate( Expr const & expr ) { // Evaluate the expression with default_context, // to give the operators their C++ meanings: proto::default_context ctx; proto::eval(expr, ctx); } int main() { evaluate( cout_ << "hello" << ',' << " world" ); return 0; }
该程序输出以下内容
hello, world
proto::default_context
是根据 default_eval<>
模板简单定义的,如下所示
// Definition of default_context struct default_context { template<typename Expr> struct eval : default_eval< Expr , default_context const , typename tag_of<Expr>::type > {}; };
有很多 default_eval<>
特化,每个特化处理不同的 C++ 运算符。例如,这是二元加法的特化
// A default expression evaluator for binary addition template<typename Expr, typename Context> struct default_eval<Expr, Context, proto::tag::plus> { private: static Expr & s_expr; static Context & s_ctx; public: typedef decltype( proto::eval(proto::child_c<0>(s_expr), s_ctx) + proto::eval(proto::child_c<1>(s_expr), s_ctx) ) result_type; result_type operator ()(Expr &expr, Context &ctx) const { return proto::eval(proto::child_c<0>(expr), ctx) + proto::eval(proto::child_c<1>(expr), ctx); } };
上面的代码使用 decltype
来计算函数调用运算符的返回类型。decltype
是下一个 C++ 版本中的新关键字,用于获取任何表达式的类型。大多数编译器尚不支持直接使用 decltype
,因此 default_eval<>
使用 Boost.Typeof 库来模拟它。在某些编译器上,这可能意味着 default_context
要么无法工作,要么需要你向 Boost.Typeof 库注册你的类型。查看 Boost.Typeof 的文档以了解详情。
proto::null_context<>
是一个简单的上下文,它递归地求值子节点,但不以任何方式组合结果,并返回 void。它与 callable_context<>
结合使用很有用,或者在定义你自己的上下文时,这些上下文就地修改表达式树,而不是累积结果,我们将在下面看到。
proto::null_context<>
是根据 null_eval<>
简单实现的,如下所示
// Definition of null_context struct null_context { template<typename Expr> struct eval : null_eval<Expr, null_context const, Expr::proto_arity::value> {}; };
并且 null_eval<>
也被简单地实现。例如,这是一个二元 null_eval<>
// Binary null_eval<> template<typename Expr, typename Context> struct null_eval<Expr, Context, 2> { typedef void result_type; void operator()(Expr &expr, Context &ctx) const { proto::eval(proto::child_c<0>(expr), ctx); proto::eval(proto::child_c<1>(expr), ctx); } };
这些类在什么情况下有用?假设你有一个带有整数终结符的表达式树,并且你想就地递增每个整数。你可以定义一个如下所示的求值上下文
struct increment_ints { // By default, just evaluate all children by delegating // to the null_eval<> template<typename Expr, typename Arg = proto::result_of::child<Expr>::type> struct eval : null_eval<Expr, increment_ints const> {}; // Increment integer terminals template<typename Expr> struct eval<Expr, int> { typedef void result_type; void operator()(Expr &expr, increment_ints const &) const { ++proto::child(expr); } }; };
在下一节关于 proto::callable_context<>
的内容中,我们将看到一种更简单的方法来实现相同的事情。
proto::callable_context<>
是一个辅助工具,简化了编写上下文类的工作。使用 proto::callable_context<>
,你无需编写模板特化,而是编写一个带有重载函数调用运算符的函数对象。任何未被重载处理的表达式都会自动分派到你可以指定的默认求值上下文。
与其说 proto::callable_context<>
本身是一个求值上下文,不如说它更像是一个上下文适配器。要使用它,你必须定义自己的上下文,该上下文继承自 proto::callable_context<>
。
在 null_context
章节中,我们看到了如何实现一个求值上下文,该上下文递增表达式树中所有整数。以下是如何使用 proto::callable_context<>
做同样的事情
// An evaluation context that increments all // integer terminals in-place. struct increment_ints : callable_context< increment_ints const // derived context , null_context const // fall-back context > { typedef void result_type; // Handle int terminals here: void operator()(proto::tag::terminal, int &i) const { ++i; } };
使用这样的上下文,我们可以执行以下操作
literal<int> i = 0, j = 10; proto::eval( i - j * 3.14, increment_ints() ); std::cout << "i = " << i.get() << std::endl; std::cout << "j = " << j.get() << std::endl;
此程序输出以下内容,表明整数 i
和 j
已递增 1
i = 1 j = 11
在 increment_ints
上下文中,我们不必定义任何嵌套的 eval<>
模板。这是因为 proto::callable_context<>
为我们实现了它们。proto::callable_context<>
接受两个模板参数:派生上下文和回退上下文。对于正在求值的表达式树中的每个节点,proto::callable_context<>
检查派生上下文中是否存在接受它的重载 operator()
。给定类型为 Expr
的某个表达式 expr
和上下文 ctx
,它尝试调用
ctx( typename Expr::proto_tag() , proto::child_c<0>(expr) , proto::child_c<1>(expr) ... );
通过使用函数重载和元编程技巧,proto::callable_context<>
可以在编译时检测是否存在这样的函数。如果存在,则调用该函数。如果不存在,则将当前表达式传递给回退求值上下文进行处理。
当我们查看简单计算器表达式求值器时,我们看到了 proto::callable_context<>
的另一个示例。在那里,我们想要自定义占位符终结符的求值,并将所有其他节点的处理委托给 proto::default_context
。我们这样做如下:
// An evaluation context for calculator expressions that // explicitly handles placeholder terminals, but defers the // processing of all other nodes to the default_context. struct calculator_context : proto::callable_context< calculator_context const > { std::vector<double> args; // Define the result type of the calculator. typedef double result_type; // Handle the placeholders: template<int I> double operator()(proto::tag::terminal, placeholder<I>) const { return this->args[I]; } };
在这种情况下,我们没有指定回退上下文。在这种情况下,proto::callable_context<>
使用 proto::default_context
。使用上面的 calculator_context
和几个适当定义的占位符终结符,我们可以求值计算器表达式,如下所示
template<int I> struct placeholder {}; terminal<placeholder<0> >::type const _1 = {{}}; terminal<placeholder<1> >::type const _2 = {{}}; // ... calculator_context ctx; ctx.args.push_back(4); ctx.args.push_back(5); double j = proto::eval( (_2 - _1) / _2 * 100, ctx ); std::cout << "j = " << j << std::endl;
上面的代码显示以下内容
j = 20
如果你曾经借助像 Antlr、yacc 或 Boost.Spirit 这样的工具构建过解析器,你可能熟悉 语义动作。除了允许你定义解析器识别的语言的文法之外,这些工具还允许你将代码嵌入到你的文法中,这些代码在文法的某些部分参与解析时执行。Proto 具有与语义动作等效的功能。它们被称为 转换。本节介绍如何将转换嵌入到你的 Proto 文法中,将你的文法变成函数对象,这些函数对象可以以强大的方式操作或求值表达式。
Proto 转换是一个高级主题。我们将放慢速度,使用示例来说明关键概念,从简单的开始。
到目前为止,我们看到的 Proto 文法是静态的。你可以在编译时检查表达式类型是否与文法匹配,但仅此而已。当你赋予它们运行时行为时,事情会变得更有趣。带有嵌入式转换的文法不仅仅是一个静态文法。它是一个函数对象,它接受与文法匹配的表达式,并对它们 做一些 事情。
下面是一个非常简单的文法。它匹配终结符表达式。
// A simple Proto grammar that matches all terminals proto::terminal< _ >
这是相同的文法,带有一个从终结符中提取值的转换
// A simple Proto grammar that matches all terminals // *and* a function object that extracts the value from // the terminal proto::when< proto::terminal< _ > , proto::_value // <-- Look, a transform! >
你可以这样理解:当你匹配一个终结符表达式时,提取该值。类型 proto::_value
是所谓的转换。稍后我们将看到是什么使它成为转换,但现在只需将其视为一种函数对象。请注意 proto::when<>
的用法:第一个模板参数是要匹配的文法,第二个是要执行的转换。结果既是一个匹配终结符表达式的文法,又是一个接受终结符表达式并提取其值的函数对象。
与普通文法一样,我们可以定义一个空结构体,该结构体继承自文法+转换,以便我们轻松地引用我们正在定义的事物,如下所示
// A grammar and a function object, as before struct Value : proto::when< proto::terminal< _ > , proto::_value > {}; // "Value" is a grammar that matches terminal expressions BOOST_MPL_ASSERT(( proto::matches< proto::terminal<int>::type, Value > )); // "Value" also defines a function object that accepts terminals // and extracts their value. proto::terminal<int>::type answer = {42}; Value get_value; int i = get_value( answer );
如前所述,Value
是一个匹配终结符表达式的文法和一个操作终结符表达式的函数对象。将非终结符表达式传递给 Value
函数对象将是一个错误。这是带有转换的文法的一般属性;当将它们用作函数对象时,传递给它们的表达式必须与文法匹配。
Proto 文法是有效的 TR1 风格的函数对象。这意味着你可以使用 boost::result_of<>
来询问文法,给定特定的表达式类型,其返回类型将是什么。例如,我们可以按如下方式访问 Value
文法的返回类型
// We can use boost::result_of<> to get the return type // of a Proto grammar. typedef typename boost::result_of<Value(proto::terminal<int>::type)>::type result_type; // Check that we got the type we expected BOOST_MPL_ASSERT(( boost::is_same<result_type, int> ));
![]() |
注意 |
---|---|
带有嵌入式转换的文法既是文法又是函数对象。将这些东西称为 “带有转换的文法” 会很乏味。我们可以将它们称为 “活动文法” 之类的东西,但正如我们将看到的,你可以用 Proto 定义的 每个 文法都是 “活动” 的;也就是说,每个文法在用作函数对象时都具有某种行为。因此,我们将继续将这些东西称为普通的 “文法”。术语 “转换” 保留用于用作 |
大多数文法比上一节中的文法稍微复杂一些。为了说明,让我们定义一个相当荒谬的文法,它匹配任何表达式并递归到最左边的终结符并返回其值。它将演示 Proto 文法的两个关键概念——选择和递归——如何与转换交互。文法描述如下。
// A grammar that matches any expression, and a function object // that returns the value of the leftmost terminal. struct LeftmostLeaf : proto::or_< // If the expression is a terminal, return its value proto::when< proto::terminal< _ > , proto::_value > // Otherwise, it is a non-terminal. Return the result // of invoking LeftmostLeaf on the 0th (leftmost) child. , proto::when< _ , LeftmostLeaf( proto::_child0 ) > > {}; // A Proto terminal wrapping std::cout proto::terminal< std::ostream & >::type cout_ = { std::cout }; // Create an expression and use LeftmostLeaf to extract the // value of the leftmost terminal, which will be std::cout. std::ostream & sout = LeftmostLeaf()( cout_ << "the answer: " << 42 << '\n' );
我们之前已经见过 proto::or_<>
。在这里,它扮演着两个角色。首先,它是一个文法,它匹配其任何备用子文法;在本例中,要么是终结符,要么是非终结符。其次,它也是一个函数对象,它接受一个表达式,找到与该表达式匹配的备用子文法,并应用其转换。并且由于 LeftmostLeaf
继承自 proto::or_<>
,因此 LeftmostLeaf
既是文法又是函数对象。
![]() |
注意 |
---|---|
第二个备用项使用 |
下一节将进一步描述此文法。
在前一节中定义的文法中,与非终结符关联的转换看起来有点奇怪
proto::when< _ , LeftmostLeaf( proto::_child0 ) // <-- a "callable" transform >
它的效果是接受非终结符表达式,获取第 0 个(最左边的)子节点,并在其上递归调用 LeftmostLeaf
函数。但是 LeftmostLeaf( proto::_child0 )
实际上是一个 函数类型。从字面上看,它是接受 proto::_child0
类型的对象并返回 LeftmostLeaf
类型的对象的函数的类型。那么我们如何理解这个转换呢?显然,实际上没有具有此签名的函数,这样的函数也没有用。关键在于理解 proto::when<>
如何 解释 其第二个模板参数。
当 proto::when<>
的第二个模板参数是函数类型时,proto::when<>
将函数类型解释为转换。在这种情况下,LeftmostLeaf
被视为要调用的函数对象的类型,而 proto::_child0
被视为转换。首先,proto::_child0
应用于当前表达式(与此备用子文法匹配的非终结符),结果(第 0 个子节点)作为参数传递给 LeftmostLeaf
。
![]() |
注意 |
---|---|
转换是一种特定领域的语言
|
类型 LeftmostLeaf( proto::_child0 )
是 可调用转换 的一个示例。它是一种函数类型,表示要调用的函数对象及其参数。类型 proto::_child0
和 proto::_value
是 原始转换。它们是普通的结构体,与函数对象非常相似,可调用转换可以从中组合出来。还有另一种类型的转换,对象转换,我们将在接下来遇到。
我们看到的第一个转换只是提取终结符的值。让我们做同样的事情,但这次我们将首先将所有 int 提升为 long。(请原谅到目前为止的示例的牵强性;稍后它们会变得更有趣。)这是文法
// A simple Proto grammar that matches all terminals, // and a function object that extracts the value from // the terminal, promoting ints to longs: struct ValueWithPomote : proto::or_< proto::when< proto::terminal< int > , long(proto::_value) // <-- an "object" transform > , proto::when< proto::terminal< _ > , proto::_value > > {};
你可以将上面的文法理解为:当你匹配一个 int 终结符时,从终结符中提取值并使用它来初始化一个 long;否则,当你匹配另一种类型的终结符时,只需提取值。类型 long(proto::_value)
是所谓的 对象 转换。它看起来像是创建了一个临时的 long,但它实际上是一个函数类型。正如可调用转换是一种函数类型,表示要调用的函数及其参数一样,对象转换是一种函数类型,表示要构造的对象及其构造函数的参数。
![]() |
注意 |
---|---|
对象转换 vs. 可调用转换 当使用函数类型作为 Proto 转换时,它们可以表示要构造的对象或要调用的函数。这类似于 “正常” 的 C++,其中语法
LeftmostLeaf(proto::_child0) // <-- a callable transform long(proto::_value) // <-- an object transform
Proto 通常无法知道哪个是哪个,因此它使用一个特征 |
现在我们已经掌握了 Proto 转换的基础知识,让我们考虑一个稍微更实际的例子。我们可以使用转换来提高 计算器 EDSL 的类型安全性。如果你还记得,它允许你编写涉及像 _1
和 _2
这样的参数占位符的中缀算术表达式,并将它们作为函数对象传递给 STL 算法,如下所示
double a1[4] = { 56, 84, 37, 69 }; double a2[4] = { 65, 120, 60, 70 }; double a3[4] = { 0 }; // Use std::transform() and a calculator expression // to calculate percentages given two input sequences: std::transform(a1, a1+4, a2, a3, (_2 - _1) / _2 * 100);
这样做是因为我们为计算器表达式提供了一个 operator()
,它可以求值表达式,用 operator()
的参数替换占位符。重载的 calculator<>::operator()
看起来像这样
// Overload operator() to invoke proto::eval() with // our calculator_context. template<typename Expr> double calculator<Expr>::operator()(double a1 = 0, double a2 = 0) const { calculator_context ctx; ctx.args.push_back(a1); ctx.args.push_back(a2); return proto::eval(*this, ctx); }
虽然这可行,但它并不理想,因为它不会在用户为计算器表达式提供过多或过少参数时发出警告。考虑以下错误
(_1 * _1)(4, 2); // Oops, too many arguments! (_2 * _2)(42); // Oops, too few arguments!
表达式 _1 * _1
定义了一个一元计算器表达式;它接受一个参数并对其求平方。如果我们传递多个参数,则额外的参数将被静默忽略,这可能会让用户感到惊讶。下一个表达式 _2 * _2
定义了一个二元计算器表达式;它接受两个参数,忽略第一个并对第二个求平方。如果我们只传递一个参数,则代码会静默地将 0.0
填充为第二个参数,这可能也不是用户期望的。可以做些什么呢?
我们可以说计算器表达式的 元数 是它期望的参数数量,并且它等于表达式中最大的占位符。因此,_1 * _1
的元数为 1,_2 * _2
的元数为 2。我们可以通过确保表达式的元数等于提供的实际参数数量来提高计算器 EDSL 的类型安全性。借助 Proto 转换,计算表达式的元数很简单。
用文字描述如何计算表达式的元数很简单。考虑一下,计算器表达式可以由 _1
、_2
、字面量、一元表达式和二元表达式组成。下表显示了这 5 个组成部分的每一个的元数。
使用此信息,我们可以编写计算器表达式的文法,并附加转换以计算每个组成部分的元数。下面的代码使用 Boost MPL 库中的整数包装器和元函数将表达式元数计算为编译时整数。文法描述如下。
struct CalcArity : proto::or_< proto::when< proto::terminal< placeholder<0> >, mpl::int_<1>() > , proto::when< proto::terminal< placeholder<1> >, mpl::int_<2>() > , proto::when< proto::terminal<_>, mpl::int_<0>() > , proto::when< proto::unary_expr<_, CalcArity>, CalcArity(proto::_child) > , proto::when< proto::binary_expr<_, CalcArity, CalcArity>, mpl::max<CalcArity(proto::_left), CalcArity(proto::_right)>() > > {};
当我们找到占位符终结符或字面量时,我们使用 对象转换,例如 mpl::int_<1>()
来创建一个(默认构造的)编译时整数,表示该终结符的元数。
对于一元表达式,我们使用 CalcArity(proto::_child)
,这是一个 可调用转换,它计算表达式子节点的元数。
二元表达式的转换有一些新技巧。让我们仔细看看
// Compute the left and right arities and // take the larger of the two. mpl::max<CalcArity(proto::_left), CalcArity(proto::_right)>()
这是一个对象转换;它默认构造 ... 到底是什么?
模板是一个 MPL 元函数,它接受两个编译时整数。它有一个嵌套的 mpl::max<>
typedef (未显示),它是两者的最大值。但是在这里,我们似乎传递给它两个不是编译时整数的东西;它们是 Proto 可调用转换。 Proto 足够智能,可以识别到这一点。它首先评估两个嵌套的可调用转换,计算左右子表达式的元数。然后它将结果整数放入 ::type
并通过请求嵌套的 mpl::max<>
来评估元函数。这就是默认构造并返回的对象的类型。::type
更一般地,当评估对象转换时,Proto 查看对象类型并检查它是否是模板特化,例如
。如果是,Proto 会查找它可以评估的嵌套转换。在任何嵌套转换被评估并替换回模板之后,新的模板特化就是结果类型,除非该类型具有嵌套的 mpl::max<>
,在这种情况下,它将成为结果。::type
现在我们可以计算计算器表达式的元数了,让我们重新定义我们在入门指南中编写的
表达式包装器,以使用 calculator<>
语法和 Boost.MPL 中的一些宏,以便在用户指定过多或过少的参数时发出编译时错误。CalcArity
// The calculator expression wrapper, as defined in the Hello // Calculator example in the Getting Started guide. It behaves // just like the expression it wraps, but with extra operator() // member functions that evaluate the expression. // NEW: Use the CalcArity grammar to ensure that the correct // number of arguments are supplied. template<typename Expr> struct calculator : proto::extends<Expr, calculator<Expr>, calculator_domain> { typedef proto::extends<Expr, calculator<Expr>, calculator_domain> base_type; calculator(Expr const &expr = Expr()) : base_type(expr) {} typedef double result_type; // Use CalcArity to compute the arity of Expr: static int const arity = boost::result_of<CalcArity(Expr)>::type::value; double operator()() const { BOOST_MPL_ASSERT_RELATION(0, ==, arity); calculator_context ctx; return proto::eval(*this, ctx); } double operator()(double a1) const { BOOST_MPL_ASSERT_RELATION(1, ==, arity); calculator_context ctx; ctx.args.push_back(a1); return proto::eval(*this, ctx); } double operator()(double a1, double a2) const { BOOST_MPL_ASSERT_RELATION(2, ==, arity); calculator_context ctx; ctx.args.push_back(a1); ctx.args.push_back(a2); return proto::eval(*this, ctx); } };
请注意使用
来访问 boost::result_of<>
函数对象的返回类型。由于我们在转换中使用了编译时整数,因此表达式的元数编码在 CalcArity
函数对象的返回类型中。 Proto 语法是有效的 TR1 风格的函数对象,因此您可以使用 CalcArity
来确定它们的返回类型。boost::result_of<>
通过我们编译时断言的设置,当用户为计算器表达式提供过多或过少的参数时,例如在
(_2 * _2)(42); // Oops, too few arguments!
... 他们将在带有断言的行上收到一个编译时错误消息,内容如下[29]
c:\boost\org\trunk\libs\proto\scratch\main.cpp(97) : error C2664: 'boost::mpl::asse rtion_failed' : cannot convert parameter 1 from 'boost::mpl::failed ************boo st::mpl::assert_relation<x,y,__formal>::************' to 'boost::mpl::assert<false> ::type' with [ x=1, y=2, __formal=bool boost::mpl::operator==(boost::mpl::failed,boost::mpl::failed) ]
此练习的目的是展示我们可以编写一个相当简单的 Proto 语法,其中嵌入了声明式和可读的转换,并且可以计算任意复杂表达式的有趣属性。但是转换可以做的更多。 Boost.Xpressive 使用转换将表达式转换为有限状态自动机以匹配正则表达式,而 Boost.Spirit 使用转换来构建递归下降解析器生成器。 Proto 附带了一系列内置转换,您可以使用它们来执行非常复杂的表达式操作,例如这些。在接下来的几节中,我们将看到其中的一些实际应用。
到目前为止,我们只看到了语法与转换的示例,这些转换接受一个参数:要转换的表达式。但是,请考虑一下,在普通的程序代码中,您将如何将二叉树转换为链表。您将从一个空列表开始。然后,您将递归地将右分支转换为列表,并在将左分支转换为列表时将结果用作初始状态。也就是说,您需要一个接受两个参数的函数:当前节点和到目前为止的列表。当处理树时,这些类型的累积问题非常常见。链表是累积变量或状态的示例。算法的每次迭代都采用当前元素和状态,将某个二元函数应用于两者,并创建一个新状态。在 STL 中,此算法称为
。在许多其他语言中,它被称为fold(折叠)。让我们看看如何使用 Proto 转换实现 fold 算法。std::accumulate()
除了要转换的表达式之外,所有 Proto 语法都可以选择性地接受状态参数。如果您想将树折叠成列表,则需要利用状态参数来传递您到目前为止构建的列表。至于列表,Boost.Fusion 库提供了一个
类型,您可以从中构建异构列表。类型 fusion::cons<>
表示一个空列表。fusion::nil
下面是一个语法,它可以识别像
这样的输出表达式,并将参数放入 Fusion 列表中。下面将对其进行解释。cout_ << 42 << '\n'
// Fold the terminals in output statements like // "cout_ << 42 << '\n'" into a Fusion cons-list. struct FoldToList : proto::or_< // Don't add the ostream terminal to the list proto::when< proto::terminal< std::ostream & > , proto::_state > // Put all other terminals at the head of the // list that we're building in the "state" parameter , proto::when< proto::terminal<_> , fusion::cons<proto::_value, proto::_state>( proto::_value, proto::_state ) > // For left-shift operations, first fold the right // child to a list using the current state. Use // the result as the state parameter when folding // the left child to a list. , proto::when< proto::shift_left<FoldToList, FoldToList> , FoldToList( proto::_left , FoldToList(proto::_right, proto::_state) ) > > {};
在继续阅读之前,看看您是否可以应用您已经了解的关于对象、可调用和原始转换的知识来弄清楚这个语法是如何工作的。
当您使用
函数时,您需要传递两个参数:要折叠的表达式和初始状态:一个空列表。这两个参数将传递给每个转换。我们之前了解到 FoldToList
是一个原始转换,它接受一个终端表达式并提取其值。我们直到现在才知道的是,它也接受当前状态并忽略它。 proto::_value
也是一个原始转换。它接受当前的表达式(它忽略该表达式)和当前的状态(它返回该状态)。proto::_state
当我们找到一个终端时,我们将其粘贴到 cons 列表的头部,使用当前状态作为列表的尾部。(第一个备选项导致
被跳过。我们不希望 ostream
在列表中。)当我们找到一个左移节点时,我们应用以下转换cout
// Fold the right child and use the result as // state while folding the right. FoldToList( proto::_left , FoldToList(proto::_right, proto::_state) )
您可以将此转换理解为:使用当前状态,将右子节点折叠成列表。在将左子节点折叠成列表时,将新列表用作状态。
![]() |
提示 |
---|---|
如果您的编译器是 Microsoft Visual C++,您会发现上面的转换无法编译。编译器在处理嵌套函数类型时存在错误。您可以通过将内部转换包装在
FoldToList( proto::_left , proto::call<FoldToList(proto::_right, proto::_state)> )
|
现在我们已经定义了
函数对象,我们可以使用它将输出表达式转换为列表,如下所示FoldToList
proto::terminal<std::ostream &>::type const cout_ = {std::cout}; // This is the type of the list we build below typedef fusion::cons< int , fusion::cons< double , fusion::cons< char , fusion::nil > > > result_type; // Fold an output expression into a Fusion list, using // fusion::nil as the initial state of the transformation. FoldToList to_list; result_type args = to_list(cout_ << 1 << 3.14 << '\n', fusion::nil()); // Now "args" is the list: {1, 3.14, '\n'}
在编写转换时,“折叠”是一个基本操作,Proto 提供了许多内置的折叠转换。我们稍后会讲到它们。现在,请放心,您不必总是如此费尽心思地去做如此基本的事情。
在上一节中,我们看到我们可以将第二个参数传递给具有转换的语法:一个累积变量或状态,它会在您的转换执行时更新。有时,您的转换需要访问不累积的辅助数据,因此将其与状态参数捆绑在一起是不切实际的。相反,您可以将辅助数据作为第三个参数传递,称为数据参数。
让我们修改之前的示例,以便在将每个终端放入列表之前,先将其写入
。例如,这对于调试您的转换可能很方便。我们可以通过在数据参数中传递 std::cout
来使其通用化。在转换本身中,我们可以使用 std::ostream
转换来检索 proto::_data
。策略如下:使用 ostream
转换来链接两个操作。第二个操作将像以前一样创建 proto::and_<>
节点。但是,第一个操作将显示当前表达式。为此,我们首先构造一个 fusion::cons<>
实例,然后调用它。proto::functional::display_expr
// Fold the terminals in output statements like // "cout_ << 42 << '\n'" into a Fusion cons-list. struct FoldToList : proto::or_< // Don't add the ostream terminal to the list proto::when< proto::terminal< std::ostream & > , proto::_state > // Put all other terminals at the head of the // list that we're building in the "state" parameter , proto::when< proto::terminal<_> , proto::and_< // First, write the terminal to an ostream passed // in the data parameter proto::lazy< proto::make<proto::functional::display_expr(proto::_data)>(_) > // Then, constuct the new cons list. , fusion::cons<proto::_value, proto::_state>( proto::_value, proto::_state ) > > // For left-shift operations, first fold the right // child to a list using the current state. Use // the result as the state parameter when folding // the left child to a list. , proto::when< proto::shift_left<FoldToList, FoldToList> , FoldToList( proto::_left , FoldToList(proto::_right, proto::_state, proto::_data) , proto::_data ) > > {};
毫无疑问,这需要理解很多内容。但是请关注上面的第二个
子句。它表示:当您找到一个终端时,首先使用您在数据参数中找到的 when
显示该终端,然后获取终端的值和当前状态以构建一个新的 ostream
列表。函数对象 cons
完成打印终端的工作,而 display_expr
将操作链接在一起并按顺序执行它们,返回最后一个操作的结果。proto::and_<>
![]() |
注意 |
---|---|
新的还有 |
我们可以像以前一样使用上面的转换,但现在我们可以将
作为第三个参数传递,并观看转换的实际操作。这是一个示例用法ostream
proto::terminal<std::ostream &>::type const cout_ = {std::cout}; // This is the type of the list we build below typedef fusion::cons< int , fusion::cons< double , fusion::cons< char , fusion::nil > > > result_type; // Fold an output expression into a Fusion list, using // fusion::nil as the initial state of the transformation. // Pass std::cout as the data parameter so that we can track // the progress of the transform on the console. FoldToList to_list; result_type args = to_list(cout_ << 1 << 3.14 << '\n', fusion::nil(), std::cout); // Now "args" is the list: {1, 3.14, '\n'}
此代码显示以下内容
terminal( ) terminal(3.14) terminal(1)
这是一种相当迂回的方式来演示您可以将额外的数据作为第三个参数传递给转换。对这个参数可以是什么没有限制,而且,与状态参数不同,Proto 永远不会干扰它。
![]() |
注意 |
---|---|
这是一个高级主题。如果您是 Proto 新手,请随意跳过。 |
上面的示例使用数据参数作为非结构化数据 blob 的传输机制;在本例中,是对
的引用。随着您的 Proto 算法变得越来越复杂,您可能会发现非结构化数据 blob 使用起来不是很方便。您的算法的不同部分可能对不同的数据位感兴趣。相反,您想要的是一种将环境变量集合传递给转换的方法,例如键/值对的集合。然后,您可以通过向数据参数询问与特定键关联的值来轻松获取您想要的数据片段。 Proto 的转换环境为您提供了这个功能。ostream
让我们首先定义一个键。
BOOST_PROTO_DEFINE_ENV_VAR(mykey_type, mykey);
这定义了一个全局常量
,类型为 mykey
。我们可以使用 mykey_type
将一块关联数据存储在转换环境中,如下所示mykey
// Call the MyEval algorithm with a transform environment containing // two key/value pairs: one for proto::data and one for mykey MyEval()( expr, state, (proto::data = 42, mykey = "hello world") );
上面意味着使用三个参数调用
算法:一个表达式、一个初始状态和一个包含两个键/值对的转换环境。MyEval
在 Proto 算法中,您可以使用
转换来访问与不同键关联的值。例如,proto::_env_var<>
将从上面创建的转换环境中获取值 proto::_env_var<mykey_type>
。"hello world"
转换有一些额外的智能。它不会总是返回第三个参数,而不管它是一个 blob 还是一个转换环境,而是首先检查它是否是一个 blob。如果是,则返回该 blob。如果不是,则返回与 proto::_data
键关联的值。在上面的示例中,这将是值 proto::data
。42
您可以使用少量函数、元函数和类来创建和操作转换环境,一些用于测试对象是否是转换环境,一些用于强制对象成为转换环境,还有一些用于查询转换环境是否具有特定键的值。有关该主题的详尽处理,请查看
标头的参考。boost/proto/transform/env.hpp
让我们使用前两节中的
示例来说明 Proto 转换的其他一些优点。我们已经看到,当语法用作函数对象时,它可以接受最多 3 个参数,并且当在可调用转换中使用这些语法时,您也可以指定最多 3 个参数。让我们再次看一下上一节中与非终端关联的转换FoldToList
FoldToList( proto::_left , FoldToList(proto::_right, proto::_state, proto::_data) , proto::_data )
在这里,我们为
语法的两次调用都指定了所有三个参数。但是我们不必指定所有三个参数。如果我们不指定第三个参数,则假定为 FoldToList
。第二个参数和 proto::_data
也是如此。因此,上面的转换可以更简单地写成proto::_state
FoldToList( proto::_left , StringCopy(proto::_right) )
对于任何原始转换也是如此。以下都是等效的
表 30.9. 原始转换的隐式参数
等效转换 |
---|
|
|
|
|
|
![]() |
注意 |
---|---|
语法是原始转换,原始转换是函数对象 到目前为止,我们已经说过所有 Proto 语法都是函数对象。但是更准确的说法是 Proto 语法是原始转换——一种特殊的函数对象,它接受 1 到 3 个参数,并且 Proto 知道在可调用转换中使用时对其进行特殊处理,如上表所示。 |
![]() |
注意 |
---|---|
并非所有函数对象都是原始转换 您现在可能很想为所有可调用转换删除 |
一旦您知道原始转换将始终接收所有三个参数——表达式、状态和数据——那么就使得原本不可能的事情成为可能。例如,考虑一下对于二元表达式,这两个转换是等效的。您能看出为什么吗?
表 30.10. 两个等效的转换
没有 |
有 |
---|---|
FoldToList( proto::_left , FoldToList(proto::_right, proto::_state, proto::_data) , proto::_data )
|
proto::reverse_fold<_, proto::_state, FoldToList>
|
处理具有任意数量子节点的表达式可能很麻烦。如果您想对每个子节点执行某些操作,然后将结果作为参数传递给其他函数,该怎么办?您能否只执行一次,而不必担心表达式有多少个子节点?是的。这就是 Proto 的解包表达式派上用场的地方。解包表达式为您提供了一种编写可调用和对象转换的方法,以处理n元表达式。
![]() |
注意 |
---|---|
灵感来自 C++11 可变参数模板 Proto 的解包表达式从 C++11 的同名功能中汲取灵感。如果您熟悉可变参数函数,尤其是如何展开函数参数包,那么此讨论应该会非常熟悉。但是,此功能实际上不使用任何 C++11 功能,因此此处描述的代码将适用于任何符合 C++98 标准的编译器。 |
Proto 具有内置的
转换,用于以类似 C++ 的方式评估 Proto 表达式。但是,如果没有它,使用 Proto 的解包模式从头开始实现一个也不会太难。下面的 proto::_default<>
转换就是这样做的。eval
// A callable polymorphic function object that takes an unpacked expression // and a tag, and evaluates the expression. A plus tag and two operands adds // them with operator +, for instance. struct do_eval : proto::callable { typedef double result_type; #define UNARY_OP(TAG, OP) \ template<typename Arg> \ double operator()(proto::tag::TAG, Arg arg) const \ { \ return OP arg; \ } \ /**/ #define BINARY_OP(TAG, OP) \ template<typename Left, typename Right> \ double operator()(proto::tag::TAG, Left left, Right right) const \ { \ return left OP right; \ } \ /**/ UNARY_OP(negate, -) BINARY_OP(plus, +) BINARY_OP(minus, -) BINARY_OP(multiplies, *) BINARY_OP(divides, /) /*... others ...*/ }; struct eval : proto::or_< // Evaluate terminals by simply returning their value proto::when<proto::terminal<_>, proto::_value> // Non-terminals are handled by unpacking the expression, // recursively calling eval on each child, and passing // the results along with the expression's tag to do_eval // defined above. , proto::otherwise<do_eval(proto::tag_of<_>(), eval(proto::pack(_))...)> // UNPACKING PATTERN HERE -------------------^^^^^^^^^^^^^^^^^^^^^^^^ > {};
上面代码的大部分致力于将标签类型映射到行为的
函数对象,但有趣的部分是底部 do_eval
算法的定义。终端的处理非常简单,但非终端可以是单unary、binary、ternary,甚至是n-ary(如果我们考虑函数调用表达式)。 eval
算法借助解包模式统一处理这种情况。eval
非终端使用此可调用转换进行评估
do_eval(proto::tag_of<_>(), eval(proto::pack(_))...)
您可以将其理解为:使用当前表达式的标签和所有子节点调用
函数对象,在每个子节点都使用 do_eval
评估之后。解包模式是省略号之前的位:eval
。eval(proto::pack(_))
这里发生的事情是这样的。解包表达式为当前正在评估的表达式中的每个子节点重复一次。在每次重复中,类型
被替换为 proto::pack(_)
。因此,如果将一元表达式传递给 proto::_child_c<N>
,它实际上会被评估为这样eval
// After the unpacking pattern is expanded for a unary expression do_eval(proto::tag_of<_>(), eval(proto::_child_c<0>))
当传递二元表达式时,解包模式展开成这样
// After the unpacking pattern is expanded for a binary expression do_eval(proto::tag_of<_>(), eval(proto::_child_c<0>), eval(proto::_child_c<1>))
虽然在我们的示例中不可能发生,但当传递终端时,解包模式展开,使其从终端而不是子节点中提取值。因此,它的处理方式如下
// If a terminal were passed to this transform, Proto would try // to evaluate it like this, which would fail: do_eval(proto::tag_of<_>(), eval(proto::_value))
这没有道理。
将返回一些不是 Proto 表达式的东西,并且 proto::_value
将无法评估它。 Proto 算法除非您将 Proto 表达式传递给它们,否则无法工作。eval
![]() |
注意 |
---|---|
回到老式风格 您可能在想,我的编译器不支持 C++11 可变参数模板!这怎么可能工作?答案很简单:上面的 |
解包模式非常具有表现力。任何可调用或对象转换都可以用作解包模式,只要
在其中某处恰好出现一次。这为您提供了很大的灵活性,可以控制在将表达式的子节点传递给某个函数对象或对象构造函数之前如何处理它们。proto::pack(_)
![]() |
注意 |
---|---|
这是一个高级主题,仅对于定义大型 EDSLs 的人员是必要的。如果您刚开始使用 Proto,请随意跳过此部分。 |
到目前为止,我们已经看到了带有嵌入式转换的语法的示例。在实践中,语法可能会变得非常庞大,您可能希望使用它们来驱动几种不同的计算。例如,您可能有一个线性代数领域的语法,并且您可能希望使用它来计算结果的形状(向量还是矩阵?)以及最佳地计算结果。您不希望必须复制和粘贴整个 shebang 只是为了调整其中一个嵌入式转换。相反,您想要的是定义一次语法,并在您准备评估表达式时稍后指定转换。为此,您可以使用外部转换。您将使用的模式是这样的:将语法中的一个或多个转换替换为特殊的占位符
。然后,您将创建一个转换束,当评估表达式时,您将在数据参数(表达式和状态之后的第三个参数)中将其传递给语法。proto::external_transform
为了说明外部转换,我们将构建一个计算器求值器,该求值器可以配置为在除以零时抛出异常。这是一个简单的前端,它定义了一个域、一个语法、一个表达式包装器和一些占位符终端。
#include <boost/assert.hpp> #include <boost/mpl/int.hpp> #include <boost/fusion/container/vector.hpp> #include <boost/fusion/container/generation/make_vector.hpp> #include <boost/proto/proto.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; namespace fusion = boost::fusion; // The argument placeholder type template<typename I> struct placeholder : I {}; // The grammar for valid calculator expressions struct calc_grammar : proto::or_< proto::terminal<placeholder<proto::_> > , proto::terminal<int> , proto::plus<calc_grammar, calc_grammar> , proto::minus<calc_grammar, calc_grammar> , proto::multiplies<calc_grammar, calc_grammar> , proto::divides<calc_grammar, calc_grammar> > {}; template<typename E> struct calc_expr; struct calc_domain : proto::domain<proto::generator<calc_expr> > {}; template<typename E> struct calc_expr : proto::extends<E, calc_expr<E>, calc_domain> { calc_expr(E const &e = E()) : calc_expr::proto_extends(e) {} }; calc_expr<proto::terminal<placeholder<mpl::int_<0> > >::type> _1; calc_expr<proto::terminal<placeholder<mpl::int_<1> > >::type> _2; int main() { // Build a calculator expression, and do nothing with it. (_1 + _2); }
现在,让我们将转换嵌入到
中,以便我们可以使用它来评估计算器表达式calc_grammar
// The calculator grammar with embedded transforms for evaluating expression. struct calc_grammar : proto::or_< proto::when< proto::terminal<placeholder<proto::_> > , proto::functional::at(proto::_state, proto::_value) > , proto::when< proto::terminal<int> , proto::_value > , proto::when< proto::plus<calc_grammar, calc_grammar> , proto::_default<calc_grammar> > , proto::when< proto::minus<calc_grammar, calc_grammar> , proto::_default<calc_grammar> > , proto::when< proto::multiplies<calc_grammar, calc_grammar> , proto::_default<calc_grammar> > , proto::when< proto::divides<calc_grammar, calc_grammar> , proto::_default<calc_grammar> > > {};
通过
的这个定义,我们可以通过传递一个 Fusion 向量来评估表达式,该向量包含用于 calc_grammar
和 _1
占位符的值_2
int result = calc_grammar()(_1 + _2, fusion::make_vector(3, 4)); BOOST_ASSERT(result == 7);
我们还想要一种替代的评估策略,该策略检查除以零并抛出异常。仅仅为了更改转换除法表达式的那一行而复制整个
会有多么可笑?!外部转换非常适合解决这个问题。calc_grammar
首先,我们在语法中给除法规则起一个“名称”;也就是说,我们将其设为一个结构体。稍后我们将使用这个唯一的类型来调度到正确的转换。
struct calc_grammar; struct divides_rule : proto::divides<calc_grammar, calc_grammar> {};
接下来,我们更改
以使除法表达式的处理外部化。calc_grammar
// The calculator grammar with an external transform for evaluating // division expressions. struct calc_grammar : proto::or_< /* ... as before ... */ , proto::when< divides_rule , proto::external_transform > > {};
使用上面的 proto::external_transform
可以使除法表达式的处理在外部可参数化。
接下来,我们使用 proto::external_transforms<>
(注意末尾的 's') 来捕获我们的求值策略,将其打包以便我们可以将其作为数据参数传递给转换。请继续阅读以了解详细解释。
// Evaluate division nodes as before struct non_checked_division : proto::external_transforms< proto::when< divides_rule, proto::_default<calc_grammar> > > {}; /* ... */ non_checked_division non_checked; int result2 = calc_grammar()(_1 / _2, fusion::make_vector(6, 2), non_checked);
结构体 non_cecked_division
将转换 proto::_default<calc_grammar>
与 divides_rule
语法规则关联起来。该结构体的一个实例在调用 calc_grammar
时作为第三个参数传递。
现在,让我们实现 checked division(带检查的除法)。其余部分应该不足为奇。
struct division_by_zero : std::exception {}; struct do_checked_divide : proto::callable { typedef int result_type; int operator()(int left, int right) const { if (right == 0) throw division_by_zero(); return left / right; } }; struct checked_division : proto::external_transforms< proto::when< divides_rule , do_checked_divide(calc_grammar(proto::_left), calc_grammar(proto::_right)) > > {}; /* ... */ try { checked_division checked; int result3 = calc_grammar_extern()(_1 / _2, fusion::make_vector(6, 0), checked); } catch(division_by_zero) { std::cout << "caught division by zero!\n"; }
上面的代码演示了如何将单个语法与外部指定的不同转换一起使用。这使得重用语法来驱动几种不同的计算成为可能。
如上所述,外部转换功能占用了数据参数,该参数旨在作为您可以传递任意数据的位置,并赋予其特定的含义。但是,如果您已经将数据参数用于其他用途怎么办?答案是使用转换环境。通过将您的外部转换与 proto::transforms
键关联,您可以自由地在其他槽中传递任意数据。
为了继续上面的例子,如果我们需要将一块数据与外部转换一起传递到我们的转换中,那会是什么样子呢?它会像这样:
int result3 = calc_grammar_extern()( _1 / _2 , fusion::make_vector(6, 0) , (proto::data = 42, proto::transforms = checked) );
在上面 calc_grammar_extern
算法的调用中,外部转换的映射与 proto::transforms
键关联,并在转换环境中传递给算法。转换环境中还包含一个键/值对,该键/值对将值 42
与 proto::data
键关联。
原始转换是构建更复杂的复合转换的基础模块。 Proto 定义了一系列通用的原始转换。 它们总结如下。
proto::_value
给定一个终端表达式,返回终端的值。
proto::_child_c<>
给定一个非终端表达式,proto::_child_c<
返回第 N
>N
个子节点。
proto::_child
是 proto::_child_c<0>
的同义词。
proto::_left
是 proto::_child_c<0>
的同义词。
proto::_right
是 proto::_child_c<1>
的同义词。
proto::_expr
返回当前表达式,不做修改。
proto::_state
返回当前状态,不做修改。
proto::_data
返回当前数据,不做修改。
proto::call<>
对于给定的可调用转换
,CT
proto::call<
将可调用转换转换为原始转换。 这对于区分可调用转换和对象转换很有用,也用于解决编译器在处理嵌套函数类型时的错误。CT
>
proto::make<>
对于给定的对象转换
,OT
proto::make<
将对象转换转换为原始转换。 这对于区分对象转换和可调用转换很有用,也用于解决编译器在处理嵌套函数类型时的错误。OT
>
proto::_default<>
给定一个语法 G
, proto::_default<
根据节点表示的操作的标准 C++ 含义评估当前节点。 例如,如果当前节点是二元加法节点,则将根据 G
>
评估两个子节点,并将结果相加并返回。 返回类型借助 Boost.Typeof 库推导。G
proto::fold<>
给定三个转换
、ET
和 ST
,FT
proto::fold<
首先评估 ET
, ST
, FT
>
以获得 Fusion 序列,并评估 ET
以获得 fold 的初始状态,然后为序列中的每个元素评估 ST
,以从前一个状态生成下一个状态。FT
proto::reverse_fold<>
类似于 proto::fold<>
,不同之处在于 Fusion 序列中的元素以相反的顺序迭代。
proto::fold_tree<>
类似于 proto::fold<
,不同之处在于 ET
, ST
, FT
>
转换的结果被视为表达式树,该表达式树被 扁平化 以生成要 fold 的序列。 扁平化表达式树会导致具有与父节点相同标签类型的子节点被放入序列中。 例如,ET
a >> b >> c
将被扁平化为序列 [a
, b
, c
],这将是要 fold 的序列。
proto::reverse_fold_tree<>
类似于 proto::fold_tree<>
,不同之处在于扁平化的序列以相反的顺序迭代。
proto::lazy<>
是 proto::make<>
和 proto::call<>
的组合,当转换的性质取决于表达式、状态和/或数据参数时,它非常有用。 proto::lazy<R(A0,A1...An)>
首先评估 proto::make<R()>
以计算可调用类型 R2
。 然后,它评估 proto::call<R2(A0,A1...An)>
。
除了上述原始转换之外,Proto 的所有语法元素也是原始转换。 它们的行为描述如下。
proto::_
返回当前表达式,不做修改。
proto::or_<>
对于指定的备选子语法集,找到与给定表达式匹配的语法,并应用其关联的转换。
proto::and_<>
对于给定的子语法集,应用所有关联的转换并返回最后一个的结果。
proto::not_<>
返回当前表达式,不做修改。
proto::if_<>
给定三个转换,评估第一个并将结果视为编译时布尔值。 如果为真,则评估第二个转换。 否则,评估第三个。
proto::switch_<>
与 proto::or_<>
类似,找到与给定表达式匹配的子语法,并应用其关联的转换。
proto::terminal<>
返回当前的终端表达式,不做修改。
proto::plus<>
, proto::nary_expr<>
, 等等。当用作原始转换时,Proto 语法(匹配非终端,例如 proto::plus<
)会创建一个新的 plus 节点,其中左子节点根据 G0
, G1
>
进行转换,右子节点根据 G0
进行转换。G1
请注意与语法元素(例如上面描述的 proto::plus<>
)关联的原始转换。 它们具有所谓的 透传 转换。 透传转换接受具有特定标签类型(例如,proto::tag::plus
)的表达式,并创建具有相同标签类型的新表达式,其中每个子表达式根据透传转换的相应子语法进行转换。 因此,例如,这个语法 ...
proto::function< X, proto::vararg<Y> >
... 匹配函数表达式,其中第一个子节点匹配 X
语法,其余子节点匹配 Y
语法。 当用作转换时,上面的语法将创建一个新的函数表达式,其中第一个子节点根据 X
进行转换,其余子节点根据 Y
进行转换。
Proto 中的以下类模板可以用作具有透传转换的语法
表 30.11. 具有透传转换的类模板
具有透传转换的模板 |
---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
我们已经看到 proto::terminal<>
、proto::plus<>
和 proto::nary_expr<>
等模板扮演着多种角色。 它们是生成表达式类型的元函数。 它们是匹配表达式类型的语法。 并且它们是原始转换。 以下代码示例显示了每个示例。
作为元函数 ...
// proto::terminal<> and proto::plus<> are metafunctions // that generate expression types: typedef proto::terminal<int>::type int_; typedef proto::plus<int_, int_>::type plus_; int_ i = {42}, j = {24}; plus_ p = {i, j};
作为语法 ...
// proto::terminal<> and proto::plus<> are grammars that // match expression types struct Int : proto::terminal<int> {}; struct Plus : proto::plus<Int, Int> {}; BOOST_MPL_ASSERT(( proto::matches< int_, Int > )); BOOST_MPL_ASSERT(( proto::matches< plus_, Plus > ));
作为原始转换 ...
// A transform that removes all unary_plus nodes in an expression struct RemoveUnaryPlus : proto::or_< proto::when< proto::unary_plus<RemoveUnaryPlus> , RemoveUnaryPlus(proto::_child) > // Use proto::terminal<> and proto::nary_expr<> // both as grammars and as primitive transforms. , proto::terminal<_> , proto::nary_expr<_, proto::vararg<RemoveUnaryPlus> > > {}; int main() { proto::literal<int> i(0); proto::display_expr( +i - +(i - +i) ); proto::display_expr( RemoveUnaryPlus()( +i - +(i - +i) ) ); }
上面的代码显示了以下内容,这表明一元加法节点已从表达式中剥离
minus( unary_plus( terminal(0) ) , unary_plus( minus( terminal(0) , unary_plus( terminal(0) ) ) ) ) minus( terminal(0) , minus( terminal(0) , terminal(0) ) )
在前面的章节中,我们已经了解了如何使用函数类型将较小的转换组合成较大的转换。 组成较大转换的较小转换是 原始转换,Proto 提供了许多常见的原始转换,例如 _child0
和 _value
。 在本节中,我们将了解如何编写自己的原始转换。
![]() |
注意 |
---|---|
您可能想要编写自己的原始转换有几个原因。 例如,您的转换可能很复杂,并且从原始转换中组合出来变得笨拙。 您可能还需要解决旧编译器上的编译器错误,这些错误使得使用函数类型组合转换变得有问题。 最后,您也可能决定定义自己的原始转换以缩短编译时间。 由于 Proto 可以直接调用原始转换,而无需处理参数或区分可调用转换和对象转换,因此原始转换效率更高。 |
原始转换继承自 proto::transform<>
并具有嵌套的 impl<>
模板,该模板继承自 proto::transform_impl<>
。 例如,这是 Proto 如何定义 _child_c<
转换的方式,该转换返回当前表达式的第 N
>N
个子节点
namespace boost { namespace proto { // A primitive transform that returns N-th child // of the current expression. template<int N> struct _child_c : transform<_child_c<N> > { template<typename Expr, typename State, typename Data> struct impl : transform_impl<Expr, State, Data> { typedef typename result_of::child_c<Expr, N>::type result_type; result_type operator ()( typename impl::expr_param expr , typename impl::state_param state , typename impl::data_param data ) const { return proto::child_c<N>(expr); } }; }; // Note that _child_c<N> is callable, so that // it can be used in callable transforms, as: // _child_c<0>(_child_c<1>) template<int N> struct is_callable<_child_c<N> > : mpl::true_ {}; }}
proto::transform<>
基类提供了 operator()
重载和嵌套的 result<>
模板,这些模板使您的转换成为有效的函数对象。 这些是在您定义的嵌套 impl<>
模板中实现的。
proto::transform_impl<>
基类是一个便利类。 它提供了一些通常有用的嵌套 typedef。 它们在下表中指定
表 30.12. proto::transform_impl<Expr, State, Data> typedefs
typedef |
等效于 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
您会注意到 _child_c::impl::operator()
接受类型为 expr_param
、state_param
和 data_param
的参数。 typedef 使您可以轻松地按引用或 const 引用接受参数。
唯一其他有趣的部分是 is_callable<>
特化,这将在下一节中描述。
转换通常采用 proto::when< Something, R(A0,A1,...) >
的形式。 问题是 R
表示要调用的函数还是要构造的对象,答案决定了 proto::when<>
如何评估转换。 proto::when<>
使用 proto::is_callable<>
特性来区分两者。 Proto 尽力猜测类型是否可调用,但并非总是正确。 最好了解 Proto 使用的规则,以便您知道何时需要更明确。
对于大多数类型 R
,proto::is_callable<R>
检查是否继承自 proto::callable
。 但是,如果类型 R
是模板特化,则 Proto 假定它 不可 调用,即使该模板继承自 proto::callable
。 我们稍后会明白原因。 考虑以下错误的可调用对象
// Proto can't tell this defines something callable! template<typename T> struct times2 : proto::callable { typedef T result_type; T operator()(T i) const { return i * 2; } }; // ERROR! This is not going to multiply the int by 2: struct IntTimes2 : proto::when< proto::terminal<int> , times2<int>(proto::_value) > {};
问题是 Proto 不知道 times2<int>
是可调用的,因此 Proto 不会调用 times2<int>
函数对象,而是尝试构造一个 times2<int>
对象并使用 int
对其进行初始化。 这将无法编译。
![]() |
注意 |
---|---|
为什么 Proto 不能判断 |
对于 times2<int>
问题,有几种解决方案。 一种解决方案是将转换包装在 proto::call<>
中。 这强制 Proto 将 times2<int>
视为可调用的
// OK, calls times2<int> struct IntTimes2 : proto::when< proto::terminal<int> , proto::call<times2<int>(proto::_value)> > {};
这可能有点麻烦,因为我们需要包装 times2<int>
的每次使用,这可能很乏味且容易出错,并且会使我们的语法变得混乱且难以阅读。
另一种解决方案是在我们的 times2<>
模板上特化 proto::is_callable<>
namespace boost { namespace proto { // Tell Proto that times2<> is callable template<typename T> struct is_callable<times2<T> > : mpl::true_ {}; }} // OK, times2<> is callable struct IntTimes2 : proto::when< proto::terminal<int> , times2<int>(proto::_value) > {};
这更好,但仍然很麻烦,因为需要打开 Proto 的命名空间。
您可以简单地确保可调用类型不是模板特化。 考虑以下情况
// No longer a template specialization! struct times2int : times2<int> {}; // OK, times2int is callable struct IntTimes2 : proto::when< proto::terminal<int> , times2int(proto::_value) > {};
这可行,因为现在 Proto 可以判断 times2int
(间接)继承自 proto::callable
。 可以安全地检查任何非模板类型的继承,因为由于它们不是模板,因此不必担心实例化错误。
还有最后一种方法可以告诉 Proto times2<>
是可调用的。 您可以添加一个额外的虚拟模板参数,该参数默认为 proto::callable
// Proto will recognize this as callable template<typename T, typename Callable = proto::callable> struct times2 : proto::callable { typedef T result_type; T operator()(T i) const { return i * 2; } }; // OK, this works! struct IntTimes2 : proto::when< proto::terminal<int> , times2<int>(proto::_value) > {};
请注意,除了额外的模板参数之外,times2<>
仍然继承自 proto::callable
。 这在本例中不是必需的,但它是一种好的风格,因为任何从 times2<>
派生的类型(如上面定义的 times2int
)仍将被视为可调用的。
proto::extends<>
添加成员一个代码示例胜过千言万语 ...
一个简单的示例,它构建并评估一个表达式模板。
//////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at https://boost.ac.cn/LICENSE_1_0.txt) #include <iostream> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> // This #include is only needed for compilers that use typeof emulation: #include <boost/typeof/std/ostream.hpp> namespace proto = boost::proto; proto::terminal< std::ostream & >::type cout_ = {std::cout}; template< typename Expr > void evaluate( Expr const & expr ) { proto::default_context ctx; proto::eval(expr, ctx); } int main() { evaluate( cout_ << "hello" << ',' << " world" ); return 0; }
一个简单的示例,它构建了一个微型的嵌入式领域特定语言,用于延迟算术表达式,带有 TR1 bind 风格的参数占位符。
// Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at https://boost.ac.cn/LICENSE_1_0.txt) // // This is a simple example of how to build an arithmetic expression // evaluator with placeholders. #include <iostream> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> namespace proto = boost::proto; using proto::_; template<int I> struct placeholder {}; // Define some placeholders proto::terminal< placeholder< 1 > >::type const _1 = {{}}; proto::terminal< placeholder< 2 > >::type const _2 = {{}}; // Define a calculator context, for evaluating arithmetic expressions struct calculator_context : proto::callable_context< calculator_context const > { // The values bound to the placeholders double d[2]; // The result of evaluating arithmetic expressions typedef double result_type; explicit calculator_context(double d1 = 0., double d2 = 0.) { d[0] = d1; d[1] = d2; } // Handle the evaluation of the placeholder terminals template<int I> double operator ()(proto::tag::terminal, placeholder<I>) const { return d[ I - 1 ]; } }; template<typename Expr> double evaluate( Expr const &expr, double d1 = 0., double d2 = 0. ) { // Create a calculator context with d1 and d2 substituted for _1 and _2 calculator_context const ctx(d1, d2); // Evaluate the calculator expression with the calculator_context return proto::eval(expr, ctx); } int main() { // Displays "5" std::cout << evaluate( _1 + 2.0, 3.0 ) << std::endl; // Displays "6" std::cout << evaluate( _1 * _2, 3.0, 2.0 ) << std::endl; // Displays "0.5" std::cout << evaluate( (_1 - _2) / _2, 3.0, 2.0 ) << std::endl; return 0; }
Calc1 示例的扩展,它使用 proto::extends<>
使计算器表达式成为有效的函数对象,可以与 STL 算法一起使用。
// Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at https://boost.ac.cn/LICENSE_1_0.txt) // // This example enhances the simple arithmetic expression evaluator // in calc1.cpp by using proto::extends to make arithmetic // expressions immediately evaluable with operator (), a-la a // function object #include <iostream> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> namespace proto = boost::proto; using proto::_; template<typename Expr> struct calculator_expression; // Tell proto how to generate expressions in the calculator_domain struct calculator_domain : proto::domain<proto::generator<calculator_expression> > {}; // Will be used to define the placeholders _1 and _2 template<int I> struct placeholder {}; // Define a calculator context, for evaluating arithmetic expressions // (This is as before, in calc1.cpp) struct calculator_context : proto::callable_context< calculator_context const > { // The values bound to the placeholders double d[2]; // The result of evaluating arithmetic expressions typedef double result_type; explicit calculator_context(double d1 = 0., double d2 = 0.) { d[0] = d1; d[1] = d2; } // Handle the evaluation of the placeholder terminals template<int I> double operator ()(proto::tag::terminal, placeholder<I>) const { return d[ I - 1 ]; } }; // Wrap all calculator expressions in this type, which defines // operator () to evaluate the expression. template<typename Expr> struct calculator_expression : proto::extends<Expr, calculator_expression<Expr>, calculator_domain> { explicit calculator_expression(Expr const &expr = Expr()) : calculator_expression::proto_extends(expr) {} BOOST_PROTO_EXTENDS_USING_ASSIGN(calculator_expression<Expr>) // Override operator () to evaluate the expression double operator ()() const { calculator_context const ctx; return proto::eval(*this, ctx); } double operator ()(double d1) const { calculator_context const ctx(d1); return proto::eval(*this, ctx); } double operator ()(double d1, double d2) const { calculator_context const ctx(d1, d2); return proto::eval(*this, ctx); } }; // Define some placeholders (notice they're wrapped in calculator_expression<>) calculator_expression<proto::terminal< placeholder< 1 > >::type> const _1; calculator_expression<proto::terminal< placeholder< 2 > >::type> const _2; // Now, our arithmetic expressions are immediately executable function objects: int main() { // Displays "5" std::cout << (_1 + 2.0)( 3.0 ) << std::endl; // Displays "6" std::cout << ( _1 * _2 )( 3.0, 2.0 ) << std::endl; // Displays "0.5" std::cout << ( (_1 - _2) / _2 )( 3.0, 2.0 ) << std::endl; return 0; }
Calc2 示例的扩展,它使用 Proto 转换来计算计算器表达式的元数,并静态断言传递了正确数量的参数。
// Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at https://boost.ac.cn/LICENSE_1_0.txt) // // This example enhances the arithmetic expression evaluator // in calc2.cpp by using a proto transform to calculate the // number of arguments an expression requires and using a // compile-time assert to guarantee that the right number of // arguments are actually specified. #include <iostream> #include <boost/mpl/int.hpp> #include <boost/mpl/assert.hpp> #include <boost/mpl/min_max.hpp> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> #include <boost/proto/transform.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; using proto::_; // Will be used to define the placeholders _1 and _2 template<typename I> struct placeholder : I {}; // This grammar basically says that a calculator expression is one of: // - A placeholder terminal // - Some other terminal // - Some non-terminal whose children are calculator expressions // In addition, it has transforms that say how to calculate the // expression arity for each of the three cases. struct CalculatorGrammar : proto::or_< // placeholders have a non-zero arity ... proto::when< proto::terminal< placeholder<_> >, proto::_value > // Any other terminals have arity 0 ... , proto::when< proto::terminal<_>, mpl::int_<0>() > // For any non-terminals, find the arity of the children and // take the maximum. This is recursive. , proto::when< proto::nary_expr<_, proto::vararg<_> > , proto::fold<_, mpl::int_<0>(), mpl::max<CalculatorGrammar, proto::_state>() > > > {}; // Simple wrapper for calculating a calculator expression's arity. // It specifies mpl::int_<0> as the initial state. The data, which // is not used, is mpl::void_. template<typename Expr> struct calculator_arity : boost::result_of<CalculatorGrammar(Expr)> {}; template<typename Expr> struct calculator_expression; // Tell proto how to generate expressions in the calculator_domain struct calculator_domain : proto::domain<proto::generator<calculator_expression> > {}; // Define a calculator context, for evaluating arithmetic expressions // (This is as before, in calc1.cpp and calc2.cpp) struct calculator_context : proto::callable_context< calculator_context const > { // The values bound to the placeholders double d[2]; // The result of evaluating arithmetic expressions typedef double result_type; explicit calculator_context(double d1 = 0., double d2 = 0.) { d[0] = d1; d[1] = d2; } // Handle the evaluation of the placeholder terminals template<typename I> double operator ()(proto::tag::terminal, placeholder<I>) const { return d[ I() - 1 ]; } }; // Wrap all calculator expressions in this type, which defines // operator () to evaluate the expression. template<typename Expr> struct calculator_expression : proto::extends<Expr, calculator_expression<Expr>, calculator_domain> { typedef proto::extends<Expr, calculator_expression<Expr>, calculator_domain> base_type; explicit calculator_expression(Expr const &expr = Expr()) : base_type(expr) {} BOOST_PROTO_EXTENDS_USING_ASSIGN(calculator_expression<Expr>) // Override operator () to evaluate the expression double operator ()() const { // Assert that the expression has arity 0 BOOST_MPL_ASSERT_RELATION(0, ==, calculator_arity<Expr>::type::value); calculator_context const ctx; return proto::eval(*this, ctx); } double operator ()(double d1) const { // Assert that the expression has arity 1 BOOST_MPL_ASSERT_RELATION(1, ==, calculator_arity<Expr>::type::value); calculator_context const ctx(d1); return proto::eval(*this, ctx); } double operator ()(double d1, double d2) const { // Assert that the expression has arity 2 BOOST_MPL_ASSERT_RELATION(2, ==, calculator_arity<Expr>::type::value); calculator_context const ctx(d1, d2); return proto::eval(*this, ctx); } }; // Define some placeholders (notice they're wrapped in calculator_expression<>) calculator_expression<proto::terminal< placeholder< mpl::int_<1> > >::type> const _1; calculator_expression<proto::terminal< placeholder< mpl::int_<2> > >::type> const _2; // Now, our arithmetic expressions are immediately executable function objects: int main() { // Displays "5" std::cout << (_1 + 2.0)( 3.0 ) << std::endl; // Displays "6" std::cout << ( _1 * _2 )( 3.0, 2.0 ) << std::endl; // Displays "0.5" std::cout << ( (_1 - _2) / _2 )( 3.0, 2.0 ) << std::endl; // This won't compile because the arity of the // expression doesn't match the number of arguments // ( (_1 - _2) / _2 )( 3.0 ); return 0; }
此示例构建了一个用于线性代数的小型库,使用表达式模板来消除在添加数字向量时对临时变量的需求。
此示例使用带有语法的域来修剪重载运算符的集合。 仅允许生成有效 lazy vector 表达式的那些运算符。
/////////////////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at https://boost.ac.cn/LICENSE_1_0.txt) // // This example constructs a mini-library for linear algebra, using // expression templates to eliminate the need for temporaries when // adding vectors of numbers. // // This example uses a domain with a grammar to prune the set // of overloaded operators. Only those operators that produce // valid lazy vector expressions are allowed. #include <vector> #include <iostream> #include <boost/mpl/int.hpp> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; using proto::_; template<typename Expr> struct lazy_vector_expr; // This grammar describes which lazy vector expressions // are allowed; namely, vector terminals and addition // and subtraction of lazy vector expressions. struct LazyVectorGrammar : proto::or_< proto::terminal< std::vector<_> > , proto::plus< LazyVectorGrammar, LazyVectorGrammar > , proto::minus< LazyVectorGrammar, LazyVectorGrammar > > {}; // Tell proto that in the lazy_vector_domain, all // expressions should be wrapped in laxy_vector_expr<> // and must conform to the lazy vector grammar. struct lazy_vector_domain : proto::domain<proto::generator<lazy_vector_expr>, LazyVectorGrammar> {}; // Here is an evaluation context that indexes into a lazy vector // expression, and combines the result. template<typename Size = std::size_t> struct lazy_subscript_context { lazy_subscript_context(Size subscript) : subscript_(subscript) {} // Use default_eval for all the operations ... template<typename Expr, typename Tag = typename Expr::proto_tag> struct eval : proto::default_eval<Expr, lazy_subscript_context> {}; // ... except for terminals, which we index with our subscript template<typename Expr> struct eval<Expr, proto::tag::terminal> { typedef typename proto::result_of::value<Expr>::type::value_type result_type; result_type operator ()( Expr const & expr, lazy_subscript_context & ctx ) const { return proto::value( expr )[ ctx.subscript_ ]; } }; Size subscript_; }; // Here is the domain-specific expression wrapper, which overrides // operator [] to evaluate the expression using the lazy_subscript_context. template<typename Expr> struct lazy_vector_expr : proto::extends<Expr, lazy_vector_expr<Expr>, lazy_vector_domain> { lazy_vector_expr( Expr const & expr = Expr() ) : lazy_vector_expr::proto_extends( expr ) {} // Use the lazy_subscript_context<> to implement subscripting // of a lazy vector expression tree. template< typename Size > typename proto::result_of::eval< Expr, lazy_subscript_context<Size> >::type operator []( Size subscript ) const { lazy_subscript_context<Size> ctx(subscript); return proto::eval(*this, ctx); } }; // Here is our lazy_vector terminal, implemented in terms of lazy_vector_expr template< typename T > struct lazy_vector : lazy_vector_expr< typename proto::terminal< std::vector<T> >::type > { typedef typename proto::terminal< std::vector<T> >::type expr_type; lazy_vector( std::size_t size = 0, T const & value = T() ) : lazy_vector_expr<expr_type>( expr_type::make( std::vector<T>( size, value ) ) ) {} // Here we define a += operator for lazy vector terminals that // takes a lazy vector expression and indexes it. expr[i] here // uses lazy_subscript_context<> under the covers. template< typename Expr > lazy_vector &operator += (Expr const & expr) { std::size_t size = proto::value(*this).size(); for(std::size_t i = 0; i < size; ++i) { proto::value(*this)[i] += expr[i]; } return *this; } }; int main() { // lazy_vectors with 4 elements each. lazy_vector< double > v1( 4, 1.0 ), v2( 4, 2.0 ), v3( 4, 3.0 ); // Add two vectors lazily and get the 2nd element. double d1 = ( v2 + v3 )[ 2 ]; // Look ma, no temporaries! std::cout << d1 << std::endl; // Subtract two vectors and add the result to a third vector. v1 += v2 - v3; // Still no temporaries! std::cout << '{' << v1[0] << ',' << v1[1] << ',' << v1[2] << ',' << v1[3] << '}' << std::endl; // This expression is disallowed because it does not conform // to the LazyVectorGrammar //(v2 + v3) += v1; return 0; }
这是一个使用 Proto 转换进行任意类型操作的简单示例。 它接受一些涉及原色的表达式,并根据任意规则组合颜色。 它是 PETE 中 RGB 示例的移植。
/////////////////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at https://boost.ac.cn/LICENSE_1_0.txt) // // This is a simple example of doing arbitrary type manipulations with proto // transforms. It takes some expression involving primary colors and combines // the colors according to arbitrary rules. It is a port of the RGB example // from PETE (http://www.codesourcery.com/pooma/download.html). #include <iostream> #include <boost/proto/core.hpp> #include <boost/proto/transform.hpp> namespace proto = boost::proto; struct RedTag { friend std::ostream &operator <<(std::ostream &sout, RedTag) { return sout << "This expression is red."; } }; struct BlueTag { friend std::ostream &operator <<(std::ostream &sout, BlueTag) { return sout << "This expression is blue."; } }; struct GreenTag { friend std::ostream &operator <<(std::ostream &sout, GreenTag) { return sout << "This expression is green."; } }; typedef proto::terminal<RedTag>::type RedT; typedef proto::terminal<BlueTag>::type BlueT; typedef proto::terminal<GreenTag>::type GreenT; struct Red; struct Blue; struct Green; /////////////////////////////////////////////////////////////////////////////// // A transform that produces new colors according to some arbitrary rules: // red & green give blue, red & blue give green, blue and green give red. struct Red : proto::or_< proto::plus<Green, Blue> , proto::plus<Blue, Green> , proto::plus<Red, Red> , proto::terminal<RedTag> > {}; struct Green : proto::or_< proto::plus<Red, Blue> , proto::plus<Blue, Red> , proto::plus<Green, Green> , proto::terminal<GreenTag> > {}; struct Blue : proto::or_< proto::plus<Red, Green> , proto::plus<Green, Red> , proto::plus<Blue, Blue> , proto::terminal<BlueTag> > {}; struct RGB : proto::or_< proto::when< Red, RedTag() > , proto::when< Blue, BlueTag() > , proto::when< Green, GreenTag() > > {}; template<typename Expr> void printColor(Expr const & expr) { int i = 0; // dummy state and data parameter, not used std::cout << RGB()(expr, i, i) << std::endl; } int main() { printColor(RedT() + GreenT()); printColor(RedT() + GreenT() + BlueT()); printColor(RedT() + (GreenT() + BlueT())); return 0; }
此示例构建了一个用于线性代数的小型库,使用表达式模板来消除在添加数字数组时对临时变量的需求。 它复制了 PETE 中的 TArray 示例。
/////////////////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at https://boost.ac.cn/LICENSE_1_0.txt) // // This example constructs a mini-library for linear algebra, using // expression templates to eliminate the need for temporaries when // adding arrays of numbers. It duplicates the TArray example from // PETE (http://www.codesourcery.com/pooma/download.html) #include <iostream> #include <boost/mpl/int.hpp> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; using proto::_; // This grammar describes which TArray expressions // are allowed; namely, int and array terminals // plus, minus, multiplies and divides of TArray expressions. struct TArrayGrammar : proto::or_< proto::terminal< int > , proto::terminal< int[3] > , proto::plus< TArrayGrammar, TArrayGrammar > , proto::minus< TArrayGrammar, TArrayGrammar > , proto::multiplies< TArrayGrammar, TArrayGrammar > , proto::divides< TArrayGrammar, TArrayGrammar > > {}; template<typename Expr> struct TArrayExpr; // Tell proto that in the TArrayDomain, all // expressions should be wrapped in TArrayExpr<> and // must conform to the TArrayGrammar struct TArrayDomain : proto::domain<proto::generator<TArrayExpr>, TArrayGrammar> {}; // Here is an evaluation context that indexes into a TArray // expression, and combines the result. struct TArraySubscriptCtx : proto::callable_context< TArraySubscriptCtx const > { typedef int result_type; TArraySubscriptCtx(std::ptrdiff_t i) : i_(i) {} // Index array terminals with our subscript. Everything // else will be handled by the default evaluation context. int operator ()(proto::tag::terminal, int const (&data)[3]) const { return data[this->i_]; } std::ptrdiff_t i_; }; // Here is an evaluation context that prints a TArray expression. struct TArrayPrintCtx : proto::callable_context< TArrayPrintCtx const > { typedef std::ostream &result_type; TArrayPrintCtx() {} std::ostream &operator ()(proto::tag::terminal, int i) const { return std::cout << i; } std::ostream &operator ()(proto::tag::terminal, int const (&arr)[3]) const { return std::cout << '{' << arr[0] << ", " << arr[1] << ", " << arr[2] << '}'; } template<typename L, typename R> std::ostream &operator ()(proto::tag::plus, L const &l, R const &r) const { return std::cout << '(' << l << " + " << r << ')'; } template<typename L, typename R> std::ostream &operator ()(proto::tag::minus, L const &l, R const &r) const { return std::cout << '(' << l << " - " << r << ')'; } template<typename L, typename R> std::ostream &operator ()(proto::tag::multiplies, L const &l, R const &r) const { return std::cout << l << " * " << r; } template<typename L, typename R> std::ostream &operator ()(proto::tag::divides, L const &l, R const &r) const { return std::cout << l << " / " << r; } }; // Here is the domain-specific expression wrapper, which overrides // operator [] to evaluate the expression using the TArraySubscriptCtx. template<typename Expr> struct TArrayExpr : proto::extends<Expr, TArrayExpr<Expr>, TArrayDomain> { typedef proto::extends<Expr, TArrayExpr<Expr>, TArrayDomain> base_type; TArrayExpr( Expr const & expr = Expr() ) : base_type( expr ) {} // Use the TArraySubscriptCtx to implement subscripting // of a TArray expression tree. int operator []( std::ptrdiff_t i ) const { TArraySubscriptCtx const ctx(i); return proto::eval(*this, ctx); } // Use the TArrayPrintCtx to display a TArray expression tree. friend std::ostream &operator <<(std::ostream &sout, TArrayExpr<Expr> const &expr) { TArrayPrintCtx const ctx; return proto::eval(expr, ctx); } }; // Here is our TArray terminal, implemented in terms of TArrayExpr // It is basically just an array of 3 integers. struct TArray : TArrayExpr< proto::terminal< int[3] >::type > { explicit TArray( int i = 0, int j = 0, int k = 0 ) { (*this)[0] = i; (*this)[1] = j; (*this)[2] = k; } // Here we override operator [] to give read/write access to // the elements of the array. (We could use the TArrayExpr // operator [] if we made the subscript context smarter about // returning non-const reference when appropriate.) int &operator [](std::ptrdiff_t i) { return proto::value(*this)[i]; } int const &operator [](std::ptrdiff_t i) const { return proto::value(*this)[i]; } // Here we define a operator = for TArray terminals that // takes a TArray expression. template< typename Expr > TArray &operator =(Expr const & expr) { // proto::as_expr<TArrayDomain>(expr) is the same as // expr unless expr is an integer, in which case it // is made into a TArrayExpr terminal first. return this->assign(proto::as_expr<TArrayDomain>(expr)); } template< typename Expr > TArray &printAssign(Expr const & expr) { *this = expr; std::cout << *this << " = " << expr << std::endl; return *this; } private: template< typename Expr > TArray &assign(Expr const & expr) { // expr[i] here uses TArraySubscriptCtx under the covers. (*this)[0] = expr[0]; (*this)[1] = expr[1]; (*this)[2] = expr[2]; return *this; } }; int main() { TArray a(3,1,2); TArray b; std::cout << a << std::endl; std::cout << b << std::endl; b[0] = 7; b[1] = 33; b[2] = -99; TArray c(a); std::cout << c << std::endl; a = 0; std::cout << a << std::endl; std::cout << b << std::endl; std::cout << c << std::endl; a = b + c; std::cout << a << std::endl; a.printAssign(b+c*(b + 3*c)); return 0; }
这是一个使用 proto::extends<>
扩展终端类型并添加额外行为,以及使用自定义上下文和 proto::eval()
评估表达式的简单示例。它是 PETE 中 Vec3 示例的移植。
/////////////////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at https://boost.ac.cn/LICENSE_1_0.txt) // // This is a simple example using proto::extends to extend a terminal type with // additional behaviors, and using custom contexts and proto::eval for // evaluating expressions. It is a port of the Vec3 example // from PETE (http://www.codesourcery.com/pooma/download.html). #include <iostream> #include <functional> #include <boost/assert.hpp> #include <boost/mpl/int.hpp> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> #include <boost/proto/proto_typeof.hpp> #include <boost/proto/transform.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; using proto::_; // Here is an evaluation context that indexes into a Vec3 // expression, and combines the result. struct Vec3SubscriptCtx : proto::callable_context< Vec3SubscriptCtx const > { typedef int result_type; Vec3SubscriptCtx(int i) : i_(i) {} // Index array terminals with our subscript. Everything // else will be handled by the default evaluation context. int operator ()(proto::tag::terminal, int const (&arr)[3]) const { return arr[this->i_]; } int i_; }; // Here is an evaluation context that counts the number // of Vec3 terminals in an expression. struct CountLeavesCtx : proto::callable_context< CountLeavesCtx, proto::null_context > { CountLeavesCtx() : count(0) {} typedef void result_type; void operator ()(proto::tag::terminal, int const(&)[3]) { ++this->count; } int count; }; struct iplus : std::plus<int>, proto::callable {}; // Here is a transform that does the same thing as the above context. // It demonstrates the use of the std::plus<> function object // with the fold transform. With minor modifications, this // transform could be used to calculate the leaf count at compile // time, rather than at runtime. struct CountLeaves : proto::or_< // match a Vec3 terminal, return 1 proto::when<proto::terminal<int[3]>, mpl::int_<1>() > // match a terminal, return int() (which is 0) , proto::when<proto::terminal<_>, int() > // fold everything else, using std::plus<> to add // the leaf count of each child to the accumulated state. , proto::otherwise< proto::fold<_, int(), iplus(CountLeaves, proto::_state) > > > {}; // Here is the Vec3 struct, which is a vector of 3 integers. struct Vec3 : proto::extends<proto::terminal<int[3]>::type, Vec3> { explicit Vec3(int i=0, int j=0, int k=0) { (*this)[0] = i; (*this)[1] = j; (*this)[2] = k; } int &operator [](int i) { return proto::value(*this)[i]; } int const &operator [](int i) const { return proto::value(*this)[i]; } // Here we define a operator = for Vec3 terminals that // takes a Vec3 expression. template< typename Expr > Vec3 &operator =(Expr const & expr) { typedef Vec3SubscriptCtx const CVec3SubscriptCtx; (*this)[0] = proto::eval(proto::as_expr(expr), CVec3SubscriptCtx(0)); (*this)[1] = proto::eval(proto::as_expr(expr), CVec3SubscriptCtx(1)); (*this)[2] = proto::eval(proto::as_expr(expr), CVec3SubscriptCtx(2)); return *this; } // This copy-assign is needed because a template is never // considered for copy assignment. Vec3 &operator=(Vec3 const &that) { (*this)[0] = that[0]; (*this)[1] = that[1]; (*this)[2] = that[2]; return *this; } void print() const { std::cout << '{' << (*this)[0] << ", " << (*this)[1] << ", " << (*this)[2] << '}' << std::endl; } }; // The count_leaves() function uses the CountLeaves transform and // to count the number of leaves in an expression. template<typename Expr> int count_leaves(Expr const &expr) { // Count the number of Vec3 terminals using the // CountLeavesCtx evaluation context. CountLeavesCtx ctx; proto::eval(expr, ctx); // This is another way to count the leaves using a transform. int i = 0; BOOST_ASSERT( CountLeaves()(expr, i, i) == ctx.count ); return ctx.count; } int main() { Vec3 a, b, c; c = 4; b[0] = -1; b[1] = -2; b[2] = -3; a = b + c; a.print(); Vec3 d; BOOST_PROTO_AUTO(expr1, b + c); d = expr1; d.print(); int num = count_leaves(expr1); std::cout << num << std::endl; BOOST_PROTO_AUTO(expr2, b + 3 * c); num = count_leaves(expr2); std::cout << num << std::endl; BOOST_PROTO_AUTO(expr3, b + c * d); num = count_leaves(expr3); std::cout << num << std::endl; return 0; }
这是一个使用 BOOST_PROTO_DEFINE_OPERATORS()
将使用 std::vector<>
(非 Proto 类型)的表达式 Proto 化的示例。它是 PETE 中 Vector 示例的移植。
/////////////////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at https://boost.ac.cn/LICENSE_1_0.txt) // // This is an example of using BOOST_PROTO_DEFINE_OPERATORS to Protofy // expressions using std::vector<>, a non-proto type. It is a port of the // Vector example from PETE (http://www.codesourcery.com/pooma/download.html). #include <vector> #include <iostream> #include <stdexcept> #include <boost/mpl/bool.hpp> #include <boost/proto/core.hpp> #include <boost/proto/debug.hpp> #include <boost/proto/context.hpp> #include <boost/utility/enable_if.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; using proto::_; template<typename Expr> struct VectorExpr; // Here is an evaluation context that indexes into a std::vector // expression and combines the result. struct VectorSubscriptCtx { VectorSubscriptCtx(std::size_t i) : i_(i) {} // Unless this is a vector terminal, use the // default evaluation context template<typename Expr, typename EnableIf = void> struct eval : proto::default_eval<Expr, VectorSubscriptCtx const> {}; // Index vector terminals with our subscript. template<typename Expr> struct eval< Expr , typename boost::enable_if< proto::matches<Expr, proto::terminal<std::vector<_, _> > > >::type > { typedef typename proto::result_of::value<Expr>::type::value_type result_type; result_type operator ()(Expr &expr, VectorSubscriptCtx const &ctx) const { return proto::value(expr)[ctx.i_]; } }; std::size_t i_; }; // Here is an evaluation context that verifies that all the // vectors in an expression have the same size. struct VectorSizeCtx { VectorSizeCtx(std::size_t size) : size_(size) {} // Unless this is a vector terminal, use the // null evaluation context template<typename Expr, typename EnableIf = void> struct eval : proto::null_eval<Expr, VectorSizeCtx const> {}; // Index array terminals with our subscript. Everything // else will be handled by the default evaluation context. template<typename Expr> struct eval< Expr , typename boost::enable_if< proto::matches<Expr, proto::terminal<std::vector<_, _> > > >::type > { typedef void result_type; result_type operator ()(Expr &expr, VectorSizeCtx const &ctx) const { if(ctx.size_ != proto::value(expr).size()) { throw std::runtime_error("LHS and RHS are not compatible"); } } }; std::size_t size_; }; // A grammar which matches all the assignment operators, // so we can easily disable them. struct AssignOps : proto::switch_<struct AssignOpsCases> {}; // Here are the cases used by the switch_ above. struct AssignOpsCases { template<typename Tag, int D = 0> struct case_ : proto::not_<_> {}; template<int D> struct case_< proto::tag::plus_assign, D > : _ {}; template<int D> struct case_< proto::tag::minus_assign, D > : _ {}; template<int D> struct case_< proto::tag::multiplies_assign, D > : _ {}; template<int D> struct case_< proto::tag::divides_assign, D > : _ {}; template<int D> struct case_< proto::tag::modulus_assign, D > : _ {}; template<int D> struct case_< proto::tag::shift_left_assign, D > : _ {}; template<int D> struct case_< proto::tag::shift_right_assign, D > : _ {}; template<int D> struct case_< proto::tag::bitwise_and_assign, D > : _ {}; template<int D> struct case_< proto::tag::bitwise_or_assign, D > : _ {}; template<int D> struct case_< proto::tag::bitwise_xor_assign, D > : _ {}; }; // A vector grammar is a terminal or some op that is not an // assignment op. (Assignment will be handled specially.) struct VectorGrammar : proto::or_< proto::terminal<_> , proto::and_<proto::nary_expr<_, proto::vararg<VectorGrammar> >, proto::not_<AssignOps> > > {}; // Expressions in the vector domain will be wrapped in VectorExpr<> // and must conform to the VectorGrammar struct VectorDomain : proto::domain<proto::generator<VectorExpr>, VectorGrammar> {}; // Here is VectorExpr, which extends a proto expr type by // giving it an operator [] which uses the VectorSubscriptCtx // to evaluate an expression with a given index. template<typename Expr> struct VectorExpr : proto::extends<Expr, VectorExpr<Expr>, VectorDomain> { explicit VectorExpr(Expr const &expr) : proto::extends<Expr, VectorExpr<Expr>, VectorDomain>(expr) {} // Use the VectorSubscriptCtx to implement subscripting // of a Vector expression tree. typename proto::result_of::eval<Expr const, VectorSubscriptCtx const>::type operator []( std::size_t i ) const { VectorSubscriptCtx const ctx(i); return proto::eval(*this, ctx); } }; // Define a trait type for detecting vector terminals, to // be used by the BOOST_PROTO_DEFINE_OPERATORS macro below. template<typename T> struct IsVector : mpl::false_ {}; template<typename T, typename A> struct IsVector<std::vector<T, A> > : mpl::true_ {}; namespace VectorOps { // This defines all the overloads to make expressions involving // std::vector to build expression templates. BOOST_PROTO_DEFINE_OPERATORS(IsVector, VectorDomain) typedef VectorSubscriptCtx const CVectorSubscriptCtx; // Assign to a vector from some expression. template<typename T, typename A, typename Expr> std::vector<T, A> &assign(std::vector<T, A> &arr, Expr const &expr) { VectorSizeCtx const size(arr.size()); proto::eval(proto::as_expr<VectorDomain>(expr), size); // will throw if the sizes don't match for(std::size_t i = 0; i < arr.size(); ++i) { arr[i] = proto::as_expr<VectorDomain>(expr)[i]; } return arr; } // Add-assign to a vector from some expression. template<typename T, typename A, typename Expr> std::vector<T, A> &operator +=(std::vector<T, A> &arr, Expr const &expr) { VectorSizeCtx const size(arr.size()); proto::eval(proto::as_expr<VectorDomain>(expr), size); // will throw if the sizes don't match for(std::size_t i = 0; i < arr.size(); ++i) { arr[i] += proto::as_expr<VectorDomain>(expr)[i]; } return arr; } } int main() { using namespace VectorOps; int i; const int n = 10; std::vector<int> a,b,c,d; std::vector<double> e(n); for (i = 0; i < n; ++i) { a.push_back(i); b.push_back(2*i); c.push_back(3*i); d.push_back(i); } VectorOps::assign(b, 2); VectorOps::assign(d, a + b * c); a += if_else(d < 30, b, c); VectorOps::assign(e, c); e += e - 4 / (c + 1); for (i = 0; i < n; ++i) { std::cout << " a(" << i << ") = " << a[i] << " b(" << i << ") = " << b[i] << " c(" << i << ") = " << c[i] << " d(" << i << ") = " << d[i] << " e(" << i << ") = " << e[i] << std::endl; } }
这是一个使用 BOOST_PROTO_DEFINE_OPERATORS()
将使用 std::vector<>
和 std::list<>
(非 Proto 类型)的表达式 Proto 化的示例。它是 PETE 中 Mixed 示例的移植。
/////////////////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at https://boost.ac.cn/LICENSE_1_0.txt) // // This is an example of using BOOST_PROTO_DEFINE_OPERATORS to Protofy // expressions using std::vector<> and std::list, non-proto types. It is a port // of the Mixed example from PETE. // (http://www.codesourcery.com/pooma/download.html). #include <list> #include <cmath> #include <vector> #include <complex> #include <iostream> #include <stdexcept> #include <boost/proto/core.hpp> #include <boost/proto/debug.hpp> #include <boost/proto/context.hpp> #include <boost/proto/transform.hpp> #include <boost/utility/enable_if.hpp> #include <boost/typeof/std/list.hpp> #include <boost/typeof/std/vector.hpp> #include <boost/typeof/std/complex.hpp> #include <boost/type_traits/remove_reference.hpp> namespace proto = boost::proto; namespace mpl = boost::mpl; using proto::_; template<typename Expr> struct MixedExpr; template<typename Iter> struct iterator_wrapper { typedef Iter iterator; explicit iterator_wrapper(Iter iter) : it(iter) {} mutable Iter it; }; struct begin : proto::callable { template<class Sig> struct result; template<class This, class Cont> struct result<This(Cont)> : proto::result_of::as_expr< iterator_wrapper<typename boost::remove_reference<Cont>::type::const_iterator> > {}; template<typename Cont> typename result<begin(Cont const &)>::type operator ()(Cont const &cont) const { iterator_wrapper<typename Cont::const_iterator> it(cont.begin()); return proto::as_expr(it); } }; // Here is a grammar that replaces vector and list terminals with their // begin iterators struct Begin : proto::or_< proto::when< proto::terminal< std::vector<_, _> >, begin(proto::_value) > , proto::when< proto::terminal< std::list<_, _> >, begin(proto::_value) > , proto::when< proto::terminal<_> > , proto::when< proto::nary_expr<_, proto::vararg<Begin> > > > {}; // Here is an evaluation context that dereferences iterator // terminals. struct DereferenceCtx { // Unless this is an iterator terminal, use the // default evaluation context template<typename Expr, typename EnableIf = void> struct eval : proto::default_eval<Expr, DereferenceCtx const> {}; // Dereference iterator terminals. template<typename Expr> struct eval< Expr , typename boost::enable_if< proto::matches<Expr, proto::terminal<iterator_wrapper<_> > > >::type > { typedef typename proto::result_of::value<Expr>::type IteratorWrapper; typedef typename IteratorWrapper::iterator iterator; typedef typename std::iterator_traits<iterator>::reference result_type; result_type operator ()(Expr &expr, DereferenceCtx const &) const { return *proto::value(expr).it; } }; }; // Here is an evaluation context that increments iterator // terminals. struct IncrementCtx { // Unless this is an iterator terminal, use the // default evaluation context template<typename Expr, typename EnableIf = void> struct eval : proto::null_eval<Expr, IncrementCtx const> {}; // advance iterator terminals. template<typename Expr> struct eval< Expr , typename boost::enable_if< proto::matches<Expr, proto::terminal<iterator_wrapper<_> > > >::type > { typedef void result_type; result_type operator ()(Expr &expr, IncrementCtx const &) const { ++proto::value(expr).it; } }; }; // A grammar which matches all the assignment operators, // so we can easily disable them. struct AssignOps : proto::switch_<struct AssignOpsCases> {}; // Here are the cases used by the switch_ above. struct AssignOpsCases { template<typename Tag, int D = 0> struct case_ : proto::not_<_> {}; template<int D> struct case_< proto::tag::plus_assign, D > : _ {}; template<int D> struct case_< proto::tag::minus_assign, D > : _ {}; template<int D> struct case_< proto::tag::multiplies_assign, D > : _ {}; template<int D> struct case_< proto::tag::divides_assign, D > : _ {}; template<int D> struct case_< proto::tag::modulus_assign, D > : _ {}; template<int D> struct case_< proto::tag::shift_left_assign, D > : _ {}; template<int D> struct case_< proto::tag::shift_right_assign, D > : _ {}; template<int D> struct case_< proto::tag::bitwise_and_assign, D > : _ {}; template<int D> struct case_< proto::tag::bitwise_or_assign, D > : _ {}; template<int D> struct case_< proto::tag::bitwise_xor_assign, D > : _ {}; }; // An expression conforms to the MixedGrammar if it is a terminal or some // op that is not an assignment op. (Assignment will be handled specially.) struct MixedGrammar : proto::or_< proto::terminal<_> , proto::and_< proto::nary_expr<_, proto::vararg<MixedGrammar> > , proto::not_<AssignOps> > > {}; // Expressions in the MixedDomain will be wrapped in MixedExpr<> // and must conform to the MixedGrammar struct MixedDomain : proto::domain<proto::generator<MixedExpr>, MixedGrammar> {}; // Here is MixedExpr, a wrapper for expression types in the MixedDomain. template<typename Expr> struct MixedExpr : proto::extends<Expr, MixedExpr<Expr>, MixedDomain> { explicit MixedExpr(Expr const &expr) : MixedExpr::proto_extends(expr) {} private: // hide this: using proto::extends<Expr, MixedExpr<Expr>, MixedDomain>::operator []; }; // Define a trait type for detecting vector and list terminals, to // be used by the BOOST_PROTO_DEFINE_OPERATORS macro below. template<typename T> struct IsMixed : mpl::false_ {}; template<typename T, typename A> struct IsMixed<std::list<T, A> > : mpl::true_ {}; template<typename T, typename A> struct IsMixed<std::vector<T, A> > : mpl::true_ {}; namespace MixedOps { // This defines all the overloads to make expressions involving // std::vector to build expression templates. BOOST_PROTO_DEFINE_OPERATORS(IsMixed, MixedDomain) struct assign_op { template<typename T, typename U> void operator ()(T &t, U const &u) const { t = u; } }; struct plus_assign_op { template<typename T, typename U> void operator ()(T &t, U const &u) const { t += u; } }; struct minus_assign_op { template<typename T, typename U> void operator ()(T &t, U const &u) const { t -= u; } }; struct sin_ { template<typename Sig> struct result; template<typename This, typename Arg> struct result<This(Arg)> : boost::remove_const<typename boost::remove_reference<Arg>::type> {}; template<typename Arg> Arg operator ()(Arg const &a) const { return std::sin(a); } }; template<typename A> typename proto::result_of::make_expr< proto::tag::function , MixedDomain , sin_ const , A const & >::type sin(A const &a) { return proto::make_expr<proto::tag::function, MixedDomain>(sin_(), boost::ref(a)); } template<typename FwdIter, typename Expr, typename Op> void evaluate(FwdIter begin, FwdIter end, Expr const &expr, Op op) { IncrementCtx const inc = {}; DereferenceCtx const deref = {}; typename boost::result_of<Begin(Expr const &)>::type expr2 = Begin()(expr); for(; begin != end; ++begin) { op(*begin, proto::eval(expr2, deref)); proto::eval(expr2, inc); } } // Add-assign to a vector from some expression. template<typename T, typename A, typename Expr> std::vector<T, A> &assign(std::vector<T, A> &arr, Expr const &expr) { evaluate(arr.begin(), arr.end(), proto::as_expr<MixedDomain>(expr), assign_op()); return arr; } // Add-assign to a list from some expression. template<typename T, typename A, typename Expr> std::list<T, A> &assign(std::list<T, A> &arr, Expr const &expr) { evaluate(arr.begin(), arr.end(), proto::as_expr<MixedDomain>(expr), assign_op()); return arr; } // Add-assign to a vector from some expression. template<typename T, typename A, typename Expr> std::vector<T, A> &operator +=(std::vector<T, A> &arr, Expr const &expr) { evaluate(arr.begin(), arr.end(), proto::as_expr<MixedDomain>(expr), plus_assign_op()); return arr; } // Add-assign to a list from some expression. template<typename T, typename A, typename Expr> std::list<T, A> &operator +=(std::list<T, A> &arr, Expr const &expr) { evaluate(arr.begin(), arr.end(), proto::as_expr<MixedDomain>(expr), plus_assign_op()); return arr; } // Minus-assign to a vector from some expression. template<typename T, typename A, typename Expr> std::vector<T, A> &operator -=(std::vector<T, A> &arr, Expr const &expr) { evaluate(arr.begin(), arr.end(), proto::as_expr<MixedDomain>(expr), minus_assign_op()); return arr; } // Minus-assign to a list from some expression. template<typename T, typename A, typename Expr> std::list<T, A> &operator -=(std::list<T, A> &arr, Expr const &expr) { evaluate(arr.begin(), arr.end(), proto::as_expr<MixedDomain>(expr), minus_assign_op()); return arr; } } int main() { using namespace MixedOps; int n = 10; std::vector<int> a,b,c,d; std::list<double> e; std::list<std::complex<double> > f; int i; for(i = 0;i < n; ++i) { a.push_back(i); b.push_back(2*i); c.push_back(3*i); d.push_back(i); e.push_back(0.0); f.push_back(std::complex<double>(1.0, 1.0)); } MixedOps::assign(b, 2); MixedOps::assign(d, a + b * c); a += if_else(d < 30, b, c); MixedOps::assign(e, c); e += e - 4 / (c + 1); f -= sin(0.1 * e * std::complex<double>(0.2, 1.2)); std::list<double>::const_iterator ei = e.begin(); std::list<std::complex<double> >::const_iterator fi = f.begin(); for (i = 0; i < n; ++i) { std::cout << "a(" << i << ") = " << a[i] << " b(" << i << ") = " << b[i] << " c(" << i << ") = " << c[i] << " d(" << i << ") = " << d[i] << " e(" << i << ") = " << *ei++ << " f(" << i << ") = " << *fi++ << std::endl; } }
演示如何使用 Proto 实现 Boost.Assign 库中的 map_list_of()
。map_list_assign()
用于方便地初始化 std::map<>
。通过使用 Proto,我们可以在构建中间表示时避免任何动态分配。
// Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at https://boost.ac.cn/LICENSE_1_0.txt) // // This is a port of map_list_of() from the Boost.Assign library. // It has the advantage of being more efficient at runtime by not // building any temporary container that requires dynamic allocation. #include <map> #include <string> #include <iostream> #include <boost/proto/core.hpp> #include <boost/proto/transform.hpp> #include <boost/type_traits/add_reference.hpp> namespace proto = boost::proto; using proto::_; struct map_list_of_tag {}; // A simple callable function object that inserts a // (key,value) pair into a map. struct insert : proto::callable { template<typename Sig> struct result; template<typename This, typename Map, typename Key, typename Value> struct result<This(Map, Key, Value)> : boost::add_reference<Map> {}; template<typename Map, typename Key, typename Value> Map &operator()(Map &map, Key const &key, Value const &value) const { map.insert(typename Map::value_type(key, value)); return map; } }; // Work-arounds for Microsoft Visual C++ 7.1 #if BOOST_WORKAROUND(BOOST_MSVC, == 1310) #define MapListOf(x) proto::call<MapListOf(x)> #define _value(x) call<proto::_value(x)> #endif // The grammar for valid map-list expressions, and a // transform that populates the map. struct MapListOf : proto::or_< proto::when< // map_list_of(a,b) proto::function< proto::terminal<map_list_of_tag> , proto::terminal<_> , proto::terminal<_> > , insert( proto::_data , proto::_value(proto::_child1) , proto::_value(proto::_child2) ) > , proto::when< // map_list_of(a,b)(c,d)... proto::function< MapListOf , proto::terminal<_> , proto::terminal<_> > , insert( MapListOf(proto::_child0) , proto::_value(proto::_child1) , proto::_value(proto::_child2) ) > > {}; #if BOOST_WORKAROUND(BOOST_MSVC, == 1310) #undef MapListOf #undef _value #endif template<typename Expr> struct map_list_of_expr; struct map_list_of_dom : proto::domain<proto::pod_generator<map_list_of_expr>, MapListOf> {}; // An expression wrapper that provides a conversion to a // map that uses the MapListOf template<typename Expr> struct map_list_of_expr { BOOST_PROTO_BASIC_EXTENDS(Expr, map_list_of_expr, map_list_of_dom) BOOST_PROTO_EXTENDS_FUNCTION() template<typename Key, typename Value, typename Cmp, typename Al> operator std::map<Key, Value, Cmp, Al> () const { BOOST_MPL_ASSERT((proto::matches<Expr, MapListOf>)); std::map<Key, Value, Cmp, Al> map; return MapListOf()(*this, 0, map); } }; map_list_of_expr<proto::terminal<map_list_of_tag>::type> const map_list_of = {{{}}}; int main() { // Initialize a map: std::map<std::string, int> op = map_list_of ("<", 1) ("<=",2) (">", 3) (">=",4) ("=", 5) ("<>",6) ; std::cout << "\"<\" --> " << op["<"] << std::endl; std::cout << "\"<=\" --> " << op["<="] << std::endl; std::cout << "\">\" --> " << op[">"] << std::endl; std::cout << "\">=\" --> " << op[">="] << std::endl; std::cout << "\"=\" --> " << op["="] << std::endl; std::cout << "\"<>\" --> " << op["<>"] << std::endl; return 0; }
一个 Proto 变换的高级示例,它实现了 Howard Hinnant 的 future groups 设计,该设计会阻塞以等待所有或某些异步操作完成,并在适当类型的元组中返回其结果。
// Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at https://boost.ac.cn/LICENSE_1_0.txt) // // This is an example of using Proto transforms to implement // Howard Hinnant's future group proposal. #include <boost/fusion/include/vector.hpp> #include <boost/fusion/include/as_vector.hpp> #include <boost/fusion/include/joint_view.hpp> #include <boost/fusion/include/single_view.hpp> #include <boost/proto/core.hpp> #include <boost/proto/transform.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; namespace fusion = boost::fusion; using proto::_; template<class L,class R> struct pick_left { BOOST_MPL_ASSERT((boost::is_same<L, R>)); typedef L type; }; // Work-arounds for Microsoft Visual C++ 7.1 #if BOOST_WORKAROUND(BOOST_MSVC, == 1310) #define FutureGroup(x) proto::call<FutureGroup(x)> #endif // Define the grammar of future group expression, as well as a // transform to turn them into a Fusion sequence of the correct // type. struct FutureGroup : proto::or_< // terminals become a single-element Fusion sequence proto::when< proto::terminal<_> , fusion::single_view<proto::_value>(proto::_value) > // (a && b) becomes a concatenation of the sequence // from 'a' and the one from 'b': , proto::when< proto::logical_and<FutureGroup, FutureGroup> , fusion::joint_view< boost::add_const<FutureGroup(proto::_left) > , boost::add_const<FutureGroup(proto::_right) > >(FutureGroup(proto::_left), FutureGroup(proto::_right)) > // (a || b) becomes the sequence for 'a', so long // as it is the same as the sequence for 'b'. , proto::when< proto::logical_or<FutureGroup, FutureGroup> , pick_left< FutureGroup(proto::_left) , FutureGroup(proto::_right) >(FutureGroup(proto::_left)) > > {}; #if BOOST_WORKAROUND(BOOST_MSVC, == 1310) #undef FutureGroup #endif template<class E> struct future_expr; struct future_dom : proto::domain<proto::generator<future_expr>, FutureGroup> {}; // Expressions in the future group domain have a .get() // member function that (ostensibly) blocks for the futures // to complete and returns the results in an appropriate // tuple. template<class E> struct future_expr : proto::extends<E, future_expr<E>, future_dom> { explicit future_expr(E const &e) : future_expr::proto_extends(e) {} typename fusion::result_of::as_vector< typename boost::result_of<FutureGroup(E)>::type >::type get() const { return fusion::as_vector(FutureGroup()(*this)); } }; // The future<> type has an even simpler .get() // member function. template<class T> struct future : future_expr<typename proto::terminal<T>::type> { future(T const &t = T()) : future::proto_derived_expr(future::proto_base_expr::make(t)) {} T get() const { return proto::value(*this); } }; // TEST CASES struct A {}; struct B {}; struct C {}; int main() { using fusion::vector; future<A> a; future<B> b; future<C> c; future<vector<A,B> > ab; // Verify that various future groups have the // correct return types. A t0 = a.get(); vector<A, B, C> t1 = (a && b && c).get(); vector<A, C> t2 = ((a || a) && c).get(); vector<A, B, C> t3 = ((a && b || a && b) && c).get(); vector<vector<A, B>, C> t4 = ((ab || ab) && c).get(); return 0; }
这是一个高级示例,展示了如何使用 Proto 实现一个简单的 lambda EDSL,例如 Boost.Lambda 库。它使用了上下文、变换和表达式扩展。
/////////////////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at https://boost.ac.cn/LICENSE_1_0.txt) // // This example builds a simple but functional lambda library using Proto. #include <iostream> #include <algorithm> #include <boost/mpl/int.hpp> #include <boost/mpl/min_max.hpp> #include <boost/mpl/eval_if.hpp> #include <boost/mpl/identity.hpp> #include <boost/mpl/next_prior.hpp> #include <boost/fusion/tuple.hpp> #include <boost/typeof/typeof.hpp> #include <boost/typeof/std/ostream.hpp> #include <boost/typeof/std/iostream.hpp> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> #include <boost/proto/transform.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; namespace fusion = boost::fusion; using proto::_; // Forward declaration of the lambda expression wrapper template<typename T> struct lambda; struct lambda_domain : proto::domain<proto::pod_generator<lambda> > {}; template<typename I> struct placeholder { typedef I arity; }; template<typename T> struct placeholder_arity { typedef typename T::arity type; }; // The lambda grammar, with the transforms for calculating the max arity struct lambda_arity : proto::or_< proto::when< proto::terminal< placeholder<_> > , mpl::next<placeholder_arity<proto::_value> >() > , proto::when< proto::terminal<_> , mpl::int_<0>() > , proto::when< proto::nary_expr<_, proto::vararg<_> > , proto::fold<_, mpl::int_<0>(), mpl::max<lambda_arity, proto::_state>()> > > {}; // The lambda context is the same as the default context // with the addition of special handling for lambda placeholders template<typename Tuple> struct lambda_context : proto::callable_context<lambda_context<Tuple> const> { lambda_context(Tuple const &args) : args_(args) {} template<typename Sig> struct result; template<typename This, typename I> struct result<This(proto::tag::terminal, placeholder<I> const &)> : fusion::result_of::at<Tuple, I> {}; template<typename I> typename fusion::result_of::at<Tuple, I>::type operator ()(proto::tag::terminal, placeholder<I> const &) const { return fusion::at<I>(this->args_); } Tuple args_; }; // The lambda<> expression wrapper makes expressions polymorphic // function objects template<typename T> struct lambda { BOOST_PROTO_BASIC_EXTENDS(T, lambda<T>, lambda_domain) BOOST_PROTO_EXTENDS_ASSIGN() BOOST_PROTO_EXTENDS_SUBSCRIPT() // Calculate the arity of this lambda expression static int const arity = boost::result_of<lambda_arity(T)>::type::value; template<typename Sig> struct result; // Define nested result<> specializations to calculate the return // type of this lambda expression. But be careful not to evaluate // the return type of the nullary function unless we have a nullary // lambda! template<typename This> struct result<This()> : mpl::eval_if_c< 0 == arity , proto::result_of::eval<T const, lambda_context<fusion::tuple<> > > , mpl::identity<void> > {}; template<typename This, typename A0> struct result<This(A0)> : proto::result_of::eval<T const, lambda_context<fusion::tuple<A0> > > {}; template<typename This, typename A0, typename A1> struct result<This(A0, A1)> : proto::result_of::eval<T const, lambda_context<fusion::tuple<A0, A1> > > {}; // Define our operator () that evaluates the lambda expression. typename result<lambda()>::type operator ()() const { fusion::tuple<> args; lambda_context<fusion::tuple<> > ctx(args); return proto::eval(*this, ctx); } template<typename A0> typename result<lambda(A0 const &)>::type operator ()(A0 const &a0) const { fusion::tuple<A0 const &> args(a0); lambda_context<fusion::tuple<A0 const &> > ctx(args); return proto::eval(*this, ctx); } template<typename A0, typename A1> typename result<lambda(A0 const &, A1 const &)>::type operator ()(A0 const &a0, A1 const &a1) const { fusion::tuple<A0 const &, A1 const &> args(a0, a1); lambda_context<fusion::tuple<A0 const &, A1 const &> > ctx(args); return proto::eval(*this, ctx); } }; // Define some lambda placeholders lambda<proto::terminal<placeholder<mpl::int_<0> > >::type> const _1 = {{}}; lambda<proto::terminal<placeholder<mpl::int_<1> > >::type> const _2 = {{}}; template<typename T> lambda<typename proto::terminal<T>::type> const val(T const &t) { lambda<typename proto::terminal<T>::type> that = {{t}}; return that; } template<typename T> lambda<typename proto::terminal<T &>::type> const var(T &t) { lambda<typename proto::terminal<T &>::type> that = {{t}}; return that; } template<typename T> struct construct_helper { typedef T result_type; // for TR1 result_of T operator()() const { return T(); } // Generate BOOST_PROTO_MAX_ARITY overloads of the // following function call operator. #define BOOST_PROTO_LOCAL_MACRO(N, typename_A, A_const_ref, A_const_ref_a, a)\ template<typename_A(N)> \ T operator()(A_const_ref_a(N)) const \ { return T(a(N)); } #define BOOST_PROTO_LOCAL_a BOOST_PROTO_a #include BOOST_PROTO_LOCAL_ITERATE() }; // Generate BOOST_PROTO_MAX_ARITY-1 overloads of the // following construct() function template. #define M0(N, typename_A, A_const_ref, A_const_ref_a, ref_a) \ template<typename T, typename_A(N)> \ typename proto::result_of::make_expr< \ proto::tag::function \ , lambda_domain \ , construct_helper<T> \ , A_const_ref(N) \ >::type const \ construct(A_const_ref_a(N)) \ { \ return proto::make_expr< \ proto::tag::function \ , lambda_domain \ >( \ construct_helper<T>() \ , ref_a(N) \ ); \ } BOOST_PROTO_REPEAT_FROM_TO(1, BOOST_PROTO_MAX_ARITY, M0) #undef M0 struct S { S() {} S(int i, char c) { std::cout << "S(" << i << "," << c << ")\n"; } }; int main() { // Create some lambda objects and immediately // invoke them by applying their operator(): int i = ( (_1 + 2) / 4 )(42); std::cout << i << std::endl; // prints 11 int j = ( (-(_1 + 2)) / 4 )(42); std::cout << j << std::endl; // prints -11 double d = ( (4 - _2) * 3 )(42, 3.14); std::cout << d << std::endl; // prints 2.58 // check non-const ref terminals (std::cout << _1 << " -- " << _2 << '\n')(42, "Life, the Universe and Everything!"); // prints "42 -- Life, the Universe and Everything!" // "Nullary" lambdas work too int k = (val(1) + val(2))(); std::cout << k << std::endl; // prints 3 // check array indexing for kicks int integers[5] = {0}; (var(integers)[2] = 2)(); (var(integers)[_1] = _1)(3); std::cout << integers[2] << std::endl; // prints 2 std::cout << integers[3] << std::endl; // prints 3 // Now use a lambda with an STL algorithm! int rgi[4] = {1,2,3,4}; char rgc[4] = {'a','b','c','d'}; S rgs[4]; std::transform(rgi, rgi+4, rgc, rgs, construct<S>(_1, _2)); return 0; }
这是一个高级示例,展示了如何外部参数化语法的变换。它定义了一个计算器 EDSL,其语法可以执行检查的或未检查的算术运算。
// Copyright 2011 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at https://boost.ac.cn/LICENSE_1_0.txt) // // This is an example of how to specify a transform externally so // that a single grammar can be used to drive multiple differnt // calculations. In particular, it defines a calculator grammar // that computes the result of an expression with either checked // or non-checked division. #include <iostream> #include <boost/assert.hpp> #include <boost/mpl/int.hpp> #include <boost/mpl/next.hpp> #include <boost/mpl/min_max.hpp> #include <boost/fusion/container/vector.hpp> #include <boost/fusion/container/generation/make_vector.hpp> #include <boost/proto/proto.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; namespace fusion = boost::fusion; // The argument placeholder type template<typename I> struct placeholder : I {}; // Give each rule in the grammar a "name". This is so that we // can easily dispatch on it later. struct calc_grammar; struct divides_rule : proto::divides<calc_grammar, calc_grammar> {}; // Use external transforms in calc_gramar struct calc_grammar : proto::or_< proto::when< proto::terminal<placeholder<proto::_> > , proto::functional::at(proto::_state, proto::_value) > , proto::when< proto::terminal<proto::convertible_to<double> > , proto::_value > , proto::when< proto::plus<calc_grammar, calc_grammar> , proto::_default<calc_grammar> > , proto::when< proto::minus<calc_grammar, calc_grammar> , proto::_default<calc_grammar> > , proto::when< proto::multiplies<calc_grammar, calc_grammar> , proto::_default<calc_grammar> > // Note that we don't specify how division nodes are // handled here. Proto::external_transform is a placeholder // for an actual transform. , proto::when< divides_rule , proto::external_transform > > {}; template<typename E> struct calc_expr; struct calc_domain : proto::domain<proto::generator<calc_expr> > {}; template<typename E> struct calc_expr : proto::extends<E, calc_expr<E>, calc_domain> { calc_expr(E const &e = E()) : calc_expr::proto_extends(e) {} }; calc_expr<proto::terminal<placeholder<mpl::int_<0> > >::type> _1; calc_expr<proto::terminal<placeholder<mpl::int_<1> > >::type> _2; // Use proto::external_transforms to map from named grammar rules to // transforms. struct non_checked_division : proto::external_transforms< proto::when< divides_rule, proto::_default<calc_grammar> > > {}; struct division_by_zero : std::exception {}; struct do_checked_divide : proto::callable { typedef int result_type; int operator()(int left, int right) const { if (right == 0) throw division_by_zero(); return left / right; } }; // Use proto::external_transforms again, this time to map the divides_rule // to a transforms that performs checked division. struct checked_division : proto::external_transforms< proto::when< divides_rule , do_checked_divide(calc_grammar(proto::_left), calc_grammar(proto::_right)) > > {}; int main() { non_checked_division non_checked; int result2 = calc_grammar()(_1 / _2, fusion::make_vector(6, 2), non_checked); BOOST_ASSERT(result2 == 3); try { checked_division checked; // This should throw int result3 = calc_grammar()(_1 / _2, fusion::make_vector(6, 0), checked); BOOST_ASSERT(false); // shouldn't get here! } catch(division_by_zero) { std::cout << "caught division by zero!\n"; } }
Proto 最初是作为 Boost.Xpressive 的一部分开发的,目的是简化将表达式模板转换为能够匹配正则表达式的可执行有限状态机的工作。此后,Proto 已在重新设计和改进的 Spirit-2 和相关的 Karma 库中找到应用。由于这些努力,Proto 演变为一个通用的抽象语法和树变换框架,适用于各种 EDSL 场景。
语法和树变换框架以 Spirit 的语法和语义动作框架为模型。表达式树数据结构在许多方面与 Fusion 数据结构相似,并且可以与 Fusion 的迭代器和算法互操作。
proto::matches<>
的语法匹配功能的语法灵感来自 MPL 的 lambda 表达式。
将函数类型用于 Proto 的复合变换的想法灵感来自 Aleksey Gurtovoy 的 “round” lambda 表示法。
Ren, D. 和 Erwig, M. 2006。用于 Haskell 的通用递归工具箱,或者:系统地废弃你的样板代码。在 2006 年 ACM SIGPLAN Haskell 研讨会论文集(美国俄勒冈州波特兰,2006 年 9 月 17 日 - 17 日)。Haskell '06。ACM,纽约,纽约,13-24 页。DOI=http://doi.acm.org/10.1145/1159842.1159845
一篇关于早期版本 Proto 的技术论文被 ACM SIGPLAN 以库为中心的软件设计研讨会 LCSD'07 接受,可以在 http://lcsd.cs.tamu.edu/2007/final/1/1_Paper.pdf 找到。该论文中描述的树变换与今天存在的变换不同。
形式为 R(A0,A1,...)
(即,函数类型)的变换,其中 proto::is_callable<R>::value
为 true
。R
被视为多态函数对象,参数被视为产生函数对象参数的变换。
在 Proto 中,术语 上下文 指的是一个对象,该对象可以与要评估的表达式一起传递给 proto::eval()
函数。上下文决定了表达式的评估方式。所有上下文结构都定义了一个嵌套的 eval<>
模板,当使用节点标签类型(例如,proto::tag::plus
)实例化时,它是一个二元多态函数对象,它接受该类型的表达式和上下文对象。通过这种方式,上下文将行为与表达式节点关联起来。
在 Proto 中,术语 域 指的是一种类型,该类型将该域内的表达式与该域的 生成器 以及可选的域 语法 相关联。域主要用于为该域内的表达式注入额外的成员,并限制 Proto 的运算符重载,以便永远不会创建不符合域语法的表达式。域是从 proto::domain<>
继承的空结构。
一种编程语言,通过提供与该问题空间内的构造相匹配的编程习惯用法、抽象和构造,从而针对特定的问题空间。
一种作为库实现的领域特定语言。编写该库所用的语言称为“宿主”语言,而该库实现的语言称为“嵌入式”语言。
在 Proto 中,表达式 是一个异构树,其中每个节点要么是 boost::proto::expr<>
、boost::proto::basic_expr<>
的实例化,要么是某些类型的扩展(通过 boost::proto::extends<>
或 BOOST_PROTO_EXTENDS()
)的实例化。
一种 C++ 技术,它使用模板和运算符重载来使表达式构建树,这些树表示表达式以供稍后延迟评估,而不是急切地评估表达式。一些 C++ 库使用表达式模板来构建嵌入式领域特定语言。
在 Proto 中,生成器 是一个一元多态函数对象,您在定义 域 时指定它。在构造新表达式后,Proto 会将表达式传递给域的生成器以进行进一步处理。通常,生成器将表达式包装在一个扩展包装器中,该包装器向其添加额外的成员。
在 Proto 中,语法 是一种描述 Proto 表达式类型子集的类型。域中的表达式必须符合该域的语法。proto::matches<>
元函数评估表达式类型是否与语法匹配。语法可以是诸如 proto::_
之类的原语、诸如 proto::plus<>
之类的复合语法、诸如 proto::or_<>
之类的控制结构,或从语法派生而来的某些类型。
形式为 R(A0,A1,...)
(即,函数类型)的变换,其中 proto::is_callable<R>::value
为 false
。R
被视为要构造的对象的类型,参数被视为产生构造函数参数的变换。
具有重载函数调用运算符和嵌套 result_type
typedef 或 result<>
模板的类类型的实例,用于计算函数调用运算符的返回类型。
一种类型,它定义了一种多态函数对象,该对象接受三个参数:表达式、状态和数据。原始变换可用于组合可调用变换和对象变换。
子域是将另一个域声明为其超域的域。子域中的表达式可以与超域中的表达式组合,并且结果表达式位于超域中。
变换用于操作表达式树。它们有三种类型:原始变换、可调用变换或对象变换。变换
可以通过 T
proto::when<>
转换为三元多态函数对象,例如 proto::when<proto::_,
。这样的函数对象接受 表达式、状态 和 数据 参数,并从中计算结果。T
>
[28] 并非总是可以通过值来保存某些内容。默认情况下,proto::as_expr()
对函数、抽象类型和 iostream(从 std::ios_base
派生的类型)进行了例外处理。这些对象通过引用保存。所有其他对象都通过值保存,即使是数组也是如此。
[29] 此错误消息是使用 Microsoft Visual C++ 9.0 生成的。不同的编译器将发出不同的消息,其可读性也各不相同。