为EventDriver传递和存储模板函数指针

时间:2018-02-05 23:28:22

标签: c++ templates c++14 variadic-templates

我正在为C ++功能样式开发事件调度程序驱动程序的框架。在此框架中,用户可以使用驱动程序为事件注册操作。稍后当事件发生时,驱动程序将使用注册的操作调度事件,如下所示:

#include <stdio.h>
#include <tuple>
using namespace std;

template < class ... ES >
class EventDriver {
  public :
    template<class T> using Func = void (*)(T& x);

    tuple< Func< ES > ... > actions;

    // Func< > default_action;

    template < class EVENT>
    void setAction ( void (*fp )(EVENT& ) ){
        std::get< Func<EVENT> >( actions )=fp;
    }

    template < class EVENT>
    void notify( EVENT& e ){
        Func<EVENT> f = std::get< Func<EVENT> >( actions );

        if( f ){
            f( e );
        }else{
         printf("Could not process event. No registered action found \n");
        }

    }
};

// TEST the EventDriver
//
class EventAA{};
class EventBB{};
class EventCC{};

void processAA( EventAA& a ){
   printf("Processing EventAA \n");
}

int main (){

   EventDriver <EventAA, EventBB, EventCC> ed;
   ed.setAction( &processAA );

   EventAA aa;
   ed.notify( aa );

   EventBB bb;
   ed.notify( bb );  // will display that no action registered
} 

这很有效。现在作为最后一部分,我想为事件操作未向驱动程序注册的情况设置默认操作。我想为默认操作定义一个常用的模板化函数:

template<class E>
void processAnyEvent( E& a ){
   printf("Processing Event %s \n", a.toString(); );
}

并使用EventDriver ed.setDefaultAction( & processAnyEvent );中的方法注册 如果没有找到注册的动作,将调用它。

我发现实现此要求并获得许多编译器错误非常困难。我使用的是gcc版本6.3.0,c ++ 14

default_action成员变量的类型应该是什么? EventDriver::setDefaultAction函数的签名应该是什么? 还有其他任何方法可以实现吗?

2 个答案:

答案 0 :(得分:1)

默认初始化action如下?

std::tuple<Func<ES>...> actions { &processAnyEvent<ES>... };

- 编辑 -

OP要求一种方法将不同的默认处理程序传递给不同的对象。

问题是模板的功能是

template <typename E>
void processAnyEvent (E &)
 { std::cout << "Generic Processing Event"; }

不是单个函数,而是一组函数。 EventDriver需要所有这些(或者,至少每个模板参数一个),我不知道传递函数指针集的简单方法。

我能想象的最好的方法是用struct static方法写一些func()作为

struct genEv1
 {
   template <typename E>
   static void func (E &)
    { std::cout << "Generic Processing Event 1"; }
 };

struct genEv2
 {
   template <typename E>
   static void func (E &)
    { std::cout << "Generic Processing Event 2"; }
 };

您可以将其作为EventDriver

中的第一个模板参数传递
EventDriver<genEv1, EventAA, EventBB, EventCC>  ed; 

并使用它来默认初始化actions

template <typename GPES, typename ... ES>
struct EventDriver
 {
   // ...
   std::tuple<Func<ES>...>  actions{ &GPES::template func<ES>... };
   // ...
 };

另一种方法是使用模板struct通用处理程序作为构造函数参数传递func()

 EventDriver<EventAA, EventBB, EventCC>  ed{ genEv1{} };

EventDriver

中添加模板构造函数
template <typename ... ES>
struct EventDriver
 {
   // ...
   std::tuple<Func<ES>...>  actions;

   template <typename GPES>
   EventDriver (GPES const &) : actions{ &GPES::template func<ES>... }
    { }
   // ...
};

观察如果将genEv1作为模板参数传递,则具有不同默认处理程序的两个EventDriver对象是不同类型的对象,其中,如果genEv1是构造函数参数,则它们是相同的类型(当Es...类型相同时)。

答案 1 :(得分:0)

这是可能的,但路径有点刮风。

首先,除了在编译器能够确定只使用一个实际函数类型的某些情况下,不允许使用指向函数名称的指针,该函数是重载函数和/或函数模板。因此,&func_name永远不会代表可以使用多种类型调用的内容。

但是C ++确实有一个可以用多种类型调用的东西:任何具有重载的类对象(称为仿函数)和/或名为operator()的模板成员函数。因此,让setDefaultAction成为接受任何此类仿函数的模板成员函数,只要可以使用所有可能的事件类型调用它。

但是我们想要在课堂上存储该仿函数的副本。 setDefaultAction可以是一个功能模板,但我们不想让类EventDriver需要一个模板参数,所以我们需要某种类型的删除&#34;类型擦除& #34;技术。 std::function应该会有所帮助,因为它可以存储任何类型的仿函数并将调用转发给它。 (std::function是指向函数的指针概念的概括,它表示可以像函数一样使用的任何东西,包括指向函数,函子和指向类成员的指针。)

但是,使用std::function需要特定的参数列表类型(和返回类型),因此我们不能声明可以采用任何事件类型的std::function。幸运的是,在EventDriver专业化的背景下,我们有一个有限的可能类型列表。 C ++ 17有一个类,用于表示从可能类型的有限列表中选择的任何类型的值&#34;:std::variant。这与函数std::visit一起使用variant中的当前值调用给定的仿函数。

除了我们想要的参数类型是对事件类型的引用,std::variant不允许直接存储引用。有几个合理的解决方法:

  • 存储std::reference_wrapper<E>而不是E&。在创建variant时创建引用包装器对象,并在调用该仿函数时获取实际引用。

  • 存储E*而不是E&。在创建variant时获取事件的地址,并在调用仿函数时再次取消引用。

