std :: function/bind就像没有标准C++库的type-erasure一样

Gyo*_*ely 6 c++ pointers type-erasure c++11

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

这是一个非常简化的想法代码:

#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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这可以使用桌面编译器进行编译和运行,并且std:function<void(int)>> fn = std::bind(&SomeClass::onSomething), &someInstance, std::placeholders::_1)优雅地实现类型擦除,因此事件调度表可以保存不同类的处理程序,从而保存不同类型的处理程序.

问题来自于支持C++ 11的嵌入式编译器(AVR-GCC 4.8.3),但是没有标准C++库:没有<functional>标头.我在想如何只使用编译器功能重新创建上述行为.我评估了一些选项,但每个选项都有异议(由编译器或我):

  1. 创建一个接口virtual void Handler::onEvent(int event)方法,并从中获得ButtonMenu如此.调度表可以保存接口指针,其余的是虚方法调用.这是最简单的方法,但我不喜欢将事件处理程序方法的数量限制为每个类一个(使用本地if-else事件调度),并且每个事件具有虚拟方法调用的开销.

  2. 我的第二个想法仍然包含一个虚方法调用,但对类ButtonMenu类没有限制.这是一个基于虚方法调用的类型擦除与仿函数:

    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);
        }
    };
    
    Run Code Online (Sandbox Code Playgroud)

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

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

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

    据我所知,这是未定义的行为,确实使编译器发出了很大的警告.它基于c ++方法有一个隐含的第一个参数指向的假设this.尽管如此,它很有效,它很轻(没有虚拟调用),而且很灵活.

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

Yak*_*ont 8

一个坚实,高效的std::function<R(Args...)>替代品并不难写.

在我们嵌入时,我们希望避免分配内存.所以我写了一个small_task< Signature, size_t sz, size_t algn >.它创建了一个大小sz和对齐的缓冲区,algn用于存储已擦除的对象.

它还存储了一个移动器,一个驱逐舰和一个调用函数指针.这些指针可以在本地small_task(最大位置)内,也可以在手册中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*) >;
Run Code Online (Sandbox Code Playgroud)

实例.

缺少的另一件事是高质量void(Args...),如果传入的callable具有返回值则无关心.

上述任务支持移动,但不支持复制.添加副本意味着存储的所有内容都必须是可复制的,并且需要vtable中的另一个函数(实现类似于move,除了srcconst和否std::move).

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

bind老实说,最好用lambdas取而代之.我甚至不在非嵌入式系统上使用它.

另一个改进是教它如何small_task通过存储vtable指针来处理更小/更少对齐的s,而不是将其复制到data缓冲区中,并将其包装在另一个缓冲区中vtable.这会鼓励使用small_tasks对你的问题集来说只是勉强够用的东西.


将成员函数转换为函数指针不仅是未定义的行为,通常函数的调用约定与成员函数不同.特别是,this在某些调用约定下传递给特定的寄存器.

这些差异可能很微妙,当您更改不相关的代码或编译器版本更改或其他任何内容时,可能会出现这种差异.除非你别无选择,否则我会避免这种情况.


如上所述,该平台缺乏图书馆.std上面的每一次使用都很小,所以我只写它们:

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

移动

template<class T>
T&& move(T&t){return static_cast<T&&>(t);}
Run Code Online (Sandbox Code Playgroud)

向前

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);
}
Run Code Online (Sandbox Code Playgroud)

衰变

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>>;
Run Code Online (Sandbox Code Playgroud)

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{};
Run Code Online (Sandbox Code Playgroud)

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>>;
Run Code Online (Sandbox Code Playgroud)

的结果

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>>;
Run Code Online (Sandbox Code Playgroud)

aligned_storage

template<size_t size, size_t align>
struct alignas(align) aligned_storage_t {
  char buff[size];
};
Run Code Online (Sandbox Code Playgroud)

是一样的

template<class A, class B>
struct is_same:false_type{};
template<class A>
struct is_same<A,A>:true_type{};
Run Code Online (Sandbox Code Playgroud)

实例,std我需要的每个库模板大约十几行.

我会把这个"std库重新实现"放进namespace notstd去,以明确发生了什么.

如果可以,请使用将相同功能折叠在一起的链接器,如黄金链接器.模板元编程可能会导致二进制膨胀而没有实体链接器来剥离它.