const methods and reference binding in C++
You can often find identically-looking methods declarations that only differ in their const qualification. One can modify its own object, the other one can’t. How do you know which one will get called? Overloading rules for “normal”, non-member, functions can help, but not quite.
Consider these two methods:
When the compiler needs to perform overloading (decide which function to call)
in an expression like a.foo()
it transforms the above declarations into
and the call into
creating an overload set of two functions that differ in cv-qualification of
their first and only reference parameter called this
.
Therefore, if foo()
is called for a const
object, the second, more
cv-qualified, version will be called:
Difference Between Methods and Free-Standing Functions
In general, that is a convenient mental model to have when dealing with const-, volatile- or ref-qualified methods in C++. However, because it is C++, there is a complication.
Consider this example:
Following the logic outlined above, we can transform that call into
foo(bar())
. The argument to foo()
is a non-const rvalue of type A
.
Compiler always tries to bind to the least-restrictive function, so the natural
candidate would be void A::foo(A& this)
. However, in this case it can’t
because the rules of reference binding (see [dcl.init.ref] and [over.ics.ref]
sections of the C++ standard) disallow binding of
an rvalue to a non-const reference. To illustrate,
So the only option left to bind the call to would be the const-qualified one:
void A::foo(const A& this)
.
In reality, the opposite happens: the non-const method gets chosen meaning that
an rvalue (the return value of bar()
) got bound to a non-const reference A&
this
. This is because there’s an exception to the reference binding rules that
are applied during overloading (quoting [over.match.funcs] p. (5.1)):
“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.”
In other words, given a declaration like void f(A&)
, you can’t do this
But if f()
is a member function void A::f()
, you can call it for a
temporary object:
Non-trivial Reference Binding
Things can get even more complicated (surprise, suprise!) with the introduction of type conversion operators and converting constructors. Consider this example:
So there is a call to a function that takes a reference and the argument can be converted to type of that reference in two ways: using the type conversion operator (1) or the converting constructor (2).
Here’s what is supposed to happen from the standard’s point of view: we are looking for the best viable function (see 16.3.3 [over.match.best]) and the candidate’s parameter is of reference type. This case is covered by 13.3.3.1.4 [over.ics.ref] and 8.6.3 [dcl.init.ref] that say - roughly - that if it’s possible to convert the argument using a conversion function and reference would bind directly to the result of such conversion, we’re done and shouldn’t look for converting constructors. In our case, type conversion operator also returns a reference, so (5.1.2) of 8.6.3 [dcl.init.ref] applies and that means direct binding.
Therefore, the call bar(a)
is OK and the type conversion operator of B
should be called (2).
Now, if we remove the ampersand from that type conversion operator so that it no longer converts to a reference, but rather to an rvalue of type A:
we would no longer have the result of conversion binding directly and,
consequently, no longer eligible for the early exit from overloading, which
forces us to look also for converting constructors. In that case, there would
be two equally-ranking implicit conversion sequences (see 16.3.3.2
[over.ics.rank]), meaning that the call bar(a)
is no longer valid.
It’s worth mentioning that not all compilers agree (at the time of writing) and g++ decides in favor of the converting constructor (2) instead of issuing an error about type conversion ambiguity.
The list of core issues that affected this particular example is impressive (at least, to me):
References
- The latest draft of the C++ standard,
- List of all core language issues (bugs in the standard),
- Matt Godbolt’s Compiler Explorer - a service that lets you run your C++ ideas past a whole bunch of compilers in no time.