Reputation: 1396
This may be related to the fact that taking references to temporary objects is allowed only in some situations, but I'm not sure how this rule applies.
In this example, the compiler stops at line a + b + c
because a + b
returns an Int
, to which a reference must be taken in order to evaluate (a+b) + c
. According to what I've read, this is a deliberate choice so temporary values aren't mutated. The way to fix this code is to change the signature to Int operator + (const Int & a, const int & b)
.
struct Int {
int a;
Int() { a = 0; }
Int(int i) { a = i; }
friend Int operator + (Int &, const Int &);
};
Int operator + (Int & a, const Int & b) {
return Int(a.a + b.a);
}
int main() {
Int a, b, c;
a + b + c;
return 0;
}
However, if we change the operator from being a friend
function to being a struct member, then this code, which should have the same problem with creating a temporary mutable value, actually compiles:
struct Int {
int a;
Int() { a = 0; }
Int(int i) { a = i; }
Int operator + (Int &);
};
Int Int::operator + (Int & v)
{
return Int(a + v.a);
}
int main()
{
Int a, b, c;
a + b + c;
return 0;
}
Why exactly does this happen? These two situations seem mostly identical, but in the latter, not only is const
-ness not required for the RHS value, it's not required for the LHS value either.
Upvotes: 3
Views: 133
Reputation: 73186
friend Int operator + (Int &, const Int &);
In this signature, the left hand side argument is non-const ref, and rvalues cannot bind to non-const lvalue references to extent their lifetime.
a + b + c;
// ^^^^^ - result: rvalue (temporary)
// -> (rvalue) + c
// -> rvalue cannot bind to non-const lvalue reference.
They may, however, bind to const lvalue references to extend their lifetime.
These two situations seem mostly identical, [...]
An representable example for the member function approach would be to CV- and ref-qualify the member function as non-const lvalue ref:
Int operator+(Int &) &; // ref-qualifier: &
// const-qualifier: non (non-const)
in which case you will get a similar error as for the friend
function above.
Similarly, the compiling example
friend Int operator + (Int &, const Int &);
would be comparable to const
and &
-qualifying the member function:
Int operator+(Int &) const &;
as an rvalue *this
could bind to const &
, extending its lifetime.
The reason why the non-ref-qualified case is accepted may come as a surprise, as the type of the implicit object parameter is an lvalue reference if no ref-qualifier is supplied, as per [over.match.funcs]/4: [emphasis mine]:
For non-static member functions, the type of the implicit object parameter is
- “lvalue reference to cv
X
” for functions declared without a ref-qualifier or with the & ref-qualifier
However, as covered by [over.match.funcs]/5, this is an explicit exception for the case where no ref-qualifier has been supplied [emphasis mine]:
[...] For non-static member functions declared without a ref-qualifier, an additional rule applies:
- even if the implicit object parameter is not const-qualified, an rvalue can be bound to the parameter as long as in all other respects the argument can be converted to the type of the implicit object parameter.
Upvotes: 3
Reputation: 23497
In the first case, the code is effectively equivalent to:
operator+(operator+(a, b), c);
Here, the expression operator+(a, b)
is an rvalue, which cannot be bound to a non-const lvalue reference.
In the second case, we have:
a.operator+(b).operator+(c);
There is no such problem, since only lvalues b
and c
are bound to that non-const lvalue reference parameter.
As @BenVoigt pointed out, you may wonder about calling a non-const member function on a temporary. This is perfectly legal. There is a special rule about it in the Standard, which is described in detail in the @dfrib's answer.
Upvotes: 5
Reputation: 275405
Try a+(b+c)
and watch your member version fail.
C++ has named values (aka lvalues, named because they are values that make sense on the LEFT hand side of an =
assignment) and temporaries (aka rvalues, which go on the RIGHT hand side of an =
assignment). (this paragraph contains simplifications).
lvalues will bind to non-const references, while rvalues will only bind to const references. So Int&
won't bind to a temporary, while Int const&
or const Int&
(same thing) will.
The error you are getting is that a+b+c
is parsed as (a+b)+c
by the order of operations in C++. And the result of a+b
is a temporary (an rvalue). So it won't bind to Int&
.
A method like Int operator+(Int& rhs)
takes *this
as the left-hand argument to +
, and rhs
is the right-hand argument. So (a+b)+c
, a+b
is *this
and c
is rhs
. Member operator+
works on temporaries, so all is good.
A friend like Int operator+(Int&, Int const&)
-- you'll note the Int&
is on the left. The left in (a+b)+c
is a temporary when you add +c. So it fails, because a=b
won't bind to Int&
Finally, there are quirks in how members bind to temporaries. Because members where added to C++ 20+ years before rvalue/lvalue references where, they'll bind to temporaries far more liberally than you might expect (by default).
Your operator+
member takes a non-const reference on the right hand side. Your friend takes a non-const reference on the left hand side.
a+b+c
in C++ is parsed as (a+b)+c
. In every case, the right hand side of a +
has a named value -- an lvalue -- and named mutable values can bind to non-const references.
However, the +c
has a temporary on the left hand side -- an rvalue -- and temporaries cannot bind to non-const lvalue references.
The final quirk has to do with the fact that a member like this:
Int Int::operator + (Int & v)
{
return Int(a + v.a);
}
can be called on an rvalue (a temporary), even though it is non-const. l/r value qualification of objects was added after initial member syntax was.
There are a bunch of ways you are permitted to declare "what kind of object, const or non const, temporary or not, will this method work on". Their signatures look like:
Int operator + (Int const& v);
Int operator + (Int const& v) const;
Int operator + (Int const& v) &;
Int operator + (Int const& v) const&;
Int operator + (Int const& v) &&;
Int operator + (Int const& v) const&&;
if you don't include a trailing &
or a &&
, your methods bind to both rvalues (temporaries) and lvalues (named values with identity). This is because l/r value references where added 20+ years after methods where added to C++, so the default method declaration works with both (for backward compatibility reasons).
friend Int operator + (Int &, const Int &);
corresponds to
Int operator + (const Int &) &;
which, as a method, will fail with basically the same error message as the friend does.
You can also induce the
friend Int operator + (Int &, Int const&);
method to work by typing:
a+(b+c)
as now the left-hand side of +c
is a temporary, and refuses to bind to Int&
.
This will also make the
Int operator+(Int&)
version fail, as now the right hand side of a+
is (b+c)
, which is a temporary, and won't bind to Int&
.
When working on operators for a class, do this.
Int operator+=(Int const& rhs)&;
this is the increment by operator. It should be a member, and it should have the &
qualification at the end -- incrementing a temporary makes no sense.
Then write:
friend Int operator+(Int lhs, Int const& rhs) {
lhs += rhs;
return lhs;
}
here we use the increment operator to implement +
. If our class has efficient move constructors (as it should), this will also be very close to optimal.
The left hand side argument we take by value. We then increment it. Then we return it (and this implicitly moves).
When you type:
a+b+c
this becomes
(a+b)+c
we copy a
and get:
copy of a += b
we then move the copy of a into the result. This is then elided (zero-cost moved) into the left hand argument of +c
.
We then increment that, and move it to the result.
So, eliminating moves (which should be near zero cost even on most complex objects), we get:
Int temporary = a;
temporary += b;
temporary += c;
return temporary;
as the result of a+b+c
.
This pattern works with simple things like fake integers, more complex things like strings, and a myriad of other situations. And it gives you +=
and +
for about the same amount of coding work as typically writing +
does.
Upvotes: 3