c++templatesoverloading

C++ detecting function overload without conversions


I've have a load of function overloads, mainly consisting of two signatures:

void func(const my_type1&);  
void func(const my_type2&, hash_t);

and a function that tries to call func based on function existance:

template <typename T>
void func_imp(const T& t, hash_t hash){
    if constexpr (has_func_with_2nd_param_v<T>){
        func(t, hash);
    }else{
        func(t);
    }
}

but I'm running into conversion issues which end up calling the 'wrong' overload:

#include <type_traits>
#include <cstdio>

struct hash_t{
};

struct my_type1{
    my_type1() {}
};

struct my_type2{
    my_type2() {}
    my_type2(const my_type1&){}
};

void func(const my_type1&){
    printf("no hash\n");
}

void func(const my_type2&, hash_t=hash_t()){
    printf("hash\n");
}

template<typename T, typename = void>
struct has_func_with_2nd_param : std::false_type {};

template<typename T>
struct has_func_with_2nd_param<T, std::void_t<decltype(func(std::declval<T>(), std::declval<hash_t>()))>> : std::true_type {};

template<typename T>
constexpr bool has_func_with_2nd_param_v = has_func_with_2nd_param<T>::value;

// I want to route to the appropriate function without conversion routing to the wrong func
template <typename T>
void func_imp(const T& t, hash_t hash){
    if constexpr (has_func_with_2nd_param_v<T>){
        func(t, hash);
    }else{
        func(t);
    }
}

int main()
{
    // this is true as expected
    static_assert(has_func_with_2nd_param_v<my_type2>);

    // this is also true, since my_type2 is constructable from my_type1 and then finds finds func(my_type2, hash_t)
    static_assert(has_func_with_2nd_param_v<my_type1>);

    func_imp(my_type1(), hash_t()); // prints "hash", but wanting it to call the "no hash" version
    func_imp(my_type2(), hash_t()); // prints "hash"

    return 0;
}

I'm having trouble coming up with the correct meta function to get the behavior I want. Anyone got any ideas?

godbolt here


Solution

  • You can make use of the rule that at most one user-defined type conversion is allowed and add a dummy type that enforces another user-defined type conversion. This way you'll have to have exact type match for the original types.

    template <typename T>
    struct ForceExactClassType{
        operator const T&() const;
    };
    

    The struct ForceExactClassType is only used to enforce a user-defined conversion in unevaluated contexts, so a definition for operator const T&() is not needed. You may need to fine-tune the const and & parts depending on your needs, but the underlying mechanism is the same.

    To enforce the extra conversion, the type trait should be changed to

    template<typename T>
    struct has_func_with_2nd_param<T, std::void_t<decltype(func(std::declval<ForceExactClassType<T>>(), std::declval<hash_t>()))>> : std::true_type {};
    

    Then at least for the example you showed, it behaves as expected.

    Outputs:

    no hash
    hash
    

    To some extent, the is_exactly_invocable trait you mentioned in a comment to a now-deleted answer can be implemented as

    template <typename Fn, typename... ArgTypes>
    struct is_exactly_invocable: std::is_invocable<Fn, ForceExactClassType<ArgTypes>...>
    {};
    

    Note that this has the limitation of only working for class types. It would not work for types that can be implicitly converted without going through a user-defined conversion function. It would also fail to work if Fn is a functor that has a templated operator() because conversions are not considered in template parameter deductions (pointed out in a comment by @Jarod42).

    With the general trait defined as above, the following assertions pass:

    static_assert(is_exactly_invocable<void(const my_type2&, hash_t), my_type2, hash_t>::value);
    static_assert(!is_exactly_invocable<void(const my_type2&, hash_t), my_type1, hash_t>::value);
    

    Demo: https://godbolt.org/z/Kqr1n14b6