Showing posts with label cpp_Surrogate_Call_Function. Show all posts
Showing posts with label cpp_Surrogate_Call_Function. 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.

Aug 11, 2024

[C++] use surrogate pattern instead of function overloading

https://en.cppreference.com/w/cpp/utility/variant/visit

#include <iomanip>
#include <iostream>
#include <string>
#include <type_traits>
#include <variant>
#include <vector>
 
// the variant to visit
using var_t = std::variant<int, long, double, std::string>;
 
// helper type for the visitor #4
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
 
int main()
{
    std::vector<var_t> vec = {10, 15l, 1.5, "hello"};
 
    for (auto& v: vec)
    {
        // 1. void visitor, only called for side-effects (here, for I/O)
        std::visit([](auto&& arg){ std::cout << arg; }, v);
 
        // 2. value-returning visitor, demonstrates the idiom of returning another variant
        var_t w = std::visit([](auto&& arg) -> var_t { return arg + arg; }, v);
 
        // 3. type-matching visitor: a lambda that handles each type differently
        std::cout << ". After doubling, variant holds ";
        std::visit([](auto&& arg)
        {
            using T = std::decay_t<decltype(arg)>;
            if constexpr (std::is_same_v<T, int>)
                std::cout << "int with value " << arg << '\n';
            else if constexpr (std::is_same_v<T, long>)
                std::cout << "long with value " << arg << '\n';
            else if constexpr (std::is_same_v<T, double>)
                std::cout << "double with value " << arg << '\n';
            else if constexpr (std::is_same_v<T, std::string>)
                std::cout << "std::string with value " << std::quoted(arg) << '\n';
            else 
                static_assert(false, "non-exhaustive visitor!");
        }, w);
    }
 
    for (auto& v: vec)
    {
        // 4. another type-matching visitor: a class with 3 overloaded operator()'s
        // Note: The `(auto arg)` template operator() will bind to `int` and `long`
        //       in this case, but in its absence the `(double arg)` operator()
        //       *will also* bind to `int` and `long` because both are implicitly
        //       convertible to double. When using this form, care has to be taken
        //       that implicit conversions are handled correctly.
        std::visit(overloaded{
            [](auto arg) { std::cout << arg << ' '; },
            [](double arg) { std::cout << std::fixed << arg << ' '; },
            [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; }
        }, v);
    }
}

the overload type can replace function overload with surrogate calls. 
Ref:

Sep 15, 2017

[C++] Using Surrogate Call Function for speed up member function name resolution.

Reference:
https://mpark.github.io/programming/2017/08/08/using-killed-the-FUN-on-GCC-5/

Surrogate Call Function:
http://en.cppreference.com/w/cpp/language/overload_resolution#Call_to_a_class_object

sample code:
template <typename Head, typename... Tail>
struct FUN;

// Base case.
template <typename Head>
struct FUN<Head> {
  using F = Head (*)(Head);
  operator F() const;
};

// Recursive case.
template <typename Head, typename... Tail>
struct FUN : FUN<Tail...> {
  using F = Head (*)(Head);
  operator F() const;
};


or just:
template <typename T>
struct FUN_leaf {
  using F = T (*)(T);
  operator F() const;
};

template <typename... Ts>
struct FUN : FUN_leaf<Ts>... {};


Originally(SLOW) using inheritance introduce function name overloading for avoiding hidden ancestor type's function name:
template <typename Head, typename... Tail>
struct FUN;

// Base case.
template <typename Head>
struct FUN<Head> {
  Head operator()(Head) const;
};

// Recursive case.
template <typename Head, typename... Tail>
struct FUN : FUN<Tail...> {
  using FUN<Tail...>::operator();
  Head operator()(Head) const;
};


Surrogate function example:
int f1(int);
int f2(float);

typedef int (*fp1)(int);
typedef int (*fp2)(float);

struct A {
  operator fp1() { return f1; }
  operator fp2() { return f2; }
} a;

int i = a(1);  // calls f1 via pointer returned from conversion function