Standard method for determining the arity and other traits of std::bind() result?
Asked Answered
O

1

7

I've been pounding my head for a few days trying to figure out how to make a class have a nice clean public interface to perform registration of callback mechanisms. The callbacks can be C++11 lambdas, std::function<void(Type1,Type2)>, std::function<void(Type2)>, std::function<void()>, or the results of std::bind().

The key to this interface is that the user of the class only needs to know about one public interface that accepts pretty much any functor/callback mechanism the user might throw at it.

Simplified class showing registration of functors and interface

struct Type1;
struct Type2; // May be the same type as Type1
class MyRegistrationClass
{
public:
    /**
     * Clean and easy to understand public interface:
     * Handle registration of any functor matching _any_ of the following
     *    std::function<void(Type1,Type2)>
     *    std::function<void(Type2)>        <-- move argument 2 into arg 1
     *    std::function<void()>
     *    or any result of std::bind() requiring two or fewer arguments that
     *    can convert to the above std::function< ... > types.
     */
    template<typename F>
    void Register(F f) {
       doRegister(f);
    }
private:
    std::list< std::function< void(Type1, Type2) > > callbacks;


    // Handle registration for std::function<void(Type1,Type2)>
    template <typename Functor>
    void doRegister(const Functor & functor,
                           typename std::enable_if< 
                                   !is_bind_expr<Functor>
                                   && functor_traits<decltype(&Functor::operator())>::arity == 2
                               >::type * = nullptr )
    {
        callbacks.push_back( functor );
    }

    // Handle registration for std::function<void(Type2)> by using std::bind
    // to discard argument 2 ...
    template <typename Functor>
    void doRegister(const Functor & functor, 
                           typename std::enable_if< 
                                   !std::is_bind_expression< Functor >::value
                                   && functor_traits<decltype(&Functor::operator())>::arity == 1
                               >::type * = nullptr )
    {
        // bind _2 into functor
        callbacks.push_back( std::bind( functor,                              std::placeholders::_2 ) );
    }

    // Handle registration for std::function<void(Type2)> if given the results
    // of std::bind()
    template <typename Functor>
    void doRegister(const Functor & functor,
                           typename std::enable_if< 
                                   is_bind_expr<Functor>
///////////////////////////////////////////////////////////////////////////
//// BEGIN Need arity of a bounded argument
///////////////////////////////////////////////////////////////////////////
                                   && functor_traits<decltype(Functor)>::arity == 1  
///////////////////////////////////////////////////////////////////////////
//// END need arity of a bounded argument
///////////////////////////////////////////////////////////////////////////
                               >::type * = nullptr )
    {
        // Push the result of a bind() that takes a signature of void(Type2)
        // and push it into the callback list, it will automatically discard
        // argument1 when called, since we didn't bind _1 placeholder
        callbacks.push_back( functor );
    }

    // And other "doRegister" methods exist in this class to handle the other
    // types I want to support ...
}; // end class

The only reason to have the complexity of using enable_if<> is to turn on/off certain methods. We have to do this because when we want to pass in the results of std::bind() to the Register() method and it can ambiguously match against multiple registration methods if we had simple signatures like this:

void doRegister( std::function< void(Type1, Type2) > arg );
void doRegister( std::function< void(Type2) > arg ); // NOTE: type2 is first arg
void doRegister( std::function< void() > arg );

Rather than re-invent the wheel, I've referenced traits.hpp and then wrapped it with my own trait helper named "functor_traits" that adds support for std::bind()

This is what I've come up with so far to identify bounded function "arity" ... or a count of how many arguments the bind result expects as a :

My attempt at finding the bind result arity

#include <stdio.h>
// Get traits.hpp here: https://github.com/kennytm/utils/blob/master/traits.hpp
#include "traits.hpp" 

using namespace utils;
using namespace std;

void f1() {};
int f2(int) { return 0; }
char f3(int,int) { return 0; }

