std :: function / bind类型擦除没有标准C ++库 [英] std::function/bind like type-erasure without Standard C++ library

查看:165
本文介绍了std :: function / bind类型擦除没有标准C ++库的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在基于发布/订阅模式在C ++ 11中开发一个简单的事件驱动应用程序。类有一个或多个 onWhateverEvent()由事件循环调用的方法(控制反转)。由于应用程序实际上是一个固件,其中代码大小至关重要,灵活性不是高优先级,订阅部分是一个简单的表,包含事件ID和相关的处理程序。



下面是一个非常简化的代码:

  #include< functional> 

枚举事件{
EV_TIMER_TICK,
EV_BUTTON_PRESSED
};

struct Button {
void onTick(int event){/ * publish EV_BUTTON_PRESSED * /}
};

struct Menu {
void onButtonPressed(int event){/ * publish EV_SOMETHING_ELSE * /}
};

按钮button1;
按钮button2;
菜单mainMenu;

std :: pair< int,std :: function< void(int)>> dispatchTable [] = {
{EV_TIMER_TICK,std :: bind(& Button :: onTick,& button1,std :: placeholders :: _ 1)},
{EV_TIMER_TICK,std :: bind & Button :: onTick,& button2,std :: placeholders :: _ 1)},
{EV_BUTTON_PRESSED,std :: bind(& Menu :: onButtonPressed,& mainMenu,std :: placeholders :: _1)}
};

int main(void)
{
while(1){
int event = EV_TIMER_TICK; // msgQueue.getEventBlocking();
(auto& a:dispatchTable){
if(event == a.first)
a.second(event);
}
}
}

桌面编译器和 std:function< void(int)>> fn = std :: bind(& SomeClass :: onSomething),& someInstance,std :: placeholders :: _ 1)优雅地实现类型擦除,所以事件分派表可以保存不同类的处理程序,因此不同类型。



问题来自支持C ++ 11的嵌入式编译器(AVR-GCC 4.8.3),但没有标准C ++库: code>< functional> 头。我在想如何重新创建上述行为只有编译器功能。我评估了几个选项,但每个(由编译器或我)有反对:


  1. virtual void Handler :: onEvent(int event)方法,并派生 Button / code>从它。调度表可以保存接口指针,虚拟方法调用完成其余操作。这是最简单的方法,但我不喜欢将事件处理程序方法的数量限制为每个类一个(使用本地if-else事件分派),并且每个事件有一个虚拟方法调用的开销。 / p>


  2. 我的第二个想法仍然包含一个虚拟方法调用,但对 Button 菜单类。这是一个虚拟的方法调用类型擦除与函子:

      struct FunctBase {
    virtual void operator事件)= 0;
    };

    template< typename T>
    struct funct:public FunctBase
    {
    T * pobj; // instance ptr
    void(T :: * pmfn)(int); // mem fun ptr
    Funct(T * pobj_,void(T :: * pmfn _)(int)):pobj(pobj_),pmfn(pmfn_){}

    void operator )(int ev)override {
    (pobj-> * pmfn)(ev);
    }
    };

    可以保存实例和方法指针,并且分发表可以由 FunctBase 指针构造。这种方式表与function / bind一样灵活:可以保存每个类的任何类(类型)和任何数量的处理程序。我只有一个问题,它仍然包含每个事件一个虚拟方法调用,它只是移动到函子。


  3. 我的第三个想法是一个简单的hack转换方法指针函数指针:

      typedef void(* Pfn)(void *,int); 
    Pfn pfn1 = reinterpret_cast< Pfn>(& Button :: onTick);
    Pfn pfn2 = reinterpret_cast< Pfn>(& Menu :: onButtonPressed);

    据我所知,这是未定义的行为,确实使编译器发出一个大的胖警告。它基于这样的假设,c ++方法有一个隐式的第一个参数指向 this


