Here are some common scenarios, and why I don't think std::function
is appropriate for them:
struct event_queue {
using event = std::function<void()>;
std::vector<event> events;
void add(event e)
{ events.emplace_back(std::move(e)); }
};
In this straightforward situation functors of a particular signature are stored. In that light my recommendation seems pretty bad, doesn't it? What could go wrong? Things like queue.add([foo, &bar] { foo.(bar, baz); })
work well, and type-erasure is precisely the feature you want, since presumably functors of heterogeneous types will be stored, so its costs are not a problem. That is in fact one situation where arguably using std::function<void()>
in the signature of add
is acceptable. But read on!
At some point in the future you realize some events could use some information when they're called back -- so you attempt:
struct event_queue {
using event = std::function<void()>;
// context_type is the useful information we can optionally
// provide to events
using rich_event = std::function<void(context_type)>;
std::vector<event> events;
std::vector<rich_event> rich_events;
void add(event e) { events.emplace_back(std::move(e)); }
void add(rich_event e) { rich_events.emplace_back(std::move(e)); }
};
The problem with that is that something as simple as queue.add([] {})
is only guaranteed to work for C++14 -- in C++11 a compiler is allowed to reject the code. (Recent enough libstdc++ and libc++ are two implementations that already follow C++14 in that respect.) Things like event_queue::event e = [] {}; queue.add(e);
still work though! So maybe it's fine to use as long as you're coding against C++14.
However, even with C++14 this feature of std::function<Sig>
might not always do what you want. Consider the following, which is invalid right now and will be in C++14 as well:
void f(std::function<int()>);
void f(std::function<void()>);
// Boom
f([] { return 4; });
For good reason, too: std::function<void()> f = [] { return 4; };
is not an error and works fine. The return value is ignored and forgotten.
Sometimes std::function
is used in tandem with template deduction as seen in this question and that one. This tends to add a further layer of pain and hardships.
Simply put, std::function<Sig>
is not specially-handled in the Standard library. It remains a user-defined type (in the sense that it's unlike e.g. int
) which follows normal overload resolution, conversion and template deduction rules. Those rules are really complicated and interact with one another -- it's not a service to the user of an interface that they have to keep these in mind to pass a callable object to it. std::function<Sig>
has that tragic allure where it looks like it helps make an interface concise and more readable, but that really only holds true as long as you don't overload such an interface.
I personally have a slew of traits that can check whether a type is callable according to a signature or not. Combined with expressive EnableIf
or Requires
clauses I can still maintain an acceptably readable interface. In turn, combined with some ranked overloads I can presumably achieve the logic of 'call this overload if functor yields something convertible to int
when called with no arguments, or fallback to this overload otherwise'. This could look like:
class Foo {
public:
// assuming is_callable<F, int()> is a subset of
// is_callable<F, void()>
template<typename Functor,
Requires<is_callable<Functor, void()>>...>
Foo(Functor f)
: Foo(std::move(f), select_overload {})
{}
private:
// assuming choice<0> is preferred over choice<1> by
// overload resolution
template<typename Functor,
EnableIf<is_callable<Functor, int()>>...>
Foo(Functor f, choice<0>);
template<typename Functor,
EnableIf<is_callable<Functor, void()>>...>
Foo(Functor f, choice<1>);
};
Note that traits in the spirit of is_callable
check for a given signatures -- that is, they check against some given arguments and some expected return type. They do not perform introspection, so they behave well in the face of e.g. overloaded functors.