File: binary.yo

package info (click to toggle)
c%2B%2B-annotations 10.6.0-1
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 10,536 kB
  • ctags: 3,247
  • sloc: cpp: 19,157; makefile: 1,521; ansic: 165; sh: 128; perl: 90
file content (204 lines) | stat: -rw-r--r-- 9,616 bytes parent folder | download
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.