所以我的问题:是可能的做类似于选项3的干净的C ++方式吗?我知道有一个基于void *的类型擦除技术(与选项2中的虚拟方法调用相反),但我不知道如何实现它。查看桌面版本的std :: bind还给我的印象是,它绑定第一个隐式参数为实例指针,但也许只是语法。

解决方案

一个坚实,高效, std :: function< R(Args ...)> 替换不难写。 / p>

由于我们是嵌入式的,我们希望避免分配内存。所以我写一个 small_task< Signature,size_t sz,size_t algn> 。它创建一个大小为 sz 和对齐 algn 的缓冲区,其中存储其已擦除的对象。



它还存储一个动子,一个驱逐器和一个调用器函数指针。这些指针可以在本地 small_task (最大局部性)或手动 struct vtable {/*...*/} const *表

 模板< class Sig,size_t sz,size_t algn> 
struct small_task;

template< class R,class ... Args,size_t sz,size_t algn>
struct small_task< R(Args ...),sz,algn> {
struct vtable_t {
void(* mover)(void * src,void * dest);
void(* destroyer)(void *);
R(* invoke)(void const * t,Args& ... args);
template< class T>
static vtable_t const * get(){
static const vtable_t table = {
[](void * src,void * dest){
new(dest)T(std: :move(* static_cast< T *(src))));
},
[](void * t){static_cast< T *(t) },
[](void const * t,Args& ... args) - > R {
return(* static_cast< T const *>(t)) forward< Args(args)...);
}
};
return& table;
}
};
vtable_t const * table = nullptr;
std :: aligned_storage_t< sz,algn>数据;
模板< class F,
class dF = std :: decay_t< F> ;,
//不要在自己的类型上使用这个ctor:
std :: enable_if_t< std :: is_same< dF,small_task> {}> * = nullptr,
//仅当调用合法时才使用此ctor:
std :: enable_if_t< std :: is_convertible<
std :: result_of_t< dF const&(Args ...)>,R
> {}> * = nullptr
>
small_task(F& f):
table(vtable_t :: template get< dF>())
{
//更高质量的small_task会处理空函数指针
//和其他可空调用项,并构造为null small_task

static_assert(sizeof(dF)< = sz,object too large
static_assert(alignof(dF)< = algn,object too aligned);
new(& data)dF(std :: forward F(f));
}
//我发现这个重载是有用的,因为它迫使一些
//函数来很好地解决它们的重载:
// small_task(R(*) ...))
〜small_task(){
if(table)
table-> destroyer(& data);
}
small_task(small_task& o):
table(o.table)
{
if(table)
table-> mover (& o.data,& data);
}
small_task(){}
small_task&运算符=(small_task& o){
//这是有点粗鲁,不是非常异常安全
//你可以做得更好:
this->〜small_task
new(this)small_task(std :: move(o));
return * this;
}
显式运算符bool()const {return table;}
R operator()(Args ... args)const {
return table-& data,std :: forward< Args>(args)...);
}
};

template< class Sig>
using task = small_task< Sig,sizeof(void *)* 4,alignof(void *)>

void(Args ...)

code>,不关心传入的callable是否有返回值。



上述任务支持移动,但不能复制。添加副本意味着存储的所有内容必须是可复制的,并且需要vtable中的另一个函数(除了 src move $ c>是 const ,没有 std :: move )。

使用了少量的C ++ 14,即 enable_if_t decay_t 别名和类似。它们可以很容易地用C ++ 11编写,或者替换为 typename std :: enable_if<?> :: type



bind 最好用lambdas替换,老实说。我不会在非嵌入式系统上使用它。



另一个改进是教它如何处理 small_task vtable 指针而不是将其复制到数据缓冲区中,更小/不太对齐的$ c>并将它包装在另一个 vtable 中。这将鼓励使用 small_tasks ,这些对你的问题集只有足够大。






将成员函数转换为函数指针不仅是未定义的行为,通常函数的调用约定不同于成员函数。特别是



