1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
|
We've seen that binary operators (like tt(operator+)) can be implemented very
efficiently, but require at least move constructors.
An expression like
verb( Binary{} + varB + varC + varD)
therefore returns a move constructed object representing tt(Binary{} +
varB), then another move constructed object receiving the first return value
and tt(varC), and finally yet another move constructed object receiving the
second returned object and tt(varD) as its arguments.
Now consider the situation where we have a function defining a tt(Binary &&)
parameter, and a second tt(Binary const &) parameter. Inside that function
these values need to be added, and their sum is then passed as argument to two
other functions. We em(could) do this:
verb( void fun1(Binary &&lhs, Binary const &rhs)
{
lhs += rhs;
fun2(lhs);
fun3(lhs);
})
But realize that when using tt(operator+=) we first construct a copy of
the current object, so a temporary object is available to perform the addition
on, and then swap the temporary object with the current object to commit the
results. But wait! Our lhs operand already em(is) a temporary object. So why
create another?
In this example another temporary object is indeed not required: tt(lhs)
remains in existence until tt(fun1) ends. But different from the binary
operators the binary compound assignment operators don't have explicitly
defined left-hand side operands. But we still can inform the compiler that a
particular em(member) (so, not merely compound assignment operators) should
only be used when the objects calling those members is an anonymous temporary
object, or a non-anonymous (modifiable or non-modifiable) object. For this
we use
em(reference bindings)hi(reference binding) a.k.a.
em(reference qualifiers)hi(reference qualifier).
Reference bindings consist of a reference token (tt(&)), optionally
preceded by tt(const), or an rvalue reference token (tt(&&)). Such reference
qualifiers are immediately affixed to the function's head (this applies to the
declaration and the implementation alike). Functions provided with rvalue
reference bindings are selected by the compiler when used by anonymous
temporary objects, whereas functions provided with lvalue reference bindings
are selected by the compiler when used by other types of objects.
Reference qualifiers allow us to fine-tune our implementations of compound
assignment operators like tt(operator+=). If we know that the object calling
the compound assignment operator is itself a temporary, then there's no need
for a separate temporary object. The operator may directly perform its
operation and could then return itself as an rvalue reference. Here is the
implementation of tt(operator+=) tailored to being used by temporary objects:
verb( Binary &&Binary::operator+=(Binary const &rhs) &&
{
add(rhs); // directly add rhs to *this,
return std::move(*this); // return the temporary object itself
})
This implementation is about as fast as it gets. But be careful: in the
previous section we learned that a temporary is destroyed at the end of the
full expression of a return stattement. In this case, however, the temporary
already exists, and so (also see the previous section) it should persist until
the expression containing the (tt(operator+=)) function call is completed. As
a consequence,
verb( cout << (Binary{} += existingBinary) << '\n';)
is OK, but
verb( Binary &&rref = (Binary{} += existingBinary);
cout << rref << '\n';)
is not, since tt(rref) becomes a dangling reference immediately after its
initialization.
A full-proof alternative implementation of the rvalue-reference bound
tt(operator+=) returns a move-constructed copy:
verb( Binary Binary::operator+=(Binary const &rhs) &&
{
add(rhs); // directly add rhs to *this,
return std::move(*this); // return a move constructed copy
})
The price to pay for this full-proof implementation is an extra move
construction. Now, using the previous example (using tt(rref)), tt(operator+=)
returns a copy of the tt(Binary{}) temporary, which is still a temporary
object which can safely be referred to by tt(rref).
Which implementation to use may be a matter of choice: if users of
tt(Binary) know what they're doing then the former implementation can be used,
since these users will never use the above tt(rref) initialization. If you're
not so sure about your users, use the latter implementation: formally your
users will do something they shouldn't do, but there's no penalty for that.
For the compound assignment operator called by an lvalue reference (i.e.,
a named object) we use the implementation for tt(operator+=) from the previous
section (note the reference qualifier):
verb( Binary &Binary::operator+=(Binary const &rhs) &
{
Binary tmp(*this);
tmp.add(rhs); // this might throw
swap(tmp);
return *this;
})
With this implementation adding tt(Binary) objects to each other
(e.g., tt(b1 += b2 += b3)) boils down to
verb( operator+= (&) = b2 += b3
Copy constructor = tmp(b2)
adding = tmp.add(b3)
swap = b2 <-> tmp
return = b2
operator+= (&) = b1 += b2
Copy constructor = tmp(b1)
adding = tmp.add(b2)
swap = b1 <-> tmp
return = b1)
When the leftmost object is a temporary then a copy construction and swap
call are replaced by the construction of an anonymous object. E.g.,
with tt(Binary{} += b2 += b3) we observe:
verb( operator+= (&) = b2 += b3
Copy constructor = tmp(b2)
adding = tmp.add(b3)
swap = b2 <-> tmp
Anonymous object = Binary{}
operator+= (&&) = Binary{} += b2
adding = add(b2)
return = move(Binary{}))
For tt(Binary &Binary::operator+=(Binary const &rhs) &) an alternative
implementation exists, merely using a single return statement, but in fact
requiring two extra function calls. It's a matter of taste whether you prefer
writing less code or executing fewer function calls:
verb( Binary &Binary::operator+=(Binary const &rhs) &
{
return *this = Binary{ *this } += rhs;
})
Notice that the implementations of tt(operator+) and tt(operator+=) are
independent of the actual definition of the class tt(Binary). Adding standard
binary operators to a class (i.e., operators operating on arguments of their
own class types) can therefore easily be realized.
|