18

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?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
mdx
  • 534
  • 3
  • 16

4 Answers4

14

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Pardon me if I got something wrong, but how come C++ has no ability to distinguish between available overloads? I see now that std::function accepts any compatible type, not only an exact match, but only one of those two baz() overloads is invokable **as if** it took specified parameters. Why is it impossible to disambiguate? – mdx Dec 13 '19 at 15:04
  • Because all C++ sees is the signature I quoted. It doesn't know what it is supposed to match against. Essentially, any time you use an overload set against anything that it isn't obvious what the right answer is *explicitly from the C++ code* (the code being that template declaration), the language forces you to spell out what you mean. – Nicol Bolas Dec 13 '19 at 15:10
  • So the template parameter declares a family of compatible types and because of it being a "family" and not a single type I have to explicitly state what I'm matching against? Even though the overload set I specified contains only 1 proper match? Did I get it right? – mdx Dec 13 '19 at 15:21
  • 1
    @TuRtoise: The template parameter on the `function` class template is *irrelevant*. What matters is the template parameter on the *constructor* you are calling. Which is just `typename F`: aka, any type. – Nicol Bolas Dec 13 '19 at 15:27
  • And the reason it's just F is because an overload set doesn't represent any specific type? – mdx Dec 13 '19 at 15:32
  • 1
    @TuRtoise: I think you're misunderstanding something. It's "just F" because that's how templates work. From the signature, that function constructor takes any type, so any attempt to call it with template argument deduction will deduce the type from the parameter. Any attempt to apply deduction to an overload set is a compile error. The compiler doesn't try to deduce every possible type from the set to see which ones work out. – Nicol Bolas Dec 13 '19 at 15:35
  • 1
    "Any attempt to apply deduction to an overload set is a compile error. The compiler doesn't try to deduce every possible type from the set to see which ones work out." Exactly what I was missing, thank you :) – mdx Dec 13 '19 at 15:38
7

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)...); }; \
    } \
  }
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
5

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.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • "since baz is an overloaded function there is no single type that can be deduced " but since C++14 "this constructor does not participate in overload resolution unless f is Callable for argument types Args... and return type R" I am not sure, but I was close to expecting that this would be enough to resolve the ambiguity – 463035818_is_not_an_ai Dec 13 '19 at 14:48
  • 3
    @formerlyknownas_463035818 But to determine that, it needs to deduce a type first, and it can't since it is an overloaded name. – NathanOliver Dec 13 '19 at 14:49
2

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){}
Alan Birtles
  • 32,622
  • 4
  • 31
  • 60