12

I need a method of figuring out a function's argument types, and so I wrote a closure_traits class, given below, as inspired by Is it possible to figure out the parameter type and return type of a lambda?.

However, when I try to apply it to a simple lambda, I get the error that 'operator()' is not a member of '(lambda type)'. However, according to cppreference, lambda's do have an operator(). I also tried using std::function, and got the equivalent error. I guess I'm not sure what's going wrong, and any help would be greatly appreciated.

#include<type_traits> 
#include<tuple>                                                                           
#include<utility> 
#include<iostream>                                                                                                     

/* For generic types use the type signature of their operator() */                                                     
template <typename T>                                                                                      
struct closure_traits : public
                        closure_traits<decltype(&T::operator())> {};                                        

/* Otherwise, we do a template match on a function type. */
template <typename ClassType, typename ReturnType, 
          typename... ArgTypes>                                                  
struct closure_traits<ReturnType (ClassType::*) (ArgTypes... args)>                                                  
{
    using arity = std::integral_constant<std::size_t,
                                         sizeof...(ArgTypes)>;                                              
    using Ret = ReturnType;                                  

    /* The argument types will be the same as the types of the 
     * elements of a tuple composed of them. 
     */                                                                                             
    template <std::size_t I>       
    struct Args {        
        using type = typename std::tuple_element<I, 
                                       std::tuple<ArgTypes...>>::type;                                                                                                                     
    };                                                                                                                                                                                                                                        

};                                                                                                                                                                                                                                                

int main() {                                                                                                                     
    auto thing = [=] (int x) {return x;}; 

    std::cerr << "The number of arguments is " 
              << closure_traits<decltype(thing)>::arity << std::endl;                                                                                                                     

    return 0;                                                                                                                       
}    

The compiler error messages that I get are below. My compile command is simply g++ -std=c++14 main.cpp.

main.cpp: In instantiation of ‘struct closure_traits<int (main()::<lambda(int)>::*)(int) const>’:
main.cpp:9:8:   required from ‘struct closure_traits<main()::<lambda(int)> >’
main.cpp:34:82:   required from here
main.cpp:9:56: error: ‘operator()’ is not a member of ‘int (main()::<lambda(int)>::*)(int) const’
 struct closure_traits : public closure_traits<decltype(&T::operator())> {};                                        
                                                    ^
main.cpp: In function ‘int main()’:
main.cpp:34:51: error: ‘arity’ is not a member of ‘closure_traits<main()::<lambda(int)> >’
 std::cerr << "The number of arguments is " << closure_traits<decltype(thing)>::arity << std::endl; 
Community
  • 1
  • 1
sangrey
  • 488
  • 3
  • 13
  • You do know why this is a bad idea in C++14, due to `auto` arguments on templates, right? Asking "how many arguments of these types" is often a better idea. – Yakk - Adam Nevraumont Nov 04 '16 at 18:06
  • I'm not sure I understand your question. I'm trying to deduce the number of arguments of the function and their types. Why would that be affected by auto? – sangrey Nov 04 '16 at 18:10
  • 1
    Your design fundamentally cannot support `[](auto x){std::cout << x << "\n";}`. Callables in C++ do not have a fixed number of arguments nor do their arguments have fixed types. It is like trying to determine the base of an `int` in C++: the concept does not really apply, as `int` doesn't *have* a base. In your case, for *some* callables you can have fixed arity and argument types, which confuses the issue. – Yakk - Adam Nevraumont Nov 04 '16 at 18:18

4 Answers4

12

Your specialization does not match the decltype(&T::operator()) argument.

Because of this, instead of choosing the specialization (as you wanted it to), the compiler is forced to recursively choose the same main template. Which makes it to apply &T::operator() expression again, after it was already applied once. I.e the initial attempt to do &T::operator() actually succeeds, but then the compiler attempts to apply &T::operator() again, when T is already int (main()::<lambda(int)>::*)(int) const. The latter, obviously, does not have operator (), which is why you are getting this error message.

The reason it cannot choose your specialization is missing const in template parameter declaration. The lambda's operator () is actually a const member of the lambda class. Add const to the declaration of your specialization

template <typename ClassType, typename ReturnType, 
          typename... ArgTypes>                                                  
struct closure_traits<ReturnType (ClassType::*) (ArgTypes... args) const>   
...

an the compiler will follow the specialization path you intended it to follow.

And, of course, you have to print closure_traits<decltype(thing)>::arity::value, not just closure_traits<decltype(thing)>::arity.

AnT stands with Russia
  • 312,472
  • 42
  • 525
  • 765
4

The compiler correctly gets the operator() of the lambda type, but a pointer to that member function does not match your specialization because of the const qualifier.

You should add a second specialization

template <typename ClassType, typename ReturnType, 
          typename... ArgTypes>                                                  
