简体   繁体   中英

Type deduction when passing lambda into variadic std::function

I'm trying to retrieve values from a tuple of arrays using type information on the function used for processing them. However, type deduction fails for this case due (in part?) to the need to use an identity struct for the typename of the std::function . Is there a way to restore deduction here?

#include <functional>
#include <iostream>
#include <tuple>

class comp_a
{
public:
    static const size_t id = 0;

    int val = 0;
};

class comp_b
{
public:
    static const size_t id = 1;

    int val = 0;
};

class comp_c
{
public:
    static const size_t id = 2;

    int val = 0;
};

template<size_t size, typename ... Cs>
struct storage
{    
    template<typename T> struct identity { using type = T; };

    template<typename ... Ts>
    void get(size_t index, typename identity<std::function<void(Ts& ...)>>::type f)
    {
        f(std::get<Ts::id>(components)[index] ...);
    }

    std::tuple<std::array<Cs, size> ...> components;
};

int32_t main()
{
    storage<20, comp_a, comp_b, comp_c> storage;

    storage.get(2, [](comp_a& a, comp_c& c) {              // Doesn't work
    // storage.get<comp_a, comp_c>(2, [](comp_a& a, comp_c& c) { // Works
        std::cout << a.val << " " << c.val << std::endl;
    });
}

I've run across this and this , which seem similar, but I believe my situation is different because I need the variadic types in order to access their traits when retrieving the desired values. As in those examples, the variadic parameters to the function are treated as void:

error: cannot convert 'main()::<lambda(comp_a&, comp_c&)>' to 'storage<20, comp_a, comp_b, comp_c>::identity<std::function<void()> >::type' {aka 'std::function<void()>'}

Would a deduction guide be viable in this situation? The type information seems buried a little deep in the type of the std::function , and so I'm not sure how I would pull it out in a guide.

A live example is available here .

You can use the std::function deduction guide trick to pull out the arguments, but you really don't want to actually create a std::function . You want to stack in lambda-land. std::function adds overhead and allocation due to type erasure - but there is nothing you're doing that actually requires the benefits that type erasure provides. It is all loss and no win. Don't actually make a std::function .

That said, you still of course need the arguments. So you can do this:

template <typename T> struct type { };

template <typename F>
void get(size_t index, F f) {
    using function_type = decltype(std::function(f));
    get_impl(index, f, type<function_type>{});
}

Basically, we take some callable - and then we deduce a std::function from it. That gives us some type. In the specific example in OP, that type is std::function<void(comp_a&, comp_b&)> . We then simply forward that type through to a different function - as an empty object. No overhead. To repeat, we're not actually creating a std::function - we're just passing its type around.

That other function can take advantage of knowing what std::function knows:

template <typename T> using uncvref_t = std::remove_cv_t<std::remove_reference_t<T>>;

template <typename F, typename R, typename... Args>
void get_impl(size_t index, F f, type_t<std::function<R(Args...)>>) {
    f(std::get<uncvref_t<Args>::id>(components)[index] ...);
}

You need the uncvref_t there to handle the case where Args may or may not be a reference or cv -qualified.

Now, this won't work for any callable. If you pass in a generic lambda, deducing std::function will fail for it. But then... that couldn't work anyway, so it doesn't seem like a big loss?

I'm not sure what the identity struct is for but removing it gives a clearer error message (template deduction failed).

The compiler is unable to derive the std::function type from the lambda. To prove this the following does compile:

storage.get(2, std::function<void(comp_a& a, comp_c& c)>([](comp_a& a, comp_c& c) {              // Doesn't work
    std::cout << a.val << " " << c.val << std::endl;
}));

So to make it work we just need to give the compiler a helping hand deriving the types. Borrowing from http://www.cplusplus.com/forum/general/223816/ the following works:

namespace detail
{
    template < typename T > struct deduce_type;

    template < typename RETURN_TYPE, typename CLASS_TYPE, typename... ARGS >
    struct deduce_type< RETURN_TYPE(CLASS_TYPE::*)(ARGS...) const >
    {
        using type = std::function< RETURN_TYPE(ARGS...) >;
    };
}

template<size_t size, typename ... Cs>
struct storage
{
    template<typename ... Ts>
    void get(size_t index, typename std::function<void(Ts& ...)> f)
    {
        f(std::get<Ts::id>(components)[index] ...);
    }

    template<typename Lambda>
    void get(size_t index, Lambda l)
    {
        get( index, typename detail::deduce_type< decltype( &Lambda::operator() ) >::type( l ) );
    }

    std::tuple<std::array<Cs, size> ...> components;
};

You tagged C++17, so you can use the std::function 's deduction guides

So, as suggested by Alan Birtles, you can receive the lambda as a simple type, convert it to a std::function (deduction guides) and deduce the type of the arguments.

Something as

template<size_t size, typename ... Cs>
struct storage
{    
    template<typename ... Ts>
    void get(size_t index, std::function<void(Ts& ...)> f)
    { f(std::get<Ts::id>(components)[index] ...); }

    template <typename F>
    void get(size_t index, F f)
    { get(index, std::function{f}); }

    std::tuple<std::array<Cs, size> ...> components;
};

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