问题描述
我正在基于发布/订阅模式在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> 头。我在想如何重新创建上述行为只有编译器功能。我评估了几个选项,但每个(由编译器或我)有反对:
-
virtual void Handler :: onEvent(int event)
方法,并派生Button
和/ code>从它。调度表可以保存接口指针,虚拟方法调用完成其余操作。这是最简单的方法,但我不喜欢将事件处理程序方法的数量限制为每个类一个(使用本地if-else事件分派),并且每个事件有一个虚拟方法调用的开销。 / p>
-
我的第二个想法仍然包含一个虚拟方法调用,但对
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一样灵活:可以保存每个类的任何类(类型)和任何数量的处理程序。我只有一个问题,它仍然包含每个事件一个虚拟方法调用,它只是移动到函子。 -
我的第三个想法是一个简单的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 *)>
,我需要大约十几行每个 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):
Create an interface with a
virtual void Handler::onEvent(int event)
method, and deriveButton
andMenu
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.My second idea still contains a virtual method call, but has no restrictions on the
Button
andMenu
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 ofFunctBase
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.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*) >;
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_task
s 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 ++库的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!