Forcing C++ to prefer an overload with an implicit conversion over a template
Asked Answered
R

2

7

I have a situation where I need overload resolution to prefer an overload with an implicit conversion over a template function with the same name.

Consider the following example:

#include <iostream>
#include <functional>

void call_function(const std::function<void()>& function)
{
   std::cout << "CALL FUNCTION 1" << std::endl;
   function();
}

template <typename Function>
void call_function(const Function& function)
{
    std::cout << "CALL FUNCTION 2" << std::endl;
    function();
}

int main()
{
    // Pass a lambda to "call_function"
    // This lambda is implicitly convertible to 'std::function<void()>'
    // Even though it is implicitly convertible, the template function is selected by the compiler.
    call_function([]{
        std::cout << "TEST" << std::endl;
    });
}

Output:

CALL FUNCTION 2
TEST

Unfortunately, the compiler seems to detect that the first implementation of call_function would require an implicit conversion to convert the lambda I pass it into a std::function<void()> object and because of this it determines that the template version is a better match and uses the template. I need to force the compiler to prefer the implicit conversion overload over the template so the output would be:

CALL FUNCTION 1
TEST

How can I achieve this? (Also note that I am restricted to a C++11 compliant compiler so I am unable to use features from C++14 and beyond)

Revolving answered 3/2, 2021 at 23:18 Comment(2)
Is it surprising that the compiler prefers a perfect match to one which requires a conversion? That seems pretty straightforward to me. You should get rid of the one that takes a std::function and make everything templates.Bucentaur
Can you explain why you would want the compiler to prefer doing an implicit conversion over a perfect match? (And, no, "that's what I want to do" is not an explanation). With the template in play, there will always be a perfect match for the compiler to prefer, unless you explicitly convert the lambda to the std::function at the call point.Ptolemaic
F
3

If you want to change the overloads such that the former overload is chosen whenever there is an implicit conversion possible, with the latter being the backup, you can do this with SFINAE via std::enable_if:

#include <type_traits>

void call_function(const std::function<void()>& function)
{
   std::cout << "CALL FUNCTION 1" << std::endl;
   function();
}

template <typename Function,
    // Consider this overload only if...
    typename std::enable_if<
        // the type cannot be converted to a std::function<void()>
        !std::is_convertible<const Function&, std::function<void()>>::value,
        int>::type = 0>
void call_function(const Function& function)
{
    std::cout << "CALL FUNCTION 2" << std::endl;
    function();
}

Demo


Alternatively, if you want to be able to support an unknown number of overloads of call_function with the "CALL FUNCTION 2" being a backup overload in case none of the functions work, you can do this too, but it requires quite a bit more work:

// Rename the functions to `call_function_impl`
void call_function_impl(const std::function<void()>& function)
{
   std::cout << "CALL FUNCTION 1" << std::endl;
   function();
}

void call_function_impl(const std::function<void(int, int)>& function)
{
   std::cout << "CALL FUNCTION 2" << std::endl;
   function(1, 2);
}

// The backup function must have a distinct name
template <typename Function>
void call_function_backup_impl(const Function& function)
{
    std::cout << "CALL FUNCTION backup" << std::endl;
    function();
}


// Implement std::void_t from C++17
template <typename>
struct void_impl {
    using type = void;
};

template <typename T>
using void_t = typename void_impl<T>::type;

// Make a type trait to detect if the call_function_impl(...) call works
template <typename Function, typename = void>
struct has_call_function_impl
    : std::false_type
{};

template <typename Function>
struct has_call_function_impl<Function,
    void_t<decltype(call_function_impl(std::declval<const Function&>()))>>
    : std::true_type
{};


// If the call_function_impl(...) call works, use it
template <typename Function,
    typename std::enable_if<
        has_call_function_impl<Function>::value,
        int>::type = 0>
void call_function(const Function& function)
{
    call_function_impl(function);
}

// Otherwise, fall back to the backup implementation
template <typename Function,
    typename std::enable_if<
        !has_call_function_impl<Function>::value,
        int>::type = 0>
void call_function(const Function& function)
{
    call_function_backup_impl(function);
}

Demo