这个区别可以是微妙的,可以在更改不相关的代码,或编译器版本更改时出现,或者其他任何情况。所以我会避免,除非你没有其他选择。






如上所述,平台缺乏库。以上每次使用 std 都很小,所以我只写它们:

  template< class T> struct tag {using type = T;}; 
template< class Tag> using type_t = typename Tag :: type;
using size_t = decltype(sizeof(int));



移动



  template< class T> 
T&& move(T& t){return static_cast< T&&>(t);}



向前



 模板< class T> 
struct remove_reference:tag< T> {};
template< class T>
struct remove_reference< T&>:tag< T> {};
template< class T> using remove_reference_t = type_t< remove_reference< T>>

template< class T>
T&& forward(remove_reference_t< T& t){
return static_cast< T&&>(t);
}
template< class T>
T&& forward(remove_reference_t< T&& t){
return static_cast< T&&>(t);
}



衰减



  template< class T> 
struct remove_const:tag< T> {};
template< class T>
struct remove_const< T const>:tag< T> {};

template< class T>
struct remove_volatile:tag< T> {};
template< class T>
struct remove_volatile< T volatile>:tag< T> {};

template< class T>
struct remove_cv:remove_const< type_t< remove_volatile< T>>> {};


template< class T>
struct decay3:remove_cv< T> {};
template< class R,class ... Args>
struct decay3< R(Args ...)>:tag< R(*)(Args ...)> {};
template< class T>
struct decay2:decay3< T> {};
template< class T,size_t N>
struct decay2< T [N]>:tag< T *> {};

template< class T>
struct decay:decay2< remove_reference_t< T>> {};

template< class T>
using decay_t = type_t< decay< T>> ;;



is_convertible



  template< class T> 
T declval(); //没有实现

template< class T,T t>
struct integer_constant {
static constexpr T value = t;
constexpr integral_constant(){};
constexpr operator T()const {return value; }
constexpr T operator()()const {return value; }
};
template< bool b>
using bool_t = integral_constant< bool,b> ;;
using true_type = bool_t< true> ;;
using false_type = bool_t< false> ;;

template< class ...> struct voider:tag< void> {};
template< class ... Ts> using void_t = type_t< voider< Ts ...>>

命名空间细节{
template< template< class ...> class Z,class,class ... Ts>
struct can_apply:false_type {};
template< template< class ...> class Z,class ... Ts>
struct can_apply< Z,void_t< Z< Ts ...>>,Ts ...>:true_type {};
}
template< template< class ...> class Z,class ... Ts>
using can_apply = details :: can_apply< Z,void,Ts ...> ;;

命名空间详细信息{
template< class From,class To>
using try_convert = decltype(To {declval< From>()});
}
template< class From,class To>
struct is_convertible:can_apply< details :: try_convert,From,To> {};
模板<>
struct is_convertible< void,void>:true_type {};



enable_if



  template< bool,class = void> 
struct enable_if {};
template< class T>
struct enable_if< true,T>:tag< T> {};
template< bool b,class T = void>
using enable_if_t = type_t< enable_if< b,T>> ;;



result_of



  namespace details {
template< class F,class ... Args>
using invoke_t = decltype(declval< F>()(declval< Args>()...));

template< class Sig,class = void>
struct result_of {};
template< class F,class ... Args>
struct result_of< F(Args ...),void_t< invoke_t< F,Args ...> > >:
tag< invoke_t< F,Args ...> >
{};
}
template< class Sig>
using result_of = details :: result_of< Sig> ;;
template< class Sig>
使用result_of_t = type_t< result_of< Sig>> ;;



aligned_storage



  template< size_t size,size_t align> 
struct alignas(align)aligned_storage_t {
char buff [size];
};



is_same



  template< class A,class B> 
structure is_same:false_type {};
template< class A>
struct is_same< A,A>:true_type {};

live example ,我需要大约十几行每个 std 库模板。