他们关于相同数量的工作,我认为reference_wrapper更清楚地表明了意图,这就是我所展示的方式。

将以上所有内容放在一起:

#include <utility>
#include <functional>
#include <variant>
#include <iostream>

template < class ... ES >
class EventDriver {
public :
    // ...
    template<class T> using Func = std::function<void(T&)>;

    std::tuple< Func< ES > ... > actions;

    using VariantType = std::variant< std::reference_wrapper<ES>... >;
    std::function<void(const VariantType&)> default_action;

    template < typename F >
    void setDefaultAction( F func ) {
        auto unwrap_and_call = [func]( auto& ref )
            { func( ref.get() ); };
        default_action = [unwrap_and_call]( const VariantType& v )
            { std::visit( unwrap_and_call, v ); };
    }

    template < class EVENT >
    void notify( EVENT& e ){
        Func<EVENT> f = std::get< Func<EVENT> >( actions );

        if ( f ) {
            f( e );
        } else if ( default_action ) {
            default_action( std::ref( e ) );
        } else {
            std::cout << "Could not process event."
                " No registered action found \n";
        }
    }

    // ...
};

现在它有效。请注意,如果您仍然喜欢使用一些普通的命名函数,函数指针就像setDefaultAction作为类类型仿函数一样有效。

但接下来我要推荐一个额外的改变。由于仿函数比指针更灵活,并且该类现在接受setDefaultAction的任何仿函数,为什么不允许setAction中的仿函数?

using Func = void (*)(T&)更改为using Func = std::function<void(T&)>是一件容易的事。这次遇到的麻烦是从一般的仿函数F func确定事件类型并不是那么简单,因为它来自函数指针void (*fp)(EVENT&),我们需要知道哪个{{1}用于存储函子的插槽。

我完全没有详细介绍我对该问题的解决方案,但基本的想法是:

  • tuple函数签名中使用std::enable_if,以确保可以使用其中一种可能的事件类型调用setAction

  • 鉴于我们确切知道一个事件类型是可调用的,请定义一个帮助器特征结构来获取该事件类型。它使用递归来构建一次检查的结果。

一个完整的工作示例:

F

Run and/or edit this program on coliru.

我还定义了一个可以像// #include <cstdio> #include <iostream> #include <typeinfo> #include <utility> #include <type_traits> #include <functional> #include <tuple> #include <variant> namespace EventDriverPrivate { template <typename F, class ArgTuple, typename Enable=void> struct find_invocable_arg; template <typename F, class E0, class ... ERest> struct find_invocable_arg<F, std::tuple<E0, ERest...>, std::enable_if_t<std::is_invocable<F&, E0&>::value>> { using type = E0; }; template <typename F, class E0, class ... ERest> struct find_invocable_arg<F, std::tuple<E0, ERest...>, std::enable_if_t<!std::is_invocable<F&, E0&>::value>> { using type = typename find_invocable_arg<F, std::tuple<ERest...>>::type; }; template <typename F, class ... ES> using find_invocable_arg_t = typename find_invocable_arg<F, std::tuple<ES...>>::type; } template < class ... ES > class EventDriver { public : template<class T> using Func = std::function<void(T&)>; std::tuple< Func< ES > ... > actions; using VariantType = std::variant< std::reference_wrapper<ES>... >; std::function<void(const VariantType&)> default_action; template < typename F, std::enable_if_t<(... + std::is_invocable<F&, ES&>::value) == 1, int> =0 > void setAction ( F func ) { std::get< Func< EventDriverPrivate::find_invocable_arg_t<F, ES...> > > ( actions ) = std::move(func); } template < class EVENT, typename F > void setActionFor ( F func ) { std::get< Func<EVENT> >( actions ) = std::move(func); } template < typename F > void setDefaultAction( F func ) { auto unwrap_and_call = [func]( auto& ref ) { func( ref.get() ); }; default_action = [unwrap_and_call]( const VariantType& v ) { std::visit( unwrap_and_call, v ); }; } template < class EVENT> void notify( EVENT& e ){ Func<EVENT> f = std::get< Func<EVENT> >( actions ); if ( f ) { f( e ); } else if ( default_action ) { default_action( std::ref( e ) ); } else { std::cout << "Could not process event. No registered action found \n"; } } }; // TEST the EventDriver // class EventAA{}; class EventBB{}; class EventCC{}; void processAA( EventAA& ){ std::cout << "Processing EventAA \n"; } int main (){ EventDriver <EventAA, EventBB, EventCC> ed; ed.setAction( &processAA ); ed.setAction( [](EventBB&) { std::cout << "Processing EventBB\n"; } ); ed.setDefaultAction( [](auto& e) { std::cout << "Default action got type " << typeid(e).name() << "\n"; } ); EventAA aa; ed.notify( aa ); EventBB bb; ed.notify( bb ); EventCC cc; ed.notify( cc ); } 一样使用的函数。如果您有一些实际上可以为多个事件类型调用的仿函数,这可能很有用。只需要一个可调用事件类型的ed.setActionFor<EventXX>(handler);将失败,我们不希望编译器自动猜测要使用哪个事件槽,而是使用此备用函数可以指定要使用的事件处理程序。

enable_if_t概括中的一个缺点:如果一个事件类型可能在同一个setAction列表中使用不同的事件类型作为基类,只有一个可调用事件类型的要求不起作用,因为用于基本事件的处理程序也可以从从该基类派生的事件中调用。在这种情况下,可以提出一些更复杂的模板助手来查找一个可调用的事件类型,它比所有其他可调用的事件类型更加派生....