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:

struct A
{
    void foo();       /* (1) */
    void foo() const; /* (2) */
};

When the compiler needs to perform overloading (decide which function to call) in an expression like a.foo() it transforms the above declarations into

void A::foo(A& this);       /* (1) */
void A::foo(const A& this); /* (2) */

and the call into

foo(a);

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:

void bar(const A& a)
{
    a.foo(); /* calls (2) */
}

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:

A bar();

void baz()
{
    bar().foo(); /* calls (1) or (2)? */
}

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,

A& a = A(); /* Error: can't bind non-const reference to a temporary (rvalue) */
const A& ca = A(); /* OK: binding to const reference is allowed */

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

f(A()); /* Error: A() is a temporary (rvalue), can't bind to non-const reference */

But if f() is a member function void A::f(), you can call it for a temporary object:

A().f(); /* OK; effectively the same as f(A()), but allowed */

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:

struct A;

struct B {
    operator A& () const ; // (1) type conversion operator
};

struct A {
    A(B); // (2) converting constructor
};

void bar(const A&);

void foo(B b)
{
    bar(b); // OK or error? If OK, how b will be converted to type A?
}

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:

struct B {
    operator A () const ; // converts to A, not to 'reference to 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

Maxim Kartashev

Maxim Kartashev
Pragmatic, software engineer. Working for Altium Tasking on compilers and tools for embedded systems.