我会把这个std库重新实现到命名空间notstd 中,以清楚发生了什么。



如果可以,使用将相同的函数折叠在一起的链接器,如金色链接器。模板元编程可能导致二进制膨胀,没有固体链接器剥离它。


I'm developing a simple event driven application in C++11 based on the publish/subscribe pattern. Classes have one or more onWhateverEvent() method invoked by the event loop (inversion of control). Since the application is in fact a firmware, where code size is critical and flexibility is not of high priority, the 'subscribe' part is a simple table with event id's and associated handlers.

Here's a very simplified code of the idea:

#include <functional>

enum Events {
    EV_TIMER_TICK,
    EV_BUTTON_PRESSED
};

struct Button {
    void onTick(int event) { /* publish EV_BUTTON_PRESSED */ }
};

struct Menu {
    void onButtonPressed(int event) { /* publish EV_SOMETHING_ELSE */ }
};

Button button1;
Button button2;
Menu mainMenu;

std::pair<int, std::function<void(int)>> dispatchTable[] = {
    {EV_TIMER_TICK, std::bind(&Button::onTick, &button1, std::placeholders::_1) },
    {EV_TIMER_TICK, std::bind(&Button::onTick, &button2, std::placeholders::_1) },
    {EV_BUTTON_PRESSED, std::bind(&Menu::onButtonPressed, &mainMenu, std::placeholders::_1) }
};

int main(void) 
{
    while(1) {
        int event = EV_TIMER_TICK; // msgQueue.getEventBlocking();
        for (auto& a : dispatchTable) {
            if (event == a.first) 
                a.second(event);
        }
    }
}

This compiles and runs fine with a desktop compiler, and std:function<void(int)>> fn = std::bind(&SomeClass::onSomething), &someInstance, std::placeholders::_1) elegantly implements type erasure so the event dispatch table can hold handlers of different classes, thus different types.

The problem comes with the embedded compiler (AVR-GCC 4.8.3) which supports C++11, but there's no Standard C++ Library: no <functional> header. I was thinking how can I re-create the above behavior with compiler features only. I evaluated a few options, but there are objections for each (by the compiler or me):

  1. Create an interface with a virtual void Handler::onEvent(int event) method, and derive Button and Menu from it. The dispatch table can hold interface pointers, and virtual method calls do the rest. This is the most simple approach but I don't like the idea of limiting the number of event handler methods to one per class (with doing local if-else event dispatch), and having the overhead of a virtual method call per event.

  2. My second idea still contains a virtual method call, but has no restrictions on the Button and Menu class. It's a virtual method call based type-erasure with functors:

    struct FunctBase {
        virtual void operator()(int event) = 0;
    };
    
    template<typename T>
    struct Funct : public FunctBase
    {
        T* pobj;                 //instance ptr
        void (T::*pmfn)(int);    //mem fun ptr
        Funct(T* pobj_, void (T::*pmfn_)(int)) : pobj(pobj_), pmfn(pmfn_) {}
    
        void operator()(int ev) override {
            (pobj->*pmfn)(ev);
        }
    };
    

    Funct can hold instance and method pointers, and the dispatch table can be constructed of FunctBase pointers. This way table is as flexible as with function/bind: can hold any class (type) and any number of handlers per class. My only problem that it still contains 1 virtual method call per event, it's just moved to the functor.

  3. My third idea is a simple hack converting method pointers to function pointers:

    typedef void (*Pfn)(void*, int);
    Pfn pfn1 = reinterpret_cast<Pfn>(&Button::onTick);
    Pfn pfn2 = reinterpret_cast<Pfn>(&Menu::onButtonPressed);
    

    As far as I know this is Undefined Behavior and indeed makes the compiler emit a big fat warning. It's based on the assumption that c++ methods have an implicit 1st argument pointing to this. Nonetheless it works, it's lightweight (no virtual calls), and it's flexible.

