Showing posts with label cpp_overload. Show all posts
Showing posts with label cpp_overload. Show all posts

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.

Nov 13, 2022

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

While C++'s function overload resolution is perplex, Jeff Preshing had a way to explain it in one GIF.

Although there are sophisticated details (e.g. ADL, pointer to function;pointer to member function etc.) which should be known by seasoned C++ engineers, the GIF provides a quick look-up for code that hits ambiguous resolution state.

Reference:
https://preshing.com/20210315/how-cpp-resolves-a-function-call/


credit: Jeff Preshing



May 5, 2014

[C++] function overload resolution is completed inside a struct/class scope

1.
function overload resolution is completed inside a struct/class scope, also before virtual function dispatch.

Code below member ‘fun’ is ambiguous

struct Dummy{};                                                                           
                                                                                          
struct Base1                                                                              
{                                                                                         
   virtual void fun(int)                                                                  
   {                                                                                      
   }                                                                                      
};                                                                                        
                                                                                          
struct Base2                                                                              
{                                                                                         
   virtual void fun(Dummy){}                                                              
};                                                                                        
                                                                                          
                                                                                          
struct De : Base1, Base2                                                                  
{                                                                                         
};                                                                                        
                                                                                          
                                                                                          
int main()                                                                                
{                                                                                         
   De d;                                                                                  
   d.fun(Dummy());                                                                        
}   

2.
function overload resolution happens in sequence, which can't goes back.
Like SFINAE

--
If the resulting set of declarations are 

1. not all from sub-objects of the same type,

2. or the set has a non-static member and includes members from distinct sub-objects,

there is an ambiguity and the program is ill-formed. 
Otherwise that set is the result of the lookup.

Reference:
Name resolution and overloading
Function with parameter type that has a copy-constructor with non-const ref chosen?