Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't std::function participate in overload resolution?

I know that the following code won't compile.

void baz(int i) { }
void baz() {  }


class Bar
{
    std::function<void()> bazFn;
public:
    Bar(std::function<void()> fun = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Because std::function is said not to consider overload resolution, as I read in this other post.

I do not fully understand the technical limitations that forced this kind of solution.

I read about the phases of translation and templates on cppreference, but I can't think of any reasoning that I couldn't find a counterexample to. Explained to a half-layman (still new to C++), what and during which stage of translation makes the above fail to compile?

like image 629
mdx Avatar asked Dec 13 '19 14:12

mdx


People also ask

Why we Cannot overload a function on return type?

The return type of a function has no effect on function overloading, therefore the same function signature with different return type will not be overloaded. Example: if there are two functions: int sum() and float sum(), these two will generate a compile-time error as function overloading is not possible here.

What is overload resolution?

The process of selecting the most appropriate overloaded function or operator is called overload resolution. Suppose that f is an overloaded function name. When you call the overloaded function f() , the compiler creates a set of candidate functions.

How do you create an overloaded function in C++?

You overload a function name f by declaring more than one function with the name f in the same scope. The declarations of f must differ from each other by the types and/or the number of arguments in the argument list.

What is the point of std :: function?

Class template std::function is a general-purpose polymorphic function wrapper. Instances of std::function can store, copy, and invoke any Callable target -- functions, lambda expressions, bind expressions, or other function objects, as well as pointers to member functions and pointers to data members.


4 Answers

This doesn't really have anything to do with "phases of translation". It's purely about the constructors of std::function.

See, std::function<R(Args)> doesn't require that the given function is exactly of the type R(Args). In particular, it doesn't require that it is given a function pointer. It can take any callable type (member function pointer, some object that has an overload of operator()) so long as it is invokable as if it took Args parameters and returns something convertible to R (or if R is void, it can return anything).

To do that, the appropriate constructor of std::function must be a template: template<typename F> function(F f);. That is, it can take any function type (subject to the above restrictions).

The expression baz represents an overload set. If you use that expression to call the overload set, that's fine. If you use that expression as a parameter to a function that takes a specific function pointer, C++ can whittle down the overload set to a single call, thus making it fine.

However, once a function is a template, and you're using template argument deduction to figure out what that parameter is, C++ no longer has the ability to determine what the correct overload in the overload set is. So you must specify it directly.

like image 84
Nicol Bolas Avatar answered Oct 13 '22 18:10

Nicol Bolas


Overload resolution occurs only when (a) you are calling the name of a function/operator, or (b) are casting it to a pointer (to function or member function) with an explicit signature.

Neither is occurring here.

std::function takes any object that is compatible with its signature. It doesn't take a function pointer specifically. (a lambda is not a std function, and a std function is not a lambda)

Now in my homebrew function variants, for signature R(Args...) I also accept a R(*)(Args...) argument (an exact match) for exactly this reason. But it means that it elevates "exact match" signatures above "compatible" signatures.

The core problem is that an overload set is not a C++ object. You can name an overload set, but you cannot pass it around "natively".

Now, you can create a pseudo-overload set of a function like this:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

#define OVERLOADS_OF(...) \
  [](auto&&...args) \
  RETURNS( __VA_ARGS__(decltype(args)(args)...) )

this creates a single C++ object that can do overload resolution on a function name.

Expanding the macros, we get:

[](auto&&...args)
noexcept(noexcept( baz(decltype(args)(args)...) ) )
-> decltype( baz(decltype(args)(args)...) )
{ return baz(decltype(args)(args)...); }

which is annoying to write. A simpler, only slightly less useful, version is here:

[](auto&&...args)->decltype(auto)
{ return baz(decltype(args)(args)...); }

we have a lambda that takes any number of arguments, then perfect forwards them to baz.

Then:

class Bar {
  std::function<void()> bazFn;
public:
  Bar(std::function<void()> fun = OVERLOADS_OF(baz)) : bazFn(fun){}
};

works. We defer overload resolution into the lambda that we store in fun, instead of passing fun an overload set directly (which it cannot resolve).

There has been at least one proposal to define an operation in the C++ language that converts a function name into an overload set object. Until such a standard proposal is in the standard, the OVERLOADS_OF macro is useful.

You could go a step further, and support cast-to-compatible-function-pointer.

struct baz_overloads {
  template<class...Ts>
  auto operator()(Ts&&...ts)const
  RETURNS( baz(std::forward<Ts>(ts)...) );

  template<class R, class...Args>
  using fptr = R(*)(Args...);
  //TODO: SFINAE-friendly support
  template<class R, class...Ts>
  operator fptr<R,Ts...>() const {
    return [](Ts...ts)->R { return baz(std::forward<Ts>(ts)...); };
  }
};

but that is starting to get obtuse.

Live example.

#define OVERLOADS_T(...) \
  struct { \
    template<class...Ts> \
    auto operator()(Ts&&...ts)const \
    RETURNS( __VA_ARGS__(std::forward<Ts>(ts)...) ); \
\
    template<class R, class...Args> \
    using fptr = R(*)(Args...); \
\
    template<class R, class...Ts> \
    operator fptr<R,Ts...>() const { \
      return [](Ts...ts)->R { return __VA_ARGS__(std::forward<Ts>(ts)...); }; \
    } \
  }
like image 7
Yakk - Adam Nevraumont Avatar answered Oct 13 '22 17:10

Yakk - Adam Nevraumont


The issue here is there is nothing telling the compiler how to do the function to pointer decay. If you have

void baz(int i) { }
void baz() {  }

class Bar
{
    void (*bazFn)();
public:
    Bar(void(*fun)() = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Then code would work since now the compiler knows which function you want as there is a concrete type you are assigning to.

When you use std::function you call it's function object constructor which has the form

template< class F >
function( F f );

and since it is a template, it needs to deduce the type of the object that is passed. since baz is an overloaded function there is no single type that can be deduced so template deduction fails and you get an error. You would have to use

Bar(std::function<void()> fun = (void(*)())baz) : bazFn(fun){}

to get force a single type and allow the deduction.

like image 5
NathanOliver Avatar answered Oct 13 '22 19:10

NathanOliver


At the point the compiler is deciding which overload to pass into the std::function constructor all it knows is that the std::function constructor is templated to take any type. It doesn't have the ability to try both overloads and find that the first one doesn't compile but the second one does.

The way to resolve this is to explicitly tell the compiler which overload you want with a static_cast:

Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
like image 2
Alan Birtles Avatar answered Oct 13 '22 19:10

Alan Birtles