So my question: Is it possible to do something like option 3 in clean C++ way? I know there's a void* based type-erasure technique (opposed to virtual method call in option 2), but I don't know how to implement it. Looking at desktop version with std::bind also gives me the impression that it binds the first implicit argument to be the instance pointer, but maybe that's just the syntax.

解决方案

A solid, efficient, std::function<R(Args...)> replacement isn't hard to write.

As we are embedded, we want to avoid allocating memory. So I'd write a small_task< Signature, size_t sz, size_t algn >. It creates a buffer of size sz and alignment algn in which it stores its erased objects.

It also stores a mover, a destroyer, and an invoker function pointer. These pointers can either be locally within the small_task (maximal locality), or within a manual struct vtable { /*...*/ } const* table.

template<class Sig, size_t sz, size_t algn>
struct small_task;

template<class R, class...Args, size_t sz, size_t algn>
struct small_task<R(Args...), sz, algn>{
  struct vtable_t {
    void(*mover)(void* src, void* dest);
    void(*destroyer)(void*);
    R(*invoke)(void const* t, Args&&...args);
    template<class T>
    static vtable_t const* get() {
      static const vtable_t table = {
        [](void* src, void*dest) {
          new(dest) T(std::move(*static_cast<T*>(src)));
        },
        [](void* t){ static_cast<T*>(t)->~T(); },
        [](void const* t, Args&&...args)->R {
          return (*static_cast<T const*>(t))(std::forward<Args>(args)...);
        }
      };
      return &table;
    }
  };
  vtable_t const* table = nullptr;
  std::aligned_storage_t<sz, algn> data;
  template<class F,
    class dF=std::decay_t<F>,
    // don't use this ctor on own type:
    std::enable_if_t<!std::is_same<dF, small_task>{}>* = nullptr,
    // use this ctor only if the call is legal:
    std::enable_if_t<std::is_convertible<
      std::result_of_t<dF const&(Args...)>, R
    >{}>* = nullptr
  >
  small_task( F&& f ):
    table( vtable_t::template get<dF>() )
  {
    // a higher quality small_task would handle null function pointers
    // and other "nullable" callables, and construct as a null small_task

    static_assert( sizeof(dF) <= sz, "object too large" );
    static_assert( alignof(dF) <= algn, "object too aligned" );
    new(&data) dF(std::forward<F>(f));
  }
  // I find this overload to be useful, as it forces some
  // functions to resolve their overloads nicely:
  // small_task( R(*)(Args...) )
  ~small_task() {
    if (table)
      table->destroyer(&data);
  }
  small_task(small_task&& o):
    table(o.table)
  {
    if (table)
      table->mover(&o.data, &data);
  }
  small_task(){}
  small_task& operator=(small_task&& o){
    // this is a bit rude and not very exception safe
    // you can do better:
    this->~small_task();
    new(this) small_task( std::move(o) );
    return *this;
  }
  explicit operator bool()const{return table;}
  R operator()(Args...args)const{
    return table->invoke(&data, std::forward<Args>(args)...);
  }
};

template<class Sig>
using task = small_task<Sig, sizeof(void*)*4, alignof(void*) >;

live example.

Another thing missing is a high quality void(Args...) that doesn't care if the passed-in callable has a return value.

The above task supports move, but not copy. Adding copy means that everything stored must be copyable, and requires another function in the vtable (with an implementation similar to move, except src is const and no std::move).

A small amount of C++14 was used, namely the enable_if_t and decay_t aliases and similar. They can be easily written in C++11, or replaced with typename std::enable_if<?>::type.

bind is best replaced with lambdas, honestly. I don't use it even on non-embedded systems.

Another improvement would be to teach it how to deal with small_tasks that are smaller/less aligned by storing their vtable pointer rather than copying it into the data buffer, and wrapping it in another vtable. That would encourage using small_tasks that are just barely large enough for your problem set.


Converting member functions to function pointers is not only undefined behavior, often the calling convention of a function is different than a member function. In particular, this is passed in a particular register under some calling conventions.

