解包表达式
在 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 表达式不变,而不管表达式的域是什么。
行为变更: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 一直允许用户通过在定义域时指定生成器来事后自定义表达式。但它从未允许用户控制 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<>
简单地defer to 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。
在 spirit-devel 列表上与 Hartmut Kaiser 讨论期间,提出了 proto::matches<>
和整个语法工具的想法。 proto::matches<>
的第一个版本在 3 天后签入 CVS。消息位于此处。
Proto 重生,这次使用的是 POD 的统一表达式类型。公告位于此处。
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(Substitution Failure Is Not An Error,替换失败不是错误)技术进行类型特征分派的文章已经有很多了。有一个 Boost 库,Boost.Enable_if,可以使该技术成为惯用法。Proto 广泛地使用类型特征分派,但它不经常使用 enable_if<>
。相反,它根据嵌套类型的存在与否进行分派,通常是 void 的类型定义。
考虑 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
的类型定义,则特化将是首选。所有 Proto 表达式类型都有这样的嵌套类型定义。
为什么 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 愿意在 Proto 还只是一个愿景的时候就将 Proto 用于他们在 Spirit-2 和 Karma 上的工作。他们的要求和反馈是不可或缺的。
还要感谢 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 的开发者。我在那里找到了很多好主意。