For any expression<>
operator overload, or
any function defined using one of the function definition macros, operands
are treated in a uniform way.
The guiding design principle here is that an expression built using Boost.YAP should match the semantics of a builtin C++ expression as closely as possible. This implies that an rvalue be treated as if it were a temporary (as it may in fact have initially been) throughout the building and transformation of an expression, and that an lvalue should retain its connection to the underlying named entity to which it refers.
For example, if you see
auto expr = a + 1;
you should expect that a
will be an lvalue reference to some object of type decltype(a)
,
regardless of whether a
is
a Boost.YAP Expression
or a builtin type. Similarly, you should expect the 1
to be an rvalue, whether wrapped in a terminal or not.
Let's take a quick look at make_terminal()
.
If you call it with a T
rvalue,
the terminal's value type is a T
,
and the rvalue gets moved into it. If you call it with a T
[const]
lvalue, the value type is T [const] &
, and
the reference refers to the lvalue (read [const]
as
"possibly const
-qualified").
This is important because you might write through the terminal later in an
assignment operation. You don't want to lose the ability to do this, or be
forced to write some Baroque pile of code to do so — it should be
natural and easy.
And it is:
int i = 0; auto expr = boost::yap::make_terminal(i) = 42; evaluate(expr); std::cout << i << "\n"; // Prints 42.
Now, there is a wrinkle. Boost.YAP's lazy expressions can be built piecemeal:
auto subexpr = boost::yap::make_terminal(1) + 2; // This is fine, and acts more-or-less as if you wrote "1 / (1 + 2)". auto expr = 1 / subexpr;
whereas C++'s eager builtin expressions cannot:
auto subexpr = 1 + 2; // Same as "int subexpr = 3;". Hm. auto expr = 1 / subexpr; // Same as "int expr = 0;" Arg.
Ok, so since you can build these lazy Boost.YAP expressions up from subexpressions,
how do we treat the subexpressions? We treat them in exactly the same way
as make_terminal()
treats its parameter. Rvalues
are moved in, and lvalues are captured by (possibly const
)
reference.
Note | |
---|---|
If you want to subvert the capture-by-reference semantics of using subexpressions,
just |
The capture-by-reference behavior is implemented via a special expr_kind
,
expr_kind::expr_ref
.
An expr_ref
expression has
a single data element: a (possibly const
(Can I stop saying that every time? You get it, right? Ok, good.)) reference
to an expression. This additional level of indirection causes some complications
at times, as you can see in the examples. Fortunately, the complications
are not overly cumbersome.
So, given the rules above, here is a comprehensive breakdown of what happens
when an operand is passed to a Boost.YAP operator. In this table, expr_tmpl
is an ExpressionTemplate,
and T
is a non-Expression
type. E
refers to any non-expr_ref
Expression.
Boost.YAP does a partial decay on non-Expression
operands, in which cv
and
reference qualifiers are left unchanged, but arrays are decayed to pointers
and functions are decayed to function pointers. PARTIAL_DECAY(T)
indicates such a partial decay of T
.
Table 1.4. Operand Handling
Operand |
Captured As |
Notes |
---|---|---|
|
|
|
|
|
|
|
|
Operand moved. |
|
|
|
|
|
|
|
|
Operand moved. |
|
|
|
|
|
|
|
|
Operand moved. |
The partial decay of non-Expression operands is another example of how Boost.YAP attempts to create expression trees that are as semantically close to builtin expressions as possible.