File: binary.yo

package info (click to toggle)
c%2B%2B-annotations 13.02.02-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 13,576 kB
  • sloc: cpp: 25,297; makefile: 1,523; ansic: 165; sh: 126; perl: 90; fortran: 27
file content (196 lines) | stat: -rw-r--r-- 9,931 bytes parent folder | download | duplicates (4)
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.