Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 572
Sod Almighty Avatar asked May 11 '13 00:05

Sod Almighty


People also ask

Can constructors be overloaded in C++?

Constructors can be overloaded in a similar way as function overloading. Overloaded constructors have the same name (name of the class) but the different number of arguments. Depending upon the number and type of arguments passed, the corresponding constructor is called.

How to overload 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 function signature in function overloading?

A function's signature includes the function's name and the number, order and type of its formal parameters. Two overloaded functions must not have the same signature. The return value is not part of a function's signature.

Can we have more than one constructor in a class?

There can be multiple constructors in a class. However, the parameter list of the constructors should not be same. This is known as constructor overloading.


2 Answers

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.

like image 200
Luc Danton Avatar answered Oct 03 '22 16:10

Luc Danton


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.

like image 29
Yakk - Adam Nevraumont Avatar answered Oct 03 '22 16:10

Yakk - Adam Nevraumont