struct obj_func0 
{
    void operator()() {};
};
struct obj_func1
{
    int operator()(int) { return 0; };
};
struct obj_func2 
{
    char operator()(int,int) { return 0; };
};


/**
 * Count the number of bind placeholders in a variadic list
 */
template <typename ...Args>
struct get_placeholder_count
{
    static const int value = 0;
};
template <typename T, typename ...Args >
struct get_placeholder_count<T, Args...>
{
    static const int value = get_placeholder_count< Args... >::value + !!std::is_placeholder<T>::value;
};


/**
 * get_bind_arity<T> provides the number of arguments 
 * that a bounded expression expects to have passed in. 
 *  
 * This value is get_bind_arity<T>::arity
 */

template<typename T, typename ...Args>
struct get_bind_traits;

template<typename T, typename ...Args>
struct get_bind_traits< T(Args...) >
{
    static const int arity = get_placeholder_count<Args...>::value;
    static const int total_args = sizeof...(Args);
    static const int bounded_args = (total_args - arity);
};

template<template<typename, typename ...> class X, typename T, typename ...Args>
struct get_bind_traits<X<T, Args...>>
{
    // how many arguments were left unbounded by bind
    static const int arity        = get_bind_traits< T, Args... >::arity;

    // total arguments on function being called by bind
    static const int total_args   = get_bind_traits< T, Args... >::total_args;

    // how many arguments are bounded by bind:
    static const int bounded_args = (total_args - arity);

    // todo: add other traits (return type, args as tuple, etc
};

/**
 * Define wrapper "functor_traits" that wraps around existing function_traits
 */
template <typename T, typename Enable = void >
struct functor_traits;

// Use existing function_traits library (traits.hpp)
template <typename T>
struct functor_traits<T, typename enable_if< !is_bind_expression< T >::value >::type > :
    public function_traits<T>
{};

template <typename T>
struct functor_traits<T, typename enable_if< is_bind_expression< T >::value >::type >
{
    static const int arity = get_bind_traits<T>::arity;
};

/**
 * Proof of concept and test routine
 */
int main()
{
    auto lambda0 = [] {};
    auto lambda1 = [](int) -> int { return 0; };
    auto lambda2 = [](int,int) -> char { return 0;};
    auto func0 = std::function<void()>();
    auto func1 = std::function<int(int)>();
    auto func2 = std::function<char(int,int)>();
    auto oper0 = obj_func0();
    auto oper1 = obj_func1();
    auto oper2 = obj_func2();
    auto bind0 = bind(&f1);
    auto bind1 = bind(&f2, placeholders::_1);
    auto bind2 = bind(&f1, placeholders::_1, placeholders::_2);
    auto bindpartial = bind(&f1, placeholders::_1, 1);

    printf("action        : signature       : result\n");
    printf("----------------------------------------\n");
    printf("lambda arity 0: [](){}          : %i\n", functor_traits< decltype(lambda0) >::arity );
    printf("lambda arity 1: [](int){}       : %i\n", functor_traits< decltype(lambda1) >::arity );
    printf("lambda arity 2: [](int,int){}   : %i\n", functor_traits< decltype(lambda2) >::arity );
    printf("func arity   0: void()          : %i\n", functor_traits< function<void()> >::arity );
    printf("func arity   1: int(int)        : %i\n", functor_traits< function<void(int)> >::arity );
    printf("func arity   2: char(int,int)   : %i\n", functor_traits< function<void(int,int)> >::arity );
    printf("C::operator()() arity 0         : %i\n", functor_traits< decltype(oper0) >::arity );
    printf("C::operator()(int) arity 1      : %i\n", functor_traits< decltype(oper1) >::arity );
    printf("C::operator()(int,int) arity 2  : %i\n", functor_traits< decltype(oper2) >::arity );
///////////////////////////////////////////////////////////////////////////
// Testing the bind arity below:
///////////////////////////////////////////////////////////////////////////
    printf("bind arity   0: void()          : %i\n", functor_traits< decltype(bind0) >::arity );
    printf("bind arity   1: int(int)        : %i\n", functor_traits< decltype(bind1) >::arity );
    printf("bind arity   2: void(int,int)   : %i\n", functor_traits< decltype(bind2) >::arity );
    printf("bind arity   X: void(int, 1 )   : %i\n", functor_traits< decltype(bindpartial) >::arity );

    return 0;
}

