Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bad type deduction when passing overloaded function pointer and its arguments

I'm trying to provide a wrapper around std::invoke to do the work of deducing the function type even when the function is overloaded.
(I asked a related question yesterday for the variadic and method pointer version).

When the function has one argument this code (C++17) works as expected under normal overload conditions:

#include <functional>

template <typename ReturnType, typename ... Args>
using FunctionType = ReturnType (*)(Args...);

template <typename S, typename T>
auto Invoke (FunctionType<S, T> func, T arg)
{   
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, T&> func, T & arg)
{   
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, const T&> func, const T & arg)
{
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, T&&> func, T && arg)
{   
    return std::invoke(func, std::move(arg));
}

Reducing the code bloat is obviously needed for more input arguments, but that's a separate problem.

If the user has overloads differing only by const/references, like so:

#include <iostream>

void Foo (int &)
{
    std::cout << "(int &)" << std::endl;
}

void Foo (const int &)
{
    std::cout << "(const int &)" << std::endl;
}

void Foo (int &&)
{
    std::cout << "(int &&)" << std::endl;
}

int main()
{
    int num;
    Foo(num);
    Invoke(&Foo, num);

    std::cout << std::endl;

    Foo(0);
    Invoke(&Foo, 0);
}

Then Invoke deduces the function incorrectly, with g++ output:

(int &)
(const int &)

(int &&)
(const int &)

And clang++:

(int &)
(const int &)

(int &&)
(int &&)

(Thanks to geza for pointing out that clang's outputs were different).

So Invoke has undefined behaviour.

I suspect that metaprogramming would be the way to approach this problem. Regardless, is it possible to handle the type deduction correctly at the Invoke site?

like image 986
Elliott Avatar asked Mar 08 '20 06:03

Elliott


1 Answers

Theory

For each function template Invoke, the template argument deduction (that must succeed for overload resolution to consider it) considers each Foo to see whether it can deduce however many template parameters (here, two) for the one function parameter (func) involved. The overall deduction can succeed only if exactly one Foo matches (because otherwise there is no way to deduce S). (This was more or less stated in the comments.)

The first (“by value”) Invoke never survives: it can deduce from any of the Foos. Similarly, the second (“non-const reference”) overload accepts the first two Foos. Note that these apply regardless of the other argument to Invoke (for arg)!

The third (const T&) overload selects the corresponding Foo overload and deduces T=int; the last does the same thing with the last overload (where T&& is a normal rvalue reference), and therefore rejects lvalue arguments despite its universal reference (which deduces T as int& (or const int&) in that case and conflicts with func’s deduction).

Compilers

If the argument for arg is an rvalue (and, as usual, isn’t const), both plausible Invoke overloads succeed at deduction, and the T&& overload should win (because it binds an rvalue reference to an rvalue).

For the case from the comments:

template <typename U>
void Bar (U &&);
int main() {
  int num;
  Invoke<void>(&Bar, num);
}

No deduction takes place from &Bar since a function template is involved, so T is successfully deduced (as int) in every case. Then, deduction happens again for each case to identify the Bar specialization (if any) to use, deducing U as fail, int&, const int&, and int& respectively. The int& cases are identical and plainly better, so the call is ambiguous.

So Clang is right here. (But there’s no “undefined behavior” here.)

Solution

I don’t have a general answer for you; since certain parameter types can accept multiple value-category/const-qualification pairs, it’s not going to be easy to emulate overload resolution correctly in all such cases. There have been proposals to reify overload sets in one way or another; you might consider one of the current techniques along those lines (like a generic lambda per target function name).

like image 196
Davis Herring Avatar answered Nov 20 '22 03:11

Davis Herring