Bad type deduction when passing overloaded function pointer and its arguments
Asked Answered
L

1

8

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?

Libyan answered 8/3, 2020 at 6:25 Comment(13)
What is the expected output? Is it (int&) (int&&)?Lockout
@L.F., Yep. Those are the outputs of Foo, so they should also be the output of Invoke.Libyan
For me, clang gives a different result: it prints (int &&) twice for the second case.Peculiar
@geza, Actually, it's the same for me too. I obviously made a mistake. I'll fix it. thank you. Clearly the Invoke is providing undefined behaviour.Libyan
It must have something to do with S argument deduction. Try to comment out the const T & version of Invoke and notice the error. Also if the argument is provided explicitly (Invoke<void>(&Foo, num)) the correct version is called.Discriminate
@aparpara, you're right! I didn't expect the return type to be causing the problem given that the return types are all the same in this example. As for removing const T&, that's just avoiding the problem without solving it.Libyan
Here's a theory for the first case: when the compiler considers the non-const Invoke, it can instantiate it with both the const and non-const Foo. And it doesn't check that the return type (S) is the same for both, so it says that it cannot deduce S. So it ignores this template. While instantiating the const Invoke can be done only with the const Foo, so it can deduce S in this case. Hence the compiler uses this template.Peculiar
I don't know what's exactly happening and, above all, who's right between g++ and clang++ but... seems to me that you're ignoring that the && reference of the argument of the Foo() function is a completely different kind of reference (a r-value reference) compared to the && for the arg of Invoke (that is a reference of a template value, so is a forwarding reference).Biyearly
@max66, I don't understand what you mean. Why do you think that Invoke doesn't take that into account?Libyan
First of all, because you use (inside the && version of Invoke()) std::move() instead of std::forward; second, because seems to me (maybe I'm wrong) that your code is based on the idea that a T && arg is ever a T && (in a template function).Biyearly
@max66, okay, I think that I see you point, although I'm not sure that I agree about std::move, my understanding was (correct or not) that any Bar(T&&) input function would be deduced as a Bar (T&) if the input was an lvalue. If you replace the main with the following code, it behaves like I expected in g++, but don't compile in clang++: template <typename T> void Bar (T && t) { Foo(std::forward<T>(t)); } int main() { int num; Bar(num); Invoke<void>(&Bar, num); }Libyan
@Libyan - Frankly, I don't know what's exactly happening (I have big troubles understanding perfect forwarding details) but it seems to me that you're right when you say " any Bar(T&&) input function would be deduced as a Bar (T&) if the input was an lvalue". So if you use std::move() over a l-value, you (potentially) make a disaster: inside the function you can depredate the function when externally is expected that remain valid.Biyearly
@max66, what I meant is because if the Bar(T&&) is deduced to Bar(int&), say, because of an int lvalue call, I thought that when you do the int x; Invoke(&Bar, x) that it would call Invoke (FunctionType<S, T&>, T&). That's what happens with g++ (using my code from my previous comment, and putting a print in the invokes), but sadly this causing a compile error for clang++.Libyan
M
2

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).

Melamine answered 9/3, 2020 at 15:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.