Nov 13, 2025

[C++] Value vs. Reference type conversion overload ranking tie breaker rule.

[C++] function overload resolution in one GIF (by Jeff Preshing)

there's a missing part related to binding to value or reference.

  • Value vs. Reference:
    Creating conversion operators for both T and T& creates ambiguity because the compiler views obtaining a value directly and obtaining a reference to copy as equally valid.
  • Reference vs. Reference:
    Creating operators for T& and T&& avoids ambiguity because the C++ standard has specific tie-breaking rules to rank one reference type over the other.
C++ ISO Overloading rules link:

Failed:
#include <type_traits>

struct Any {
    template <class T> operator T();
    template <class T> operator T&();
};

// ✅ This passes
// The compiler specifically looks for a reference, so it chooses operator T&()
static_assert(std::is_convertible_v<Any, int&>); 

// ❌ This FAILS (Ambiguous)
// The compiler cannot decide between:
// 1. calling operator T() to get an int
// 2. calling operator T&() to get an int& (which creates an int via copy)
//
// static_assert(std::is_convertible_v<Any, int>);

Success:
struct Any {
    template <class T> operator T&();
    template <class T> operator T&&();
};

// ✅ This passes!
// Unlike the previous example, the compiler doesn't view this as ambiguous.
// Because 'Any' is treated as an rvalue here, the standard prefers the 
// operator T&&() (rvalue reference) binding over the T&() binding.
static_assert(std::is_convertible_v<Any, int>);


When building the list of candidates, C++ considers deleted constructors 
(and then errors or substitution-fails if the candidate is selected).

In our case, before it can select a candidate, there's an ambiguity between selecting 
T& and the copy constructor or T&& and the move constructor.

Failed:
struct Any {
    // The two conversion operators from the previous slide
    template <class T> operator T&();
    template <class T> operator T&&();
};

struct MoveOnly {
    // Declaring a user-defined move constructor causes the 
    // copy constructor to be implicitly deleted.
    MoveOnly(MoveOnly&&); 
};

// ❌ This FAILS (Ambiguous)
//
// Even though the Copy Constructor is deleted, the compiler considers it
// a "candidate" during overload resolution. 
//
// The compiler sees two ways to turn 'Any' into 'MoveOnly':
// 1. Call Any::operator T&()  -> Then call MoveOnly(const MoveOnly&) [Copy]
// 2. Call Any::operator T&&() -> Then call MoveOnly(MoveOnly&&)      [Move]
//
// C++ rules state that different user-defined conversion sequences are 
// not comparable. The compiler cannot determine which path is "better,"
// so it errors out with "ambiguous conversion" before noticing the copy
// constructor is deleted.
//
// static_assert(std::is_convertible_v<Any, MoveOnly>);

Success:
This solution works by using const-correctness rules as a tie-breaker.
Here is the step-by-step breakdown of why this specific change fixes the ambiguity:
1. The Setup
When you perform std::is_convertible_v<Any, MoveOnly>, you are effectively creating a temporary (rvalue) Any object and trying to turn it into a MoveOnly. Importantly, this temporary Any object is mutable (non-const).

2. The Two Candidates
The compiler looks at the two conversion operators to see which one fits the Any object better. This comparison happens on the implicit object parameter (the this pointer).

Candidate A (operator T&() const): To call this function, the compiler must treat the non-const Any object as const. In C++ terms, this requires a qualification conversion (adding const).

Candidate B (operator T&&()): To call this function, the compiler uses the Any object exactly as it is (non-const). No qualification conversion is needed.

3. The Tie-Breaker
C++ overload resolution rules state that an exact match is better than a match that requires adding const.

Because Candidate B matches the "const-ness" of the object perfectly, it is strictly considered a better function than Candidate A.

4. The Result
Since Candidate B is strictly better, Candidate A is discarded entirely.
The compiler picks only operator T&&().
operator T&&() returns an rvalue reference (T&&).
This rvalue reference perfectly matches the MoveOnly(MoveOnly&&) constructor.
The code compiles successfully, avoiding the deleted copy constructor entirely.

struct Any {
    template <class T> operator T&() const;
    template <class T> operator T&&();
};

struct MoveOnly {
    // Declaring a user-defined move constructor causes the 
    // copy constructor to be implicitly deleted.
    MoveOnly(MoveOnly&&); 
};

static_assert(std::is_convertible_v<Any, MoveOnly>); // ✅

struct Immovable { Immovable(Immovable&&) = delete; };
static_assert(std::is_convertible_v<Any, Immovable>); // ❌

This works due to
Template Argument Deduction for Conversion Functions In C++.
When a template is used as a conversion operator, the template parameter T is deduced from the type that is required by the context. 

This is the opposite of a normal function template where T is deduced from the arguments passed in. 
  • The Rule: This is officially called Template argument deduction - conversion function (defined in the C++ standard under [temp.deduct.conv]). 
  • The Process: The compiler sees that it needs a MoveOnly object. It looks at Any and finds the conversion templates.
    It attempts to match T such that the result of the operator is compatible with MoveOnly. It deduces T = MoveOnly.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.