Search code examples
c++c++11templatestemplate-argument-deduction

Deducing std::function with more than two args


I wonder why std::function is only aware of two-argument functions. I've written some code which is working well, but there are a number of limitations. Any feedback welcome. In particular, I suspect I'm reinventing the wheel.

My code is on ideone and I will refer to it.

For example, I can describe the type of main with:

function_type_deducer(main).describe_me();
// Output: I return i and I take 2 arguments.  They are of type:  i PPc

(where 'i' means 'int' and 'PPc' means pointer-to-pointer-to-char)

Standard std::function doesn't work with functions with more than two args (see the last two lines of my code), but this code does (the sample code demonstrates three-arg functions). Maybe my design should be used in the standard library instead! I define typedef tuple<Args...> args_as_tuple; to store all args, not just the first two argument types.

The main trick is the deduction in this function:

template<class T, class... Args>
auto function_type_deducer(T(Args...)) -> Function__<T, Args...> {
        return Function__<T, Args...> {};
}

Limitations:

  • It doesn't work with lambdas. This won't compile function_type_deducer([](){}).describe_me();
  • It doesn't notice that there is a small difference between x and y, as y takes a string&, where x takes a string. (std::function doesn't notice this either)

Any ideas on how to fix either of these? Have I reinvented the wheel?


Solution

  • This won't compile function_type_deducer([](){}).describe_me();

    It would work if function_type_deducer wasn't a template. :) Non-capturing lambdas (empty []) are implicitly convertible to function pointers. Sadly, implicit conversions aren't taken into consideration for some template argument deduction. See this question for more information (note that my answer isn't completely correct as the comments indicate).


    It doesn't notice that there is a small difference between x and y, as y takes a string&, where x takes a string.

    That's not a problem with the function, that's a problem with typeid, as this simple testcode shows:

    template<class T>
    void x(void(T)){
        T v;
        (void)v;
    }
    
    void f1(int){}
    void f2(int&){}
    
    int main(){
        x(f1);
        x(f2);
    }
    

    Live example on Ideone. Output:

    error: ‘v’ declared as reference but not initialized

    A simple fix might be using tag dispatching:

    #include <type_traits> // is_reference
    #include <iostream>
    #include <typeinfo>
    
    template<class T>
    void print_name(std::true_type){
      std::cout << "reference to " << typeid(T).name();
    }
    
    template<class T>
    void print_name(std::false_type){
      std::cout << typeid(T).name();
    }
    
    template<class T>
    void print_name(){
      print_name(typename std::is_reference<T>::type());
    }
    

    And call print_name<NextArg>() instead of typeid(NextArg).name().


    Have I reinvented the wheel?

    Yes, kind of and no, you didn't. Boost.Function provides typedefs for all arguments (argN_type style), aswell as the static constant arity for the number thereof. However, you can't easily access those typedefs generically. You would need a roundabout way to not accidentially access non-existant ones. The tuple idea works best, however it can be written in a nicer way. Here's a modified version of something I once wrote:

    #include <tuple>
    #include <type_traits>
    #include <iostream>
    #include <typeinfo>
    
    namespace detail{
    
    template<class T>
    std::ostream& print_name(std::ostream& os);
    
    template<class T>
    std::ostream& print_pointer(std::ostream& os, std::true_type){
      typedef typename std::remove_pointer<T>:: type np_type;
      os << "pointer to ";
      return print_name<np_type>(os);
    }
    
    template<class T>
    std::ostream& print_pointer(std::ostream& os, std::false_type){
      return os << typeid(T).name();
    }
    
    template<class T>
    std::ostream& print_name(std::ostream& os, std::true_type){
      return os << "reference to " << typeid(T).name();
    }
    
    template<class T>
    std::ostream& print_name(std::ostream& os, std::false_type){
      return print_pointer<T>(os, typename std::is_pointer<T>::type());
    }
    
    template<class T>
    std::ostream& print_name(std::ostream& os){
      return print_name<T>(os, typename std::is_reference<T>::type());
    }
    
    // to workaround partial function specialization
    template<unsigned> struct int2type{};
    
    template<class Tuple, unsigned I>
    std::ostream& print_types(std::ostream& os, int2type<I>){
      typedef typename std::tuple_element<I,Tuple>::type type;
    
      print_types<Tuple>(os, int2type<I-1>()); // left-folding
      os << ", ";
      return print_name<type>(os);
    }
    
    template<class Tuple>
    std::ostream& print_types(std::ostream& os, int2type<0>){
      typedef typename std::tuple_element<0,Tuple>::type type;
      return print_name<type>(os);
    }
    
    } // detail::
    
    template<class R, class... Args>
    struct function_info{
      typedef R result_type;
      typedef std::tuple<Args...> argument_tuple;
      static unsigned const arity = sizeof...(Args);
    
      void describe_me(std::ostream& os = std::cout) const{
        using namespace detail;
        os << "I return '"; print_name<result_type>(os);
        os << "' and I take '" << arity << "' arguments. They are: \n\t'";
        print_types<argument_tuple>(os, int2type<arity-1>()) << "'\n";
      }
    };
    

    Live example on Ideone. Output:

    main:   I return 'i' and I take '2' arguments. They are: 
            'i, pointer to pointer to c'
    x:      I return 'Ss' and I take '3' arguments. They are: 
            'i, Ss, c'
    y:      I return 'Ss' and I take '3' arguments. They are: 
           'i, reference to Ss, c'