简体   繁体   中英

Type-erasure and lambdas: (Partial) template speciallization matching lambda expressions

First of all, some context

As part of a policy-based particle engine I'm currently writting, I have done some type-erasure on policy classes. Specifically, I have done type erasure on the particle evoution policies:

A particle evolution policy is just a policy which says how the data (Coordinates, speed, color, etc) of a particle changes along the simulation. This is its interface:

struct evolution_policy
{
    template<PARTICLE_DATA>
    void operator()( PARTICLE_DATA& data );

    void step(); 
};

The operator() overload just takes a particle data and modifies it acording to the supposed policy. The step() function is just a function to update (Advance) the state of the policy (If the policy has some state).

Among other cases, I just needed type erasure to allow the user to use simple function entities (Functions, lambdas, etc) to be used as a evolution policy, for example:

add_policy( []( some_particle_data_type& data ) { /* do something with */ } );

Where the add_policy() function takes some policy and stores it in a vector. As you can see, the point of type erasure is to treat different kinds of policy classes/entities in the same homogeneous way.

The problem:

I'm using the dynamic dispatch approach for type erasure:

template<tyoename PARTICLE_DATA>
struct type_erased_policy
{
public:
    void operator()( PARTICLE_DATA& data )
    {
        (*_policy)( data );
    }

    void step()
    {
        _policy->step();
    }

private:
    struct policy_interface
    {
        virtual ~policy_interface() {}

        virtual void operator()( PARTICLE_DATA& data ) = 0;
        virtual void step() = 0;
    };

    template<typename POLICY>
    class policy_impl : public policy_interface
    {
    public:
        void operator()( PARTICLE_DATA& data ) override
        {
            _policy( particle );
        }

        void step() override
        {
            _policy.step();
        }

    private:
        POLICY _policy;
    };

    std::shared_ptr<policy_interface> _policy;
};

With that in mind, its easy to write an specialization to type-erase shared pointers of policies, for example:

template<typename T>
class policy_impl<std::shared_ptr<T>> : public policy_interface
{
public:
    void operator()( PARTICLE_DATA& data ) override
    {
        (*_policy)( data );
    }

    void step( cpp::evolution_policy_step step_type ) override
    {
        _policy->step( step_type );
    }

private:
    std::shared_ptr<T> _policy;
};

That could be useful if we need to share a policy between particles, for example.

We could write an specialization for std::function<void(PARTICLE_DATA&)> policies using that pattern. However, this only works for explicit instantations of std::function<void(PARTICLE_DATA&)> , ie if we pass a lambda function to the type_erased_policy ctor, it will instantiate the generic policy_impl case, because there is no specialization for the lambda type.

As lambda functors (closures) are unique (In other words, the type of a lambda expression is unique and unespecified) there is no easy way to do this kind of type erasure over a lambda .

My question is: My goal is to take any function entity (A lambda, a functor, a function pointer, a std::function ) and do type erasure on it in the way explained above. Is there any (other) way to match lambdas and/or other function entities to do type-erasure on them?

Platform:

I'm using GCC 4.8.2

First, a policy class that detects .step() :

namespace impl{
  // eat a type and do nothing with it:
  template<typename T> struct type_sink { typedef void type; };
  template<typename T> using TypeSink = typename type_sink<T>::type;

  // detect .step on T (T& means rvalue, T means lvalue, and T const& means const lvalue, etc)
  template<typename T, typename=void>
  struct has_step:std::false_type {};

  template<typename T>
  struct has_step<T, TypeSink< decltype( std::declval<T>().step() ) > >:
    std::true_type
  {};
}
template<typename T> struct has_step : impl::has_step<T> {};

So now we have a has_step<T> traits class that is true_type if T has a .step() method callable, and false_type otherwise.

If we are feeding this to a function, we can pick which implementation to run with tag dispatching:

template<typename T>
return_type_whatever type_erase_helper( T&& t, std::true_type /* test passed */ ) {
  // branch 1
}
template<typename T>
return_type_whatever type_erase_helper( T&& t, std::false_type /* test failed */ ) {
  // branch 2
}
template<typename T>
return_type_whatever type_erase( T&& t ) {
  return type_erase_helper( std::forward<T>(t), has_step< typename std::decay<T>::type& >() );
}

If you really want to specialize a particular class based on having step or not, you can use SFINAE techniques. But type erasure does not rely on the type erasure implementation being based off of specialization: I'd just use tag dispatching on the function that generates the type erasure implementation object.

We can daisy-chain such tag dispatching. Another good one might be is_signature_compatible< T, R(Args...) > :

namespace impl {
  template<typename T, typename Sig,typename=void>
  struct is_signature_compatible:std::false_type {};
  template<typename T, typename R, typename... Args>
  struct is_signature_compatible< T, R(Args...), typename std::enable_if<
    std::is_convertible< typename std::result_of< T(Args...) >::type, R >::value
  >::type >:std::true_type {};
  // dunno if this is needed?  Possibly, and shouldn't hurt:
  template<typename T, typename... Args>
  struct is_signature_compatible< T, void(Args...),
    TypeSink< typename std::result_of< T(Args...) >::type >
  >:std::true_type {};
}
template<typename T, typename Sig>
struct is_signature_compatible:impl::is_signature_compatible<T,Sig> {};

which we can then use to split the wheat from the chaff in a less "error occurs 10 recursive template invocations deep" way.

To facilitate the technique wrap your lambda function in a std::function . Since its constructor is greedy, it will absorbe any callable, yet the gain of wrapping would be declaring an explicit type

auto f1 = [](double a, double b)->int {
    return a + b > 5.; // example function body
});

std::function<int(double,double)> f2 ( [](double a, double b)->int {
    return a + b > 5.; // example function body
};);

So in the above context f1 faces the problems you mention, while f2 is a lambda (preserves all the convinience in declaring and using it) that, in a sense, has no 'stray type'

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