While this implementation works in gcc with libstdc++, I'm not quite sure if this is a portable solution since it tries to break apart the results of std::bind() ... The nearly private "_Bind" class that we really shouldn't need to do as users of libstdc++.

So my questions are: How can we determine the arity of bind results without decomposing the result of std::bind()? and How can we implement a full implementation of function_traits that supports bounded arguments as much as possible?

Oribelle answered 25/12, 2013 at 2:55 Comment(2)
Simple: Don't. Let the user worry about how to handle your two arguments. std::bind already ignores arguments without suitable placeholders, so the user only needs to pass std::bind(f, _1, _2), std::bind(f, _1), std::bind(f, _2), or std::bind(f). Any solution that builds on decomposing through function_traits is inherently bad/broken anyways.Calvincalvina
Xeo, sometimes behavioral changes are intended depending on argument order. But more importantly, the real problem being mentioned about is the need to prevent ambiguous matching of arguments.Oribelle
H
3

OP, your premises are flawed. You're looking for some kind of routine that can tell you, for any given object x, how many arguments x expects — that is, which of x(), x(a), or x(a,b) is well-formed.

The problem is that any number of those alternatives might be well-formed!

In a discussion on isocpp.org of this very topic, Nevin Liber very correctly writes:

For many function objects and functions, the concepts of arity, parameter type and return type don't have a single answer, as those things are based on how it [the object] is being used, not on how it has been defined.

Here's a concrete example.

struct X1 {
    void operator() ()        { puts("zero"); }
    void operator() (int)     { puts("one");  }
    void operator() (int,int) { puts("two");  }
    void operator() (...)     { puts("any number"); }

    template<class... T>
    void operator() (T...)    { puts("any number, the sequel"); }
};

static_assert(functor_traits<X1>::arity == ?????);

So the only interface we can actually implement is one where we supply an actual argument count, and ask whether x can be called with that number of arguments.

template<typename F>
struct functor_traits {
    template<int A> static const int has_arity = ?????;
};

...But what if it can be called with one argument of type Foo or two arguments of type Bar? It seems that just knowing a (possible) arity of x isn't useful — it doesn't really tell you how to call it. To know how to call x, we need to know more or less what types we're trying to pass to it!

So, at this point, the STL comes to our rescue in at least one way: std::result_of. (But see here for a safer decltype-based alternative to result_of; I use it here only for convenience.)

// std::void_t is coming soon to a C++ standard library near you!
template<typename...> using void_t = void;

template<typename F, typename Enable = void>
struct can_be_called_with_one_int
{ using type = std::false_type; };

template<typename F>  // SFINAE
struct can_be_called_with_one_int<F, void_t<typename std::result_of<F(int)>::type>>
{ using type = std::true_type; };

template<typename F>  // just create a handy shorthand
using can_be_called_with_one_int_t = typename can_be_called_with_one_int<F>::type;

Now we can ask questions like can_be_called_with_one_int_t<int(*)(float)> or can_be_called_with_one_int_t<int(*)(std::string&)> and get reasonable answers.

You could construct similar traits classes for can_be_called_with_no_arguments, ...with_Type2, ...with_Type1_and_Type2, and then use the results of all three of those traits to build up a complete picture of your x's behavior — at least, the part of x's behavior that is relevant to your particular library.

Habitue answered 14/9, 2014 at 4:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.