简体   繁体   中英

How to overload constructors on signature of a std::function?

I'm trying to write a class with overloaded constructors that accept std::function objects as parameters, but of course every damn thing can be implicitly cast to a std::function of any signature. Which is really helpful, naturally.

Example:

class Foo {
  Foo( std::function<void()> fn ) {
    ...code...
  }
  Foo( std::function<int()> fn ) {
    ...code...
  }
};

Foo a( []() -> void { return; } );    // Calls first constructor
Foo b( []() -> int { return 1; } );   // Calls second constructor

This won't compile, complaining that both constructors are essentially identical and ambiguous. Which is nonsense, of course. I've tried enable_if, is_same and a bunch of other things. Accepting function pointers is out of the question, because that would prevent the passing of stateful lambdas. Surely there must be a way to achieve this?

My templating skills are a little lacking, I'm afraid. Normal template classes and functions are easy enough, but playing silly buggers with the compiler is a little out of my league. Can someone help me out, please?

I know variants of this question have been asked before, but they generally focus on normal functions rather than constructors; or overloading by arguments instead of return types.

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); 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; }; 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 eg 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 eg overloaded functors.

So there are many ways to approach this, which take various amounts of work. None of them are completely trivial.

First, you can unpack signatures of passed in types by examining T::operator() and/or checking if it is of type R (*)(Args...) .

Then check for signature equivalence.

A second approach is to detect call compatibility. Write a traits class like this:

template<typename Signature, typename F>
struct call_compatible;

which is either std::true_type or std::false_type depending on if decltype<F&>()( declval<Args>()... ) is convertible to the Signature return value. In this case, this would solve your problem.

Now, more work needs to be done if the two signatures you are overloading on are compatible. Ie, imagine if you have a std::function<void(double)> and std::function<void(int)> -- they are cross call-compatible.

To determine which is the "best" one, you can look over here at a previous question of mine, where we can take a bunch of signatures and find which matches best. Then do a return type check. This is getting complex!

If we use the call_compatible solution, what you end up doing is this:

template<size_t>
struct unique_type { enum class type {}; };
template<bool b, size_t n=0>
using EnableIf = typename std::enable_if<b, typename unique_type<n>::type>::type;

class Foo {
  template<typename Lambda, EnableIf<
    call_compatible<void(), Lambda>::value
    , 0
  >...>
  Foo( Lambda&& f ) {
    std::function<void()> fn = f;
    // code
  }
  template<typename Lambda, EnableIf<
    call_compatible<int(), Lambda>::value
    , 1
  >...>
  Foo( Lambda&& f ) {
    std::function<int()> fn = f;
    // code
  }
};

which is the general pattern for the other solutions.

Here is a first stab at call_compatible :

template<typename Sig, typename F, typename=void>
struct call_compatible:std::false_type {};

template<typename R, typename...Args, typename F>
struct call_compatible<R(Args...), F, typename std::enable_if<
  std::is_convertible<
    decltype(
      std::declval<F&>()( std::declval<Args>()... )
    )
    , R
  >::value
>::type>:std::true_type {};

that is untested/uncompiled as yet.

So I have a completely new solution to this problem that works in MSVC 2013, and doesn't suck (like looking at a pointer to operator() ).

Tag dispatch it.

A tag that can carry any type:

template<class T> struct tag{using type=T;};

A function that takes a type expression, and generates a type tag:

template<class CallExpr>
tag<typename std::result_of<CallExpr>::type>
result_of_f( tag<CallExpr>={} ) { return {}; }

and use:

class Foo {
private:
  Foo( std::function<void()> fn, tag<void> ) {
    ...code...
  }
  Foo( std::function<int()> fn, tag<int> ) {
    ...code...
  }
public:
  template<class F>
  Foo( F&& f ):Foo( std::forward<F>(f), result_of_f<F&()>() ) {}
};

and now Foo(something) forwards to either the std::function<int()> or std::function<void()> constructor depending on the context.

We can make tag<> smarter by supporting conversion if you like by adding a ctor to it. Then a function that returns double will dispatch to tag<int> :

template<class T> struct tag{
  using type=T;
  tag(tag const&)=default;
  tag()=default;
  template<class U,
    class=typename std::enable_if<std::is_convertible<U,T>::value>::type
  >
  tag( tag<U> ){}
};

note that this does not support SFINAE-like Foo -construction failure. Ie, if you pass int to it, it will get a hard failure, not a soft one.

While this does not directly work in VS2012, you could forward to an initialization function in the body of the constructor.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM