7

Is there any way to recover type information from a lambda with default parameters stored in a std::function that does not have those parameters in its type?

std::function<void()> f1 = [](int i = 0){};
std::function<void(int)> f2 = [](int i = 0){};
std::function<void(int)> f3 = f1;  // error
std::function<void()> f4 = f2;     // error

Looking at std::function's copy constructor, there is no partial template specialization for other function types, so I'd imagine this information is lost and it is just a case that you can't assign a function of one type to a function of another type, even if internally they can both call the function. Is this correct? Are there any work-arounds to achieve this? I'm looking at std::function::target, but haven't had any luck, I'm no expert on function types and pointers.

On a side note, how does f1(or the lambda) bind the default parameter?

A-n-t-h-o-n-y
  • 410
  • 6
  • 14
  • How do you plan to invoke f1 or f2 (With default value)? – nakiya May 25 '17 at 07:07
  • 1
    @nakiya It is for a signals and slots implementation that requires a copy of the slot(function) when making a connection, the signal calls the slot with parameters passed from the call site of the signal. It would be nice to allow slots with default parameters to connect to any signal with a signature that is able to call it. – A-n-t-h-o-n-y May 25 '17 at 07:23

2 Answers2

5

No, that is not possible, because default arguments are a property of a set of a function's declarations, not of the function itself. In other words, this is perfectly legal C++:

A.cpp

int f(int i = 42);

const int j = f(); // will call f(42)

B.cpp

int f(int i = 314);

const int k = f(); // will call f(314)

F.cpp

int f(int i = 0)
{
  return i;
}

const int x = f(); // will call f(0)

These can all be linked together just fine.

Which means it's not possible to somehow "retrieve" a default argument from a function.

You can do the equivalent of f4 = f2 using std::bind and providing your own default argument, like this:

std::function<void()> f4 = std::bind(f2, 42);

[Live example]

However, there is no way to get something equivalent to f3 = f1.

Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
1
template<class...Sigs>
strucct functions:std::function<Sigs>...{
  using std::function<Sigs>::operator()...;
  template<class T,
    std::enable_if<!std::is_same<std::decay_t<T>,fundtions>{}>,int> =0
  >
  functions(T&&t):
    std::function<Sigs>(t)...
  {}
};

the above is a C++17 sketch of a crude object that cam store more than one operator().

A more efficient one would only store the object once, but store how to call it many ways. And I skipped many details.

It isn't really a std::function, but a compatible type; std function only stores one way to call the object.

Here is a "function view" that takes any number of signatures. It does not own the to-be-called object.

template<class Sig>
struct pinvoke_t;
template<class R, class...Args>
struct pinvoke_t<R(Args...)> {
    R(*pf)(void*, Args&&...) = 0;
    R invoke(void* p, Args...args)const{
        return pf(p, std::forward<Args>(args)...);
    }
    template<class F, std::enable_if_t<!std::is_same<pinvoke_t, std::decay_t<F>>{}, int> =0>
    pinvoke_t(F& f):
        pf(+[](void* pf, Args&&...args)->R{
            return (*static_cast<F*>(pf))(std::forward<Args>(args)...);
        })
    {}
    pinvoke_t(pinvoke_t const&)=default;
    pinvoke_t& operator=(pinvoke_t const&)=default;
    pinvoke_t()=default;
};

template<class...Sigs>
struct invoke_view:pinvoke_t<Sigs>...
{
    void* pv = 0;
    explicit operator bool()const{ return pv; }
    using pinvoke_t<Sigs>::invoke...;
    template<class F, std::enable_if_t<!std::is_same<invoke_view, std::decay_t<F>>{}, int> =0>
    invoke_view(F&& f):
        pinvoke_t<Sigs>(f)...
    {}
    invoke_view()=default;
    invoke_view(invoke_view const&)=default;
    invoke_view& operator=(invoke_view const&)=default;
    template<class...Args>
    decltype(auto) operator()(Args&&...args)const{
        return invoke( pv, std::forward<Args>(args)... );
    }
};

Live example.

I use C++17 using ... because the binary tree implementation in C++14 is ugly.

For your use case, it would looke like:

auto func_object = [](int i = 0){};
invoke_view<void(), void(int)> f1 = func_object;
std::function<void(int)> f3 = f1;  // works
std::function<void()> f4 = f1;     // works

note that the lack of lifetime management in invoke_view means that the above only works when func_object continues to exist. (If we make an invoke view to an invoke view, the "inner" invoke view is stored by pointer as well, so must continue to exist; not the case if we store the invoke view in a std function).

Lifetime management of the target, done right, takes a bit of work. You'd want to use a small buffer optimization with an optional smart pointer or something to get reasonable performance with small lambdas and avoid the overhead of the heap allocation.

A simple naive always heap allocating solution would replace the void* with a unique_ptr<void, void(*)(void*)> and store { new T(t), [](void* ptr){static_cast<T*>(ptr)->~T();} } in it (or similar).

That solution makes the function object move-only; making it copyable requires also type erasing a clone operation.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524