解包表达式
在 Boost 1.51 中,Proto 获得了简单的解包模式。在使用 Proto 转换时,解包表达式对于将表达式的子表达式解包到函数调用或对象构造函数中非常有用,同时可以选择性地对每个子表达式依次应用一些转换。
有关更多信息,请参见解包表达式部分。
行为变更:proto::and_<>
在 Boost 1.44 中,proto::and_<>
作为转换的行为发生了变化。以前,它只应用与集合中最后一个语法相关的转换。现在,它应用所有转换,但只返回最后一个的结果。这使得它的行为类似于 C++ 的逗号运算符。例如,像这样的语法
proto::and_< G0, G1, G2 >
当使用表达式e
进行评估时,现在的行为如下
((void)G0()(e), (void)G1()(e), G2()(e))
![]() |
注意 |
---|---|
为什么要进行 void 转换?这是为了避免参数依赖查找,这可能会找到一个重载的逗号运算符。 |
行为变更:proto::as_expr() 和 proto::as_child()
函数 proto::as_expr()
和 proto::as_child()
用于保证对象是一个 Proto 表达式,如果它还不是,则通过使用可选指定的域将其转换为一个。在以前的版本中,当这些函数传递一个与指定域不同的 Proto 表达式时,它们会应用指定域的生成器,从而导致一个二次包装的表达式。这种行为让一些用户感到惊讶。
这两个函数的新行为是始终保持 Proto 表达式不变,而不管表达式的域如何。
行为变更:proto::(pod_)generator<> 和 proto::basic_expr<>
熟悉 Proto 扩展机制的用户可能在定义他们的域时,使用了 proto::generator<>
或 proto::pod_generator<>
和包装器模板。过去,Proto 会使用 proto::expr<>
的实例来实例化您的包装器模板。在 Boost 1.44 中,Proto 现在使用一种新类型:proto::basic_expr<>
的实例来实例化您的包装器模板。
例如
// An expression wrapper template<class Expr> struct my_expr_wrapper; // A domain struct my_domain : proto::domain< proto::generator< my_expr_wrapper > > {}; template<class Expr> struct my_expr_wrapper : proto::extends<Expr, my_expr_wrapper<Expr>, my_domain> { // Before 1.44, Expr was an instance of proto::expr<> // In 1.44, Expr is an instance of proto::basic_expr<> };
此更改的动机是提高编译时间。proto::expr<>
是一种实例化成本很高的类型,因为它定义了大量成员函数。在定义您自己的表达式包装器时,proto::expr<>
的实例作为隐藏的数据成员函数位于您的包装器中,并且未使用 proto::expr<>
的成员。因此,这些成员函数的成本被浪费了。相比之下,proto::basic_expr<>
是一种非常轻量级的类型,根本没有成员函数。
绝大多数程序应该无需任何源代码更改即可重新编译。但是,如果您在某个地方假设您将获得专门的 proto::expr<>
实例,您的代码将会中断。
新特性:子域
在 Boost 1.44 中,Proto 引入了一个重要的新特性,称为“子域”。这为您提供了一种指定一个域与另一个域兼容的方式,以便一个域中的表达式可以与另一个域中的表达式自由混合。您可以使用 proto::domain<>
的第三个模板参数来定义一个域是另一个域的子域。
例如
// Not shown: define some expression // generators genA and genB struct A : proto::domain< genA, proto::_ > {}; // Define a domain B that is the sub-domain // of domain A. struct B : proto::domain< genB, proto::_, A > {};
域 A
和 B
中的表达式可以具有不同的包装器(因此,具有不同的接口),但可以将它们组合成更大的表达式。如果没有子域关系,这将是一个错误。在这种情况下,结果表达式的域将是 A
。
子域的完整描述可以在 proto::domain<>
和 proto::deduce_domain
的参考部分中找到。
新特性:特定于域的 as_expr() 和 as_child()
Proto 始终允许用户通过在定义他们的域时指定一个 Generator 来事后定制表达式。但它从未允许用户首先控制 Proto 如何组装子表达式。从 Boost 1.44 开始,用户现在拥有这种能力。
定义自己的域的用户现在可以指定 proto::as_expr()
和 proto::as_child()
在他们的域中如何工作。他们可以通过在他们的域类中定义名为 as_expr
和/或 as_child
的嵌套类模板来轻松地做到这一点。
例如
struct my_domain : proto::domain< my_generator > { typedef proto::domain< my_generator > base_domain; // For my_domain, as_child does the same as // what as_expr does by default. template<class T> struct as_child : base_domain::as_expr<T> {}; };
在上面的例子中,my_domain::as_child<>
只是推迟到 proto::domain::as_expr<>
。这具有很好的效果,即导致所有终端都通过值而不是通过引用来捕获,并且同样通过值来存储子表达式。结果是,my_domain
中的表达式可以安全地存储在 auto
变量中,因为它们不会对中间临时表达式有悬空引用。(当然,这也意味着表达式构造具有额外的运行时开销,即复制,编译器可能能够或可能无法将其优化掉。)
在 Boost 1.43 中,proto::extends<>
的推荐用法略有变化。新用法如下所示
// my_expr is an expression extension of the Expr parameter template<typename Expr> struct my_expr : proto::extends<Expr, my_expr<Expr>, my_domain> { my_expr(Expr const &expr = Expr()) : proto::extends<Expr, my_expr, my_domain>(expr) {} // NEW: use the following macro to bring // proto::extends::operator= into scope. BOOST_PROTO_EXTENDS_USING_ASSIGN(my_expr) };
新的是使用
宏。为了允许赋值运算符构建表达式树,BOOST_PROTO_EXTENDS_USING_ASSIGN
()proto::extends<>
重载了赋值运算符。但是,对于 my_expr
模板,编译器会生成一个默认的复制赋值运算符,该运算符隐藏了 proto::extends<>
中的那些。这通常是不希望的(尽管这取决于您想要允许的语法)。
以前,推荐的用法是这样做
// my_expr is an expression extension of the Expr parameter template<typename Expr> struct my_expr : proto::extends<Expr, my_expr<Expr>, my_domain> { my_expr(Expr const &expr = Expr()) : proto::extends<Expr, my_expr, my_domain>(expr) {} // OLD: don't do it like this anymore. using proto::extends<Expr, my_expr, my_domain>::operator=; };
虽然这在大多数情况下都有效,但它仍然不会抑制默认赋值运算符的隐式生成。因此,形式为 a = b
的表达式可以构建一个表达式模板,或者执行一个复制赋值,这取决于 a
和 b
的类型是否相同。这可能会导致细微的错误,因此行为已更改。
将 BOOST_PROTO_EXTENDS_USING_ASSIGN
()proto::extends<>
中定义的赋值运算符引入到作用域中,并抑制复制赋值运算符的生成。
另请注意,使用 proto::extends<>
的 proto::literal<>
类模板已更改为使用
。以下示例代码中突出显示了相关内容BOOST_PROTO_EXTENDS_USING_ASSIGN
()
proto::literal<int> a(1), b(2); // two non-const proto literals proto::literal<int> const c(3); // a const proto literal a = b; // No-op. Builds an expression tree and discards it. // Same behavior in 1.42 and 1.43. a = c; // CHANGE! In 1.42, this performed copy assignment, causing // a's value to change to 3. In 1.43, the behavior is now // the same as above: build and discard an expression tree.
Boost 1.44: Proto 获取子域和每个域对 proto::as_expr()
和 proto::as_child()
的控制,以满足 Phoenix3 的需求。
Proto v4 被合并到 Boost 主干,具有更强大的转换协议。
Proto 被 Boost 接受。
Proto 的 Boost 审查开始。
Boost.Proto v3 带来了语法和转换的分离,以及用于就地定义转换的“圆形” lambda 语法。
Boost.Xpressive 从 Proto 编译器移植到 Proto 转换。放弃对旧 Proto 编译器的支持。
Proto初步提交至Boost。
使用装饰语法规则的转换的想法,诞生于与Joel de Guzman和Hartmut Kaiser的私下邮件讨论中。第一个转换在5天后的12月16日提交到CVS。
proto::matches<>
以及整个语法工具的想法,是在与Hartmut Kaiser在spirit-devel邮件列表上的讨论中产生的。第一个版本的proto::matches<>
在3天后被检入CVS。消息在此。
Proto重生了,这次使用了一种统一的表达式类型,它们是POD(Plain Old Data)。公告在此。
Proto诞生了,它是Boost.Xpressive元编程的主要重构。Proto提供了表达式类型、操作符重载和“编译器”,这是后来演变为转换的早期形式。公告在此。
Proto表达式类型是POD(Plain Old Data),并且没有构造函数。它们使用花括号进行初始化,如下所示:
terminal<int>::type const _i = {1};
原因是像上面_i
这样的表达式对象可以进行静态初始化。为什么静态初始化很重要?许多嵌入式领域特定语言的终端很可能是全局const对象,例如Boost Lambda库中的_1
和_2
。如果这些对象需要运行时初始化,则可能在它们初始化之前使用这些对象。这将是不好的。静态初始化的对象不会被以这种方式滥用。
任何看过Proto源代码的人可能都会想:“为什么有这么多肮脏的预处理器代码?难道不能在MPL和Fusion等库之上干净地实现所有这些吗?” 答案是Proto可以这样实现,事实上曾经也这样实现过。问题是模板元编程(TMP)会延长编译时间。作为其他TMP密集型库的基础,Proto本身应该尽可能轻量级。 这是通过优先使用预处理器元编程而不是模板元编程来实现的。 展开宏比实例化模板效率高得多。 在某些情况下,“干净”版本的编译时间比“肮脏”版本长10倍。
Proto的“干净且缓慢”的版本仍然可以在http://svn.boost.org/svn/boost/branches/proto/v3找到。任何有兴趣的人都可以下载它并验证它实际上慢得无法使用。 请注意,该分支的开发已被放弃,并且它与Proto当前接口并不完全一致。
关于在C++中使用SFINAE(替换失败不是错误)技术基于类型特征进行分派的文章已经有很多了。 有一个Boost库Boost.Enable_if,使该技术成为惯用用法。 Proto广泛地基于类型特征进行分派,但它并不经常使用enable_if<>
。 相反,它基于嵌套类型的存在与否进行分派,通常是void的typedef。
考虑is_expr<>
的实现。它可以写成这样:
template<typename T> struct is_expr : is_base_and_derived<proto::some_expr_base, T> {};
而是这样实现的:
template<typename T, typename Void = void> struct is_expr : mpl::false_ {}; template<typename T> struct is_expr<T, typename T::proto_is_expr_> : mpl::true_ {};
这依赖于以下事实:如果T
具有嵌套的proto_is_expr_
(它是void
的typedef),则将优先选择该特化。 所有Proto表达式类型都具有这样的嵌套typedef。
为什么Proto以这种方式进行?原因是,在尝试改进编译时间的同时运行了广泛的基准测试后,我发现这种方法编译速度更快。 它只需要一个模板实例化。 另一种方法至少需要2个:is_expr<>
和is_base_and_derived<>
,以及is_base_and_derived<>
可能实例化的任何模板。
在几个地方,Proto需要知道函数对象Fun
是否可以用某些参数调用,并在不能时采取回退操作。 这发生在proto::callable_context<>
和proto::call<>
转换中。 Proto如何知道? 它涉及一些棘手的元编程。 如下所示。
表达该问题的另一种方式是尝试实现以下can_be_called<>
布尔元函数,该函数检查函数对象Fun
是否可以用A
和B
类型的参数调用。
template<typename Fun, typename A, typename B> struct can_be_called;
首先,我们定义以下dont_care
结构体,它具有来自任何内容的隐式转换。 而且不仅仅是任何隐式转换; 它具有省略号转换,这对于重载解析来说是最糟糕的转换。
struct dont_care { dont_care(...); };
我们还需要一些只有我们知道的私有类型,它具有重载的逗号运算符(!),以及一些检测该类型的存在并返回具有不同大小的类型的函数,如下所示:
struct private_type { private_type const &operator,(int) const; }; typedef char yes_type; // sizeof(yes_type) == 1 typedef char (&no_type)[2]; // sizeof(no_type) == 2 template<typename T> no_type is_private_type(T const &); yes_type is_private_type(private_type const &);
接下来,我们实现一个二进制函数对象包装器,它具有非常奇怪的转换运算符,其含义稍后将变得清楚。
template<typename Fun> struct funwrap2 : Fun { funwrap2(); typedef private_type const &(*pointer_to_function)(dont_care, dont_care); operator pointer_to_function() const; };
有了所有这些零碎的东西,我们可以实现can_be_called<>
,如下所示:
template<typename Fun, typename A, typename B> struct can_be_called { static funwrap2<Fun> &fun; static A &a; static B &b; static bool const value = ( sizeof(no_type) == sizeof(is_private_type( (fun(a,b), 0) )) ); typedef mpl::bool_<value> type; };
这个想法是使fun(a,b)
总是可以编译的,通过添加我们自己的二进制函数重载,但以这样一种方式,我们可以检测到是否选择了我们的重载。 我们对其进行设置,以便在确实没有更好的选择时才选择我们的重载。 以下是对can_be_called<>
工作原理的描述。
我们将Fun
包装在一种类型中,该类型具有到二进制函数指针的隐式转换。 可以将类类型的对象fun
调用为fun(a, b)
,如果它具有这样的转换运算符,但由于它涉及用户定义的转换运算符,因此它不如重载的operator()
更受欢迎,后者不需要这样的转换。
由于dont_care
类型,函数指针可以接受任何两个参数。 每个参数的转换序列都保证是最糟糕的转换序列:通过省略号进行隐式转换,以及到dont_care
的用户定义转换。 总之,这意味着funwrap2<Fun>()(a, b)
总是可以编译的,但只有在确实没有更好的选择时才会选择我们的重载。
如果有一个更好的选择——例如,如果Fun
具有重载的函数调用运算符,例如void operator()(A a, B b)
——那么fun(a, b)
将解析为该重载。 现在的问题是如何检测到重载解析选择了哪个函数。
注意 fun(a, b)
是如何出现在 can_be_called<>
中的: (fun(a, b), 0)
。 为什么我们在那里使用逗号运算符? 原因是我们将此表达式用作函数的参数。 如果 fun(a, b)
的返回类型是 void
,则它不能合法地用作函数的参数。 逗号运算符可以绕过这个问题。
这也应该清楚说明 private_type
中重载逗号运算符的目的。 指向函数的指针的返回类型是 private_type
。 如果重载解析选择我们的重载,那么 (fun(a, b), 0)
的类型是 private_type
。 否则,它是 int
。 这一事实用于分派到 is_private_type()
的两个重载之一,该重载将其答案编码在其返回类型的大小中。
这就是它如何处理二元函数的。 现在,针对具有预定义函数元数的函数重复上述过程,你就完成了。
我要感谢 Joel de Guzman 和 Hartmut Kaiser 愿意冒险在 Spirit-2 和 Karma 的工作中使用 Proto,当时 Proto 还只是一个愿景。 他们的要求和反馈是必不可少的。
还要感谢 Thomas Heller 和 Hartmut 在 Phoenix 重新设计期间提供的反馈和建议。 这项工作产生了几个有价值的高级功能,例如子域、外部转换和每个域的 as_child
自定义。
感谢 Daniel James 提供了一个补丁,用于删除对 C++0x 功能的已弃用配置宏的依赖。
感谢 Joel Falcou 和 Christophe Henry 的热情、支持、反馈和幽默; 并感谢他们自愿成为 Proto 的共同维护者。
感谢 Dave Abrahams 的特别详细的审查,并感谢他提供了一个包含 msvc-7.1 的 VM,以便我可以在该编译器上跟踪可移植性问题。
非常感谢 Daniel Wallin,他首先实现了用于在一组中查找公共域的代码,同时考虑了超级域和子域。 还要感谢 Jeremiah Willcock、John Bytheway 和 Krishna Achuthan,他们为这个棘手的编程问题提供了替代解决方案。
还要感谢 PETE 的开发人员。 我在那里找到了许多好主意。