Fosterling answered 3/2, 2021 at 23:40 Comment(8)
Is there a more generic way to do this if there are multiple other overloads for std::function<void(int)>, std::function<void(int, int)>, etc (an unknown amount of overloads)Revolving
Get rid of the non-template one. Then have templates that look at what parameters the passed in function takes and delegate on that.Bucentaur
@tjwrona1992 Do you want to have "overload these N functions, but if none of those overloads work, apply this backup", or do you want something more general even than that?Fosterling
Basically I have a function that converts a lambda into a C-style function pointer and I am using that function as follows: template <typename Lambda> void call_function(Lambda&& lambda) { call_function(convert_lambda(std::forward<Lambda>(lambda))) } The lambda here is being converted into a C-style function pointer which doesn't directly match with the type std::function so it ends up just recursively calling the template.Revolving
@tjwrona1992 You don't have to convert the lambda to a C-style function pointer just to pass it into a std::function. You can do without the overload and just pass the lambda into the function that accepts std::function.Fosterling
That doesn't work with capturing lambdas though. I need it to work with capturing lambdas as well. This convert_lambda function is able to convert lambdas with captures into C-style function pointers.Revolving
@tjwrona1992 That's not possible (sanely) without modifying the C-style function pointer to also take a void* context argumentFosterling
Accepting this answer because I think this is probably what most people who stumble upon this question will need. (Thanks for giving me the solution I needed in another post! #66038364)Revolving
M
6

Overload resolution will never prefer an implicit conversion over an exact match. Since the template will always match exactly, the only way to get the non-template selected is to assure that it doesn't require any conversion either.

To do that, you can cast the closure (the result of the lambda expression) to the correct type first:

    call_function(static_cast<std::function<void()>>([]{
        std::cout << "TEST" << std::endl;
    }));

Now what's passed is exactly the type taken by the first overload ("FUNCTION 1") so that's what'll be selected.

That said, if you care about which one is called, you probably shouldn't be using overloading. Overloading should normally be reserved for situations where the overloads are essentially equivalent, so you really don't care which gets called.

Meza answered 3/2, 2021 at 23:27 Comment(0)
F
3

If you want to change the overloads such that the former overload is chosen whenever there is an implicit conversion possible, with the latter being the backup, you can do this with SFINAE via std::enable_if:

#include <type_traits>

void call_function(const std::function<void()>& function)
{
   std::cout << "CALL FUNCTION 1" << std::endl;
   function();
}

template <typename Function,
    // Consider this overload only if...
    typename std::enable_if<
        // the type cannot be converted to a std::function<void()>
        !std::is_convertible<const Function&, std::function<void()>>::value,
        int>::type = 0>
void call_function(const Function& function)
{
    std::cout << "CALL FUNCTION 2" << std::endl;
    function();
}

Demo


Alternatively, if you want to be able to support an unknown number of overloads of call_function with the "CALL FUNCTION 2" being a backup overload in case none of the functions work, you can do this too, but it requires quite a bit more work:

// Rename the functions to `call_function_impl`
void call_function_impl(const std::function<void()>& function)
{
   std::cout << "CALL FUNCTION 1" << std::endl;
   function();
}

void call_function_impl(const std::function<void(int, int)>& function)
{
   std::cout << "CALL FUNCTION 2" << std::endl;
   function(1, 2);
}

// The backup function must have a distinct name
template <typename Function>
void call_function_backup_impl(const Function& function)
{
    std::cout << "CALL FUNCTION backup" << std::endl;
    function();
}


// Implement std::void_t from C++17
template <typename>
struct void_impl {
    using type = void;
};

template <typename T>
using void_t = typename void_impl<T>::type;

// Make a type trait to detect if the call_function_impl(...) call works
template <typename Function, typename = void>
struct has_call_function_impl
    : std::false_type
{};

template <typename Function>
struct has_call_function_impl<Function,
    void_t<decltype(call_function_impl(std::declval<const Function&>()))>>
    : std::true_type
{};


// If the call_function_impl(...) call works, use it
template <typename Function,
    typename std::enable_if<
        has_call_function_impl<Function>::value,
        int>::type = 0>
void call_function(const Function& function)
{
    call_function_impl(function);
}

// Otherwise, fall back to the backup implementation
template <typename Function,
    typename std::enable_if<
        !has_call_function_impl<Function>::value,
        int>::type = 0>
void call_function(const Function& function)
{
    call_function_backup_impl(function);
}

Demo

Fosterling answered 3/2, 2021 at 23:40 Comment(8)
Is there a more generic way to do this if there are multiple other overloads for std::function<void(int)>, std::function<void(int, int)>, etc (an unknown amount of overloads)Revolving
Get rid of the non-template one. Then have templates that look at what parameters the passed in function takes and delegate on that.Bucentaur
@tjwrona1992 Do you want to have "overload these N functions, but if none of those overloads work, apply this backup", or do you want something more general even than that?Fosterling
Basically I have a function that converts a lambda into a C-style function pointer and I am using that function as follows: template <typename Lambda> void call_function(Lambda&& lambda) { call_function(convert_lambda(std::forward<Lambda>(lambda))) } The lambda here is being converted into a C-style function pointer which doesn't directly match with the type std::function so it ends up just recursively calling the template.Revolving
@tjwrona1992 You don't have to convert the lambda to a C-style function pointer just to pass it into a std::function. You can do without the overload and just pass the lambda into the function that accepts std::function.Fosterling
That doesn't work with capturing lambdas though. I need it to work with capturing lambdas as well. This convert_lambda function is able to convert lambdas with captures into C-style function pointers.Revolving
@tjwrona1992 That's not possible (sanely) without modifying the C-style function pointer to also take a void* context argumentFosterling
Accepting this answer because I think this is probably what most people who stumble upon this question will need. (Thanks for giving me the solution I needed in another post! #66038364)Revolving

© 2022 - 2024 — McMap. All rights reserved.