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 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
|
In various classes overloading binary operators (like ti(operator+)) can
be a very natural extension of the class's functionality. For example, the
tt(std::string) class has various overloaded forms of tt(operator+).
Most binary operators come in two flavors: the plain binary operator (like
the tt(+) operator) and the compound assignment variant (like the tt(+=)
operator). Whereas the plain binary operators return values,
the compound assignment operators return a reference to the
object to which the operator was applied. For example, with tt(std::string)
objects the following code (annotations below the example) may be used:
verbinclude(-a examples/binarystring.cc)
itemization(
it() at tt(// 1) the contents of tt(s3) is added to tt(s2). Next, tt(s2)
is returned, and its new contents are assigned to tt(s1). Note that tt(+=)
returns tt(s2) itself.
it() at tt(// 2) the contents of tt(s3) is also added to tt(s2), but as
tt(+=) returns tt(s2) itself, it's possible to add some more to tt(s2)
it() at tt(// 3) the tt(+) operator returns a tt(std::string) containing
the concatenation of the text tt(prefix) and the contents of tt(s3). This
string returned by the tt(+) operator is thereupon assigned to tt(s1).
it() at tt(// 4) the tt(+) operator is applied twice. The effect is:
enumeration(
eit() The first tt(+) returns a tt(std::string) containing
the concatenation of the text tt(prefix) and the contents of tt(s3).
eit() The second tt(+) operator takes this returned string as its left
hand value, and returns a string containing the concatenated text of its left
and right hand operands.
eit() The string returned by the second tt(+) operator represents the
value of the expression.
)
)
Consider the following code, in which a class tt(Binary) supports
an overloaded tt(operator+):
verbinclude(-a examples/binary1.cc)
Compilation of this little program fails for statement tt(// 2), with the
compiler reporting an error like:
verb(
error: no match for 'operator+' in '3 + b2'
)
Why is statement tt(// 1) compiled correctly whereas statement tt(// 2)
won't compile?
In order to understand this remember em(promotions). As we have seen in
section ref(EXPLICIT), constructors expecting a single argument may be
implicitly activated when an argument of an appropriate type is
provided. We've encountered this repeatedly with tt(std::string) objects,
where an NTBS may be used to initialize a tt(std::string) object.
Analogously, in statement tt(// 1), the tt(+) operator is called for the
tt(b2) object. This operator expects another tt(Binary) object as its right
hand operand. However, an tt(int) is provided. As a constructor
tt(Binary(int)) exists, the tt(int) value is first promoted to a tt(Binary)
object. Next, this tt(Binary) object is passed as argument to the
tt(operator+) member.
In statement tt(// 2) no promotions are available: here the tt(+) operator
is applied to an lvalue that is an tt(int). An tt(int) is a primitive type and
primitive types have no concept of `constructors', `member functions' or
`promotions'.
How, then, are promotions of left-hand operands implemented in statements
like tt("prefix " + s3)? Since promotions are applied to function arguments,
we must make sure that both operands of binary operators are arguments. This
implies that plain binary operators supporting promotions for
either their left-hand side operand or right-hand side operand should be
declared as
hi(operator: free)hi(function: free)em(free operators),
also called em(free functions).
Functions like the plain binary operators conceptually belong to the class
for which they implement the binary operator. Consequently they should be
declared in the class's header file. We cover their implementations
shortly, but here is our first revision of the declaration of the class
tt(Binary), declaring an overloaded tt(+) operator as a free function:
verbinclude(-a examples/binary1.h)
By defining binary operators as free functions, the following promotions
are possible:
itemization(
it() If the left-hand operand is of the intended class type, the right
hand argument is promoted whenever possible;
it() If the right-hand operand is of the intended class type, the left
hand argument is promoted whenever possible;
it() No promotions occur when none of the operands are of the intended
class type;
it() An ambiguity occurs when promotions to different classes are possible
for the two operands. For example:
verbinclude(-a examples/binaryambigu.cc)
Here, both overloaded tt(+) operators are possible when compiling
the statement tt(a + a). The ambiguity must be solved by explicitly promoting
one of the arguments, e.g., tt(a + B(a)) allows the compiler to resolve
the ambiguity to the first overloaded tt(+) operator.
)
The next step is to implement the corresponding overloaded binary compound
assignment operators, having the form tt(@=), where tt(@) represents a binary
operator. As this operator em(always) has a left-hand operand which is an
object of its own class, it is implemented as a true member
function. Furthermore, the compound assignment operator should return a
reference to the object to which the binary operation applies, as the object
might be modified in the same statement. E.g.,
tt((s2 += s3) + " postfix"). Here is our second revision of the class
tt(Binary), showing both the declaration of the plain binary operator and the
corresponding compound assignment operator:
verbinclude(-a examples/binary2.h)
How should the compound addition assignment operator be implemented? When
implementing the compound assignment operator the strong guarantee should
again be kept in mind. Use a temporary object and swap if the tt(add) member
might throw. Example:
verb(
Binary &operator+=(Binary const &other)
{
Binary tmp(*this);
tmp.add(other); // this may throw
swap(tmp);
return *this;
}
)
It's easy to implement the plain binary operator for classes offering the
matching compound assignment operator: the tt(lhs) argument is copied into a
tt(Binary tmp) to which the tt(rhs) operand is added. Then tt(tmp) is
returned. The copy construction and two statements could be contracted into
one single return statement, but then compilers usually aren't able to apply
copy elision in this case. But copy elision em(is) usually used when the steps
are taken separately:
verbinclude(-a examples/binary3.h)
But wait! Remember the design principle for move-aware classes that was
given in section ref(MOVEPRINCIPLE)? When implementing binary operators we're
doing exactly what was mentioned in that design principle. A temporay
object is constructed and the compound assignment operation is applied to the
temporary object. In the next section we'll have a look at how we can use this
design principle to our advantage.
If the class tt(Binary) is a move-aware class then we can add move-aware
binary operators to our class. The actual work, as mentioned, is performed by
the compound addition assignment operator. Applying the format of the
traditional binary operator (receiving two const references) to the move-aware
addition operator we get the following signature:
verb(
Binary operator+(Binary &&lhs, Binary const &rhs);
)
To implement this, we realize that we already have a temporary object, so
we can return tt(lhs) after having added tt(rhs) to it. Since tt(lhs) already
is a temporary, we can avoid a copy construction by wrapping the compund
addition in a tt(std::move) call:
verb(
Binary operator+(Binary &&lhs, Binary const &rhs)
{
return std::move(lhs += rhs);
}
)
When executing an expression like (all tt(Binary) objects) tt(b1 + b2 +
b3) the following functions are called:
verb(
copy operator+ = b1 + b2
Copy constructor = tmp(b1)
Copy += = tmp += b2
Copy constructor = tmp2(tmp)
+= operation = tmp2.add(b3), swap(tmp2)
move operator+ = tmp + b3
Copy += = tmp += b3
Copy constructor = tmp2(tmp)
+= operation = tmp2.add(b3), swap(tmp2)
Move constructor = return std::move(tmp)
)
There's at least some gain: if the tt(std::move) wrap is omitted, then the
copy constructor is called.
But since we already have a temporary object, wouldn't it be nice if we
could lure the compiler into using return value optimization? We can, by
telling the compiler that tt(operator+'s) return value em(is) a temporary. We
do that by explicitly stating that its return value is an rvalue reference:
verb(
Binary &&operator+(Binary &&lhs, Binary const &rhs)
{
return std::move(lhs += rhs);
}
)
And, realizing that our `traditional' binary operator also returns a
temporary, we do the same for that operator:
verb(
Binary &&operator+(Binary const &lhs, Binary const &rhs)
{
Binary tmp(lhs);
return std::move(tmp += rhs);
}
)
Now the compiler applies return value optimization, and returns the
temporaries, rather than constructing new objects, and the final call of the
move constructor disappears.
But we're not there yet: in the next section we'll encounter possibilities
for some additional and interesting optimizations.
|