Such differences can be subtle, and can crop up when you change unrelated code, or the compiler version changes, or whatever else. So I'd avoid that unless you have little other choice.


As noted, the platform lacks libraries. Every use of std above is tiny, so I'll just write them:

template<class T>struct tag{using type=T;};
template<class Tag>using type_t=typename Tag::type;
using size_t=decltype(sizeof(int));

move

template<class T>
T&& move(T&t){return static_cast<T&&>(t);}

forward

template<class T>
struct remove_reference:tag<T>{};
template<class T>
struct remove_reference<T&>:tag<T>{};
template<class T>using remove_reference_t=type_t<remove_reference<T>>;

template<class T>
T&& forward( remove_reference_t<T>& t ) {
  return static_cast<T&&>(t);
}
template<class T>
T&& forward( remove_reference_t<T>&& t ) {
  return static_cast<T&&>(t);
}

decay

template<class T>
struct remove_const:tag<T>{};
template<class T>
struct remove_const<T const>:tag<T>{};

template<class T>
struct remove_volatile:tag<T>{};
template<class T>
struct remove_volatile<T volatile>:tag<T>{};

template<class T>
struct remove_cv:remove_const<type_t<remove_volatile<T>>>{};


template<class T>
struct decay3:remove_cv<T>{};
template<class R, class...Args>
struct decay3<R(Args...)>:tag<R(*)(Args...)>{};
template<class T>
struct decay2:decay3<T>{};
template<class T, size_t N>
struct decay2<T[N]>:tag<T*>{};

template<class T>
struct decay:decay2<remove_reference_t<T>>{};

template<class T>
using decay_t=type_t<decay<T>>;

is_convertible

template<class T>
T declval(); // no implementation

template<class T, T t>
struct integral_constant{
  static constexpr T value=t;
  constexpr integral_constant() {};
  constexpr operator T()const{ return value; }
  constexpr T operator()()const{ return value; }
};
template<bool b>
using bool_t=integral_constant<bool, b>;
using true_type=bool_t<true>;
using false_type=bool_t<false>;

template<class...>struct voider:tag<void>{};
template<class...Ts>using void_t=type_t<voider<Ts...>>;

namespace details {
  template<template<class...>class Z, class, class...Ts>
  struct can_apply:false_type{};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, void_t<Z<Ts...>>, Ts...>:true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z, void, Ts...>;

namespace details {
  template<class From, class To>
  using try_convert = decltype( To{declval<From>()} );
}
template<class From, class To>
struct is_convertible : can_apply< details::try_convert, From, To > {};
template<>
struct is_convertible<void,void>:true_type{};

enable_if

template<bool, class=void>
struct enable_if {};
template<class T>
struct enable_if<true, T>:tag<T>{};
template<bool b, class T=void>
using enable_if_t=type_t<enable_if<b,T>>;

result_of

namespace details {
  template<class F, class...Args>
  using invoke_t = decltype( declval<F>()(declval<Args>()...) );

  template<class Sig,class=void>
  struct result_of {};
  template<class F, class...Args>
  struct result_of<F(Args...), void_t< invoke_t<F, Args...> > >:
    tag< invoke_t<F, Args...> >
  {};
}
template<class Sig>
using result_of = details::result_of<Sig>;
template<class Sig>
using result_of_t=type_t<result_of<Sig>>;

aligned_storage

template<size_t size, size_t align>
struct alignas(align) aligned_storage_t {
  char buff[size];
};

is_same

template<class A, class B>
struct is_same:false_type{};
template<class A>
struct is_same<A,A>:true_type{};

live example, about a dozen lines per std library template I needed.

I would put this "std library reimplementation" into namespace notstd to make it clear what is going on.

If you can, use a linker that folds identical functions together, like the gold linker. template metaprogramming can cause binary bloat without a solid linker to strip it.

这篇关于std :: function / bind类型擦除没有标准C ++库的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