mirror of
https://gitlab.com/fbb-git/cppannotations
synced 2024-09-28 03:20:44 +02:00
refined section 11.6 abount overloading binary operators
This commit is contained in:
parent
8dc4cdacd5
commit
a276c1fe13
7 changed files with 75 additions and 61 deletions
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
Binary operator+(Binary const &lhs, Binary const &rhs)
|
||||
{
|
||||
Binary tmp(lhs);
|
||||
Binary tmp{ lhs };
|
||||
tmp.add(rhs);
|
||||
return tmp;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue