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
|
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 tt(operator+) members.
Most binary operators come in two flavors: the plain binary operator (like
the tt(+) operator) and the compound binary assignment operator (like
tt(operator+=)). Whereas the plain binary operators return values, the
compound binary assignment operators usually return references to the objects
for which the operators were called. 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 content of tt(s3) is added to tt(s2). Next, tt(s2)
is returned, and its new content is assigned to tt(s1). Note that tt(+=)
returns tt(s2).
it() at tt(// 2) the content 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 content 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 content 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.
)
)
Now 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 single arguments may implicitly
be activated when an argument of an appropriate type is provided. We've
already encountered this with tt(std::string) objects, where NTBSs may be used
to initialize tt(std::string) objects.
Analogously, in statement tt(// 1), tt(operator+) is called, using tt(b2)
as its left-hand side operand. This operator expects another tt(Binary) object
as its right-hand side operand. However, an tt(int) is provided. But as a
constructor tt(Binary(int)) exists, the tt(int) value can be promoted to a
tt(Binary) object. Next, this tt(Binary) object is passed as argument to the
tt(operator+) member.
Unfortunately, in statement tt(// 2) promotions are not available: here
the tt(+) operator is applied to an tt(int)-type lvalue. An tt(int) is a
primitive type and primitive types have no knowledge of `constructors',
`member functions' or `promotions'.
How, then, are promotions of left-hand operands implemented in statements
like tt("prefix " + s3)? Since promotions can be 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 these operators. 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)
After defining binary operators as free functions, several promotions are
available:
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 neither operand is 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 candidates 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}), which enables the compiler
to resolve the ambiguity to the first overloaded tt(+) operator.
)
The next step consists of implementing the required overloaded binary
compound assignment operators, having the form tt(@=), where tt(@) represents
a binary operator. As these operators em(always) have left-hand side operands
which are object of their own classes, they are implemented as genuine member
functions. Compound assignment operators usually return references to the
objects for which the binary compound assignment operators were requested, as
these objects might be modified in the same statement. E.g.,
tt((s2 += s3) + " postfix").
Here is our second revision of the class tt(Binary), showing the
declaration of the plain binary operator as well as the corresponding compound
assignment operator:
verbinclude(-a examples/binary2.h)
How should the compound addition assignment operator be implemented? When
implementing compound binary assignment operators the strong guarantee should
always be kept in mind: if the operation might throw use a temporary object
and swap. Here is our implementation of the compound assignment operator:
verb( Binary &Binary::operator+=(Binary const &rhs)
{
Binary tmp{ *this };
tmp.add(rhs); // this might throw
swap(tmp);
return *this;
})
It's easy to implement the free binary 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, using copy elision. The class tt(Binary) declares the
free binary operator as a friend (cf. chapter ref(Friends)), so it can call
tt(Binary's add) member:
verbinclude(-a examples/binary3.h)
The binary operator's implementation becomes:
verbinclude(-a examples/binaryadd1.cc)
If the class tt(Binary) is move-aware then it's attractive to add move-aware
binary operators. In this case we also need operators whose left-hand side
operands are rvalue references. When a class is move aware various interesting
implementations are suddenly possible, which we encounter below, and in
the next (sub)section. First have a look at the signature of such a binary
operator (which should also be declared as a friend in the class interface):
verb( Binary operator+(Binary &&lhs, Binary const &rhs);)
Since the lhs operand is an rvalue reference, we can modify it em(ad lib).
Binary operators are commonly designed as factory functions, returning objects
created by those operators. However, the (modified) object referred to by
tt(lhs) should itself em(not) be returned. As stated in the C++ standard,
quote(
A temporary object bound to a reference parameter in a function call
persists until the completion of the full-expression containing the call.
)
and furthermore:
quote(
The lifetime of a temporary bound to the returned value in a function
return statement is not extended; the temporary is destroyed at the end of
the full-expression in the return statement.
)
In other words, a temporary object cannot itself be returned as the
function's return value: a tt(Binary &&) return type should therefore not be
used. Therefore functions implementing binary operators are factory functions
(note, however, that the returned object may be constructed using the class's
move constructor whenever a temporary object has to be returned).
Alternatively, the binary operator can first create an object by move
constructing it from the operator's lhs operand, performing the binary
operation on that object and the operator's rhs operand, and then return the
modified object (allowing the compiler to apply copy elision). It's a matter
of taste which one is preferred.
Here are the two implementations. Because of copy elision the explicitly
defined tt(ret) object is created in the location of the return value. Both
implementations, although they appear to be different, show identical run-time
behavior:
verb( // first implementation: modify lhs
Binary operator+(Binary &&lhs, Binary const &rhs)
{
lhs.add(rhs);
return std::move(lhs);
}
// second implementation: move construct ret from lhs
Binary operator+(Binary &&lhs, Binary const &rhs)
{
Binary ret{ std::move(lhs) };
ret.add(rhs);
return ret;
})
Now, when executing expressions like (all tt(Binary) objects)
tt(b1 + b2 + b3) the following functions are called:
verb( copy operator+ = b1 + b2
Copy constructor = tmp(b1)
adding = tmp.add(b2)
copy elision : tmp is returned from b1 + b2
move operator+ = tmp + b3
adding = tmp.add(b3)
Move construction = tmp2(move(tmp)) is returned)
But we're not there yet: in the next section we encounter possibilities
for several more interesting implementations, in the context of compound
assignment operators.
|