refined section 11.6 abount overloading binary operators

This commit is contained in:
Frank B. Brokken 2017-11-14 20:09:44 +01:00
parent 8dc4cdacd5
commit a276c1fe13
7 changed files with 75 additions and 61 deletions

View file

@ -5,9 +5,9 @@ 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 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:
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 contents of tt(s3) is added to tt(s2). Next, tt(s2)
@ -30,8 +30,8 @@ value of the expression.
)
)
Let's consider the following code, in which a class tt(Binary) supports
an overloaded tt(operator+):
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:
@ -54,10 +54,10 @@ 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) no promotions are 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'.
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
@ -75,7 +75,7 @@ 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, several promotions are
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
@ -96,14 +96,14 @@ 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 true member
functions. Moreover, compound assignment operators should 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.,
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 both the
declaration of the plain binary operator and the corresponding compound
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)
@ -114,7 +114,7 @@ and swap. Here is our implementation of the compound assignment operator:
verb(
Binary &Binary::operator+=(Binary const &rhs)
{
Binary tmp(*this);
Binary tmp{ *this };
tmp.add(rhs); // this might throw
swap(tmp);
return *this;
@ -124,7 +124,8 @@ and swap. Here is our implementation of the compound assignment operator:
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, so it can call tt(Binary's add) member:
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:
@ -133,7 +134,7 @@ The binary operator's implementation becomes:
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 will encounter below and in
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(
@ -142,18 +143,29 @@ operator (which should also be declared as a friend in the class interface):
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. Although the bf(C++) standard does not explicitly
advise against it, it would be a violation of the principle of least surprise
to return that modified lhs operand as an rvalue reference: objects returned
from binary operators should neither be the operators' lhs operand nor the
operators' rhs operand. Instead these operators should return objects created
by them. But it's OK to use the move constructor to return a copy of a
modified lhs operand.
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. Then directly perform the
binary operation on that object and the operator's rhs operand, and then
return the modified object. It's a matter of taste which one is preferred.
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
@ -169,15 +181,14 @@ behavior:
// second implementation: move construct ret from lhs
Binary operator+(Binary &&lhs, Binary const &rhs)
{
Binary ret{std::move(lhs)};
Binary ret{ std::move(lhs) };
ret.add(rhs);
return ret;
}
)
Now, when executing an expression like (all tt(Binary) objects) tt(b1 + b2
+ b3) the following functions are called:
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)
@ -189,5 +200,5 @@ behavior:
)
But we're not there yet: in the next section we encounter possibilities
for more interesting implementations, concentrating on compound assignment
operators.
for several more interesting implementations, in the context of compound
assignment operators.

View file

@ -25,7 +25,7 @@ class Binary
void swap(Binary &other);
Binary &operator+=(Binary const &rhs) &;
Binary &&operator+=(Binary const &rhs) &&;
Binary operator+=(Binary const &rhs) &&;
private:
void add(Binary const &other);

View file

@ -1,6 +1,6 @@
#include "binary.ih"
Binary &&Binary::operator+=(Binary const &rhs) &&
Binary Binary::operator+=(Binary const &rhs) &&
{
cout << "&&" << d_nr << ':' << d_copy << " += " <<
rhs.d_nr << ':' << rhs.d_copy << '\n';

View file

@ -3,10 +3,9 @@
#define SOURCES "*.cc"
#define OBJ_EXT ".o"
#define TMP_DIR "tmp"
//#define USE_ALL "a"
#define USE_ECHO ON
#define CXX "g++"
#define CXXFLAGS " --std=c++14 -Wall -O2" \
#define CXXFLAGS " --std=c++17 -Wall -O2" \
" -fdiagnostics-color=never "
#define IH ".ih"
#define REFRESH
@ -14,4 +13,4 @@
#define ADD_LIBRARIES "bobcat"
#define ADD_LIBRARY_PATHS ""
#define DEFCOM "program"
#define DEFCOM "program"

View file

@ -1,5 +1,7 @@
class Binary
{
friend Binary operator+(Binary const &lhs, Binary const &rhs);
public:
Binary();
Binary(int value);
@ -8,6 +10,4 @@
private:
void add(Binary const &other);
friend Binary operator+(Binary const &lhs, Binary const &rhs);
};

View file

@ -1,6 +1,6 @@
Binary operator+(Binary const &lhs, Binary const &rhs)
{
Binary tmp(lhs);
Binary tmp{ lhs };
tmp.add(rhs);
return tmp;
}

View file

@ -1,5 +1,5 @@
In the previous section we saw that binary operators (like tt(operator+)) can
be implemented very efficiently, but still require at least move constructors.
We've seen that binary operators (like tt(operator+)) can be implemented very
efficiently, but require at least move constructors.
An expression like
verb(
@ -28,13 +28,14 @@ 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 there's indeed no need for yet another temporary
object. But different from the binary operators compound assignment operators
don't have an explicitly defined left-hand side operand. In situations like
these we nevertheless can inform the compiler that a member (not just compound
assignment operators) should only be used when the objects calling those
members is an rvalue reference, or an lvalue reference to either a modifiable
or non-modifiable object. For this we use
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).
@ -46,17 +47,20 @@ 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
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 then return. Here
is the implementation of tt(operator+=) tailored to being used by temporary
objects:
Reference qualifiers allow us to fine-tune our implementations of compund
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 then return a move-constructed copy of the temporary (note that
the copy is required, since the temporary object itself ceases to exist once
the function's return statement has been executed, see the previous
section). Here is the implementation of tt(operator+=) tailored to being used
by temporary objects:
verb(
Binary &&Binary::operator+=(Binary const &rhs) &&
Binary Binary::operator+=(Binary const &rhs) &&
{
add(rhs); // directly add rhs to *this,
return std::move(*this); // return *this as rvalue ref.
return std::move(*this); // return a move constructed copy
}
)
This implementation is about as fast as it gets.
@ -103,7 +107,7 @@ with tt(Binary{} += b2 += b3) we observe:
operator+= (&&) = Binary{} += b2
adding = add(b2)
return = Binary{}
return = move(Binary{})
)
For tt(Binary &Binary::operator+=(Binary const &other) &) an alternative