n. m. could be an AI
n. m. could be an AI

Reputation: 119877

Cannot deduce function return type

This doesn't work with either gcc-10 or clang-10.

template <typename R, typename T>
auto invoke_function(R (&f)(T), T t) { return std::invoke(f, t); }

invoke_function(std::to_string, 42);

This works with gcc-10, but not clang-10.

template <typename R, typename T>
auto invoke_function(T t, R (&f)(T)) { return std::invoke(f, t); }

invoke_function(42, std::to_string);

Error messages are very similar in all cases: "couldn't infer template argument 'R'" or "couldn’t deduce template parameter ‘R’" (gcc).

It isn't clear why this code is rejected. Since T is deduced, the overload of std::to_string can be determined. The dependency on argument order is particularly annoying. Shouldn't it Just Work?

I know this problem can be sidestepped by introducing a function object:

struct to_string
{
    template<typename T> std::string operator()(T t) { return std::to_string(t); }
};

and then just using std::invoke on it. This however requires creating a separate function object for each overload set.

Is there a better way?

Upvotes: 1

Views: 1601

Answers (1)

Barry
Barry

Reputation: 302862

It isn't clear why this code is rejected. Since T is deduced, the overload of std::to_string can be determined.

That's not how it works exactly. Template deduction deduces each parameter/argument pair independently first - and then we bring all the deductions together and ensure that they're consistent. So we deduce T from 42 and then, separately, we deduce R(&)(T) from std::to_string. But every overload of std::to_string matches that pattern, so we don't know which one to pick.

But the above is only true if we can deduce each pair independently. If a parameter is non-deducible, we skip it and then try to go back and fill it in later. And that's the key here - we restructure the deduction such that we only deduce T from 42:

template <typename T>
auto invoke_function(std::string (&f)(std::type_identity_t<T>), T t) { return std::invoke(f, t); }

Here, we deduce T and int and now we're deducing std::string(&)(int) from std::to_string. Which now works, because only a single overload matches that pattern.


Except now this is undefined behavior, as per [namespace.std]/6:

Let F denote a standard library function ([global.functions]), a standard library static member function, or an instantiation of a standard library function template. Unless F is designated an addressable function, the behavior of a C++ program is unspecified (possibly ill-formed) if it explicitly or implicitly attempts to form a pointer to F.

std::to_string is not an addressable function.

So the real better way is to just wrap to_string in a lambda and pass that along:

invoke_function([](auto x){ return std::to_string(x); }, 42);

And just adjusting invoke_function to take an arbitrary callable rather than specifically a function. That lambda wrapping generalizes to:

#define FWD(x) static_cast<decltype(x)&&>(x)
#define LIFT(name) [&](auto&&... args) noexcept(noexcept(name(FWD(args)...))) -> decltype(name(FWD(args)...)) { return name(FWD(args)...); }

invoke_function(LIFT(std::to_string), 42);

Upvotes: 3

Related Questions