Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to figure out the parameter type and return type of a polymorphic C++ 14 lambda?

Starting from this question (Is it possible to figure out the parameter type and return type of a lambda?) I used the proposed function_traits a lot. However, with C++14 polymorphic lambdas have arrived and they gave me a hard time.

template <typename T>
struct function_traits
    : public function_traits<decltype(&T::operator())>
{};
// For generic types, directly use the result of the signature of its 'operator()'

template <typename ClassType, typename ReturnType, typename... Args>
struct function_traits<ReturnType(ClassType::*)(Args...) const>
// we specialize for pointers to member function
{
    enum { arity = sizeof...(Args) };
    // arity is the number of arguments.

    typedef ReturnType result_type;

    template <size_t i>
    struct arg
    {
        typedef typename std::tuple_element<i, std::tuple<Args...>>::type type;
        // the i-th argument is equivalent to the i-th tuple element of a tuple
        // composed of those arguments.
    };
};

The operator() as proposed in the other question's answer is now overloaded as stated in the standard to support:

auto lambda1 = [](auto& a) { a.foo(); } 

and

auto lambda2 = [](auto&&... args) { foo(args...); };

This overloading now rips the function_traits class apart because the compiler cannot resolve the correct version of operator().

lambda.cpp:98:38: error: reference to overloaded function could not be
      resolved; did you mean to call it?
      typedef function_traits<decltype(&T::operator())> caller;

Is it somehow possible to achieve the functionality of function_traits on polymorphic lambdas with C++14?

like image 311
Michael Haidl Avatar asked Jan 23 '15 07:01

Michael Haidl


People also ask

What is the type of a lambda?

A type lambda lets one express a higher-kinded type directly, without a type definition. For instance, the type above defines a binary type constructor, which maps arguments X and Y to Map[Y, X] . Type parameters of type lambdas can have bounds, but they cannot carry + or - variance annotations.

What does Decltype auto do?

Use auto and decltype to declare a function template whose return type depends on the types of its template arguments. Or, use auto and decltype to declare a function template that wraps a call to another function, and then returns the return type of the wrapped function.

What is generic lambda in C++?

Generic lambdas were introduced in C++14 . Simply, the closure type defined by the lambda expression will have a templated call operator rather than the regular, non-template call operator of C++11 's lambdas (of course, when auto appears at least once in the parameter list).

When did c++ get lambdas?

CPP. Note: Lambda expressions are available after C++ 11.


2 Answers

This is currently impossible to do in the general sense, but if your lambdas have auto for all of their parameters and will work with all of them being replaced with some known type, you can modify the answer to this question: C++ metafunction to determine whether a type is callable

Along the lines of

static const bool OneArg = (sizeof( test<T, int>(0)  ) == 1);
static const bool TwoArg = (sizeof( test<T, int, int>(0)  ) == 1);
static const bool ThreeArg = (sizeof( test<T, int, int, int>(0)  ) == 1);
static constexpr std::size_t TemplatedOperatorArgSize = 
    OneArg
        ? 1
        : TwoArg
            ? 2
            : ThreeArg
                ? 3
                : -1;

Apologies for the nested ternaries. And then use something like this to call it:

template<size_t N, typename T>
struct Apply {
    template<typename F, typename... A>
    static inline decltype(auto) apply(F && f, A &&... a) {
        return Apply<N-1, T>::apply(::std::forward<F>(f), std::declval<T>(), ::std::forward<A>(a)...);
    }
};

template<typename T>
struct Apply<0, T> {
    template<typename F, typename... A>
    static inline decltype(auto) apply(F && f, A &&... a) {
        return invoke(std::forward<F>(f), std::forward<A>(a)...);
    }
};

template<std::size_t Size, typename T, typename F>
inline decltype(auto) apply(F && f) {
    return Apply<Size, T>::apply(::std::forward<F>(f));
}

Using invoke from: http://en.cppreference.com/w/cpp/utility/functional/invoke

Get the return type

using return_type = decltype(
    apply<has_callable_operator<T>::TemplatedOperatorArgSize, int>(function)
);

The first part could be rewritten to deal with an arbitrary number of arguments using std::make_index_sequence

But generally, I'd say you're going down a rabbit hole. Good luck.

Note: Didn't test the apply code, but it should be good enough to get you going.

Edit: Also, the return type needs to be independent of the parameter types to achieve what you want

like image 137
xcvr Avatar answered Sep 27 '22 16:09

xcvr


I had a similar case when implementing a Cache with a Fetch function. Something like:

template<class KEY, class VALUE, class FetchFunctor>
class Cache { ... };

Phase 1

I wanted to save the user the need to state the type of his/her FetchFunctor and without the C++17 class template argument deduction I went for a createCahce helper method like this:

// [1]
template<class KEY, class VALUE, class FetchFunctor>
auto createCache(FetchFunctor fetchFunctor) {
    return Cache<KEY, VALUE, FetchFunctor>(fetchFunctor);
}

Thus creating the cache is quite easy, e.g.:

auto cache = createCache<int, int>([](int i){return i+3;});

Phase 2

It would be much nicer to allow the user creating a cache without the need to provide the Key and Value types of the cache and to deduce them both from the provided FetchFunctor. So I added an additional createCache method:

// [2]
template<class FetchFunctor>
auto createCache(FetchFunctor fetchFunctor) {
    // function_traits is a namespace where I 'hide' the
    // traits structs for result type and arguments type deduction
    using f = function_traits::traits<decltype(fetchFunctor)>;
    using KEY = typename f::template arg<0>::type;
    using VALUE = typename f::result_type;
    return Cache<KEY, VALUE, FetchFunctor>(fetchFunctor);
}

Now creating the cache is even easier:

auto cache = createCache([](int i){return i+3;});

But, can both 'createCache' methods above live together?

Luckily yes.

  1. If the user provides the template arguments Key, Value then the first one is the only match.
  2. If the user doesn't provide the template arguments, the compiler considers the second one as 'more specialized' and will prefer it.

This way we can support all the below cases:

    // [a]
    auto cache = createCache([](int i){return i+3;});
    // compiler deduces Key, Value to be: int, int - using the 2nd createCache

    // [b]
    auto cache = createCache<int, int>([](auto i){return i+3;});
    // we have a generic lambda so we provide Key and Value - using the 1st createCache

    // [c]
    auto cache = createCache<string, string>(
        [](const char* s){return string(s) + '!';}
    );
    // we want different Key and/or Value than would be deduced - we use 1st createCache

Still...

The following will not compile:

    auto cache = createCache([](auto i){return i+3;});
    // we cannot deduce types for generic lambda
    // compiler goes to the 2nd createCache but Key and Value cannot be deduced
    // - compilation error

But this shouldn't bother us... you can provide Key and Value as in [b] above.


Code:

http://coliru.stacked-crooked.com/a/e19151a5c245d7c3

like image 23
Amir Kirsh Avatar answered Sep 27 '22 17:09

Amir Kirsh