struct closure_traits<ReturnType (ClassType::*) (ArgTypes... args) const>                                                  
{
    // ...
};

(Yes, writing templates to take a function type is painful.)

aschepler
  • 70,891
  • 9
  • 107
  • 161
  • 1
    Writing templates that take a function is not painful: just use e.g. typename Callable and don't worry about all the rest. Same with typename Container. – rubenvb Nov 03 '16 at 21:51
3

In case anyone has trouble with this the correct code is as follows :

#include<type_traits> 
#include<tuple>                                                                           
#include<utility> 
#include<iostream>                                                                                                     

/* For generic types use the type signature of their operator() */                                                     
template <typename T>                                                                                      
struct closure_traits : public
                        closure_traits<decltype(&T::operator())> {};                                        

/* Otherwise, we do a template match on a function type. */
template <typename ClassType, typename ReturnType, 
          typename... ArgTypes>                                                  
struct closure_traits<ReturnType (ClassType::*) (ArgTypes... args) const>                                                  
{
    using arity = std::integral_constant<std::size_t,
                                         sizeof...(ArgTypes)>;                                              
    using Ret = ReturnType;                                  

    /* The argument types will be the same as the types of the 
     * elements of a tuple composed of them. 
     */                                                                                             
    template <std::size_t I>       
    struct Args {        
        using type = typename std::tuple_element<I, 
                                       std::tuple<ArgTypes...>>::type;                                                                                                                     
    };                                                                                                                                                                                                                                        

};                                                                                                                                                                                                                                                

int main() {                                                                                                                     
    auto thing = [=] (int x) {return x;}; 

    std::cerr << "The number of arguments is " 
              << closure_traits<decltype(thing)>::arity::value << std::endl;                                                                                                                     

    return 0;                                                                                                                       
}    
the4thamigo_uk
  • 835
  • 4
  • 8
1

EDIT: It has been (correctly) pointed out in the comments that this does not deal with except specifiers and certain cv-qualification cases. You might need to add those cases if you need them, or look at the link posted in the comments.


If you're looking for a (more) complete solution for all types in c++ that can be invoked, a lot of these answers work but miss some of the corner cases, like

  • A reference to a lambda
  • Functions and function pointers

Here's a complete solution to my knowledge (let me know in the comments if anything is missing):

template <typename>
struct closure_traits;

template <typename FunctionT> // overloaded operator () (e.g. std::function)
struct closure_traits
    : closure_traits<decltype(&std::remove_reference_t<FunctionT>::operator())>
{
};

template <typename ReturnTypeT, typename... Args> // Free functions
struct closure_traits<ReturnTypeT(Args...)>
{
    using arguments = std::tuple<Args...>;

    static constexpr std::size_t arity = std::tuple_size<arguments>::value;

    template <std::size_t N>
    using argument_type = typename std::tuple_element<N, arguments>::type;

    using return_type = ReturnTypeT;
};

template <typename ReturnTypeT, typename... Args> // Function pointers
struct closure_traits<ReturnTypeT (*)(Args...)>
    : closure_traits<ReturnTypeT(Args...)>
{
};

// member functions
template <typename ReturnTypeT, typename ClassTypeT, typename... Args>
struct closure_traits<ReturnTypeT (ClassTypeT::*)(Args...)>
    : closure_traits<ReturnTypeT(Args...)>
{
    using class_type = ClassTypeT;
};

// const member functions (and lambda's operator() gets redirected here)
template <typename ReturnTypeT, typename ClassTypeT, typename... Args>
struct closure_traits<ReturnTypeT (ClassTypeT::*)(Args...) const>
    : closure_traits<ReturnTypeT (ClassTypeT::*)(Args...)>
{
};

Disclaimer: the std::remove_reference was inspired by this code.

Luka Govedič
  • 399
  • 4
  • 14
  • 1
    It's a start and even covers many mainstream scenarios but still a ways to go yet. You're missing specializations for functions with exception specifiers (part of a function's type since C++17), cv-qualifiers on pointers (unless the caller explicitly removes them first via "std::remove_cv"), volatile specifier on non-static member functions, ref-qualifiers on non-static member functions, references to free functions (not supported in C++ for non-static member functions), variadic functions, calling conventions (not even part of standard C++ but part of a function's type anyway normally), etc. – Larry Jul 27 '23 at 20:57
  • 1
    Unfortunately it's too much work to properly tackle a question like this in a SO answer and few (reasonably) "complete" solutions exist anywhere in my experience so I wrote one (production grade, fully documented, free). See https://github.com/HexadigmAdmin/FunctionTraits (likely the most "complete" version on the web as near as I can tell at this writing and generally more complete than even Boost's own version at https://github.com/boostorg/callable_traits). – Larry Jul 27 '23 at 21:00