4
votes

I have a template function that should take a function pointer and arguments, then invoke the function pointer with the given arguments (let's call it Invoke). However, when I call the template function with an overloaded function as an argument, template deduction fails.

I have used enable_if so that only one overload would be valid, but this didn't help.

#include <string>
#include <type_traits>

void foo(int, int){}
void foo(std::string, std::string) {}

template <bool Val1, bool Val2, bool ...Rest>
struct And
{
    enum {value = And<Val1 && Val2, Rest...>::value};
};
template <bool Val1, bool Val2>
struct And<Val1, Val2>
{
    enum {value = Val1 && Val2};
};

template <typename ...Params, typename ...Args, typename = typename std::enable_if<
    And<std::is_convertible<Args, Params>::value...>::value
>::type>
void Invoke(void (*fn)(Params...), Args ...args){}

int main() {
    Invoke(&foo, "a", "b");
    return 0;
}

Try on ideone.

The compiler complains that mismatched argument pack lengths while expanding ‘std::is_convertible<Args, Params>::value’, when both overloads are present. When I comment out the int overload, the program compiles just fine, and when I comment out the std::string overload, deduction fails as it should, since const char[] is not implicitly convertible to int.

The standard (section 17.8.2.1.6.2 of the C++17 standard) says the following:

If the argument is an overload set (not containing function templates), trial argument deduction is attempted using each of the members of the set. If deduction succeeds for only one of the overload set members, that member is used as the argument value for the deduction. If deduction succeeds for more than one member of the overload set the parameter is treated as a non-deduced context.

So I would expect that the compiler would try the int overload, where deduction would fail. When it tries the std::string overload, the deduction would succeed.

Since the deduction would succeed for only one of the overload set members, I would expect that it would then proceed as if the int overload didn't exist, and the compilation would succeed like it does when the other overload is commented out, but it fails.

Where am I wrong?

References to standard will be appreciated.

2
I'm not terribly familiar with vardic templates, but typename ...Params, typename ...Args, seems off. How do you expect the compiler to differentiate between the two? But again, not too familiar with vardic templates, so maybe I'm just missing something.user10957435
Two parameter packs can be used if they are in deduced contexts. See for example stackoverflow.com/questions/9831501/…Tomeamis
Why not just define template <bool... Args> struct And :std::bool_constant<(Args && ...)> {};?L. F.
@L.F. Because I don't have C++17. Also, if I did, I would probably just not define struct And at all, and just use the fold expression directlyTomeamis

2 Answers

5
votes

The problem here is that there is more than one function that works. The deduction of Params... is going to happen before you even get to the SFINAE part of the template. When it tries to deduce Params.. from void (*fn)(Params...) it matches void foo(int, int) and void foo(std::string, std::string). Since it finds multiple matches, 17.8.2.1.6.2 states that it is treated as a non deduced context.

Since it cant deduce a type, you get hard stop error. SFINAE only happens after the template parameter deduction step, which it can't get to in this case.

5
votes

The &foo is not a function pointer, but an overload set. You have to be explicit:

Invoke(static_cast<void(*)(std::string, std::string)>(&foo), "a", "b");

To simplify the failing enable_if, you could take an unspecified function pointer type, with a variadic arugment pack, and check is_invocable: https://en.cppreference.com/w/cpp/types/is_invocable