我正试图找出如何使用c++17(不使用boost!)最优雅地解决这个问题。我正在开发一个提供UI控件的库。控件应该能够对事件做出反应,用户(使用库)应该能够为这些事件添加他的处理程序。我想避免使用命令模式,因为这对用户来说太复杂了——他只想定义一个函数,并将其分配给一些事件和UI控件。以下是一些示例代码:
// My library
class Base; // Base class for all the UI controls
class ListBox : public Base {
// handlers reacting on clicking the item in listbox
std::vector<std::variant<std::function<void()>,
std::function<void(int)>>> click_handlers_;
// handlers reacting on changing the name of the item in listbox
std::vector<std::variant<std::function<void()>,
std::function<void(std::string, std::string)>>> edit_handlers_;
public:
void AddHandler(Event ev, const std::function< ? ? ? > handler);
};
// USER's code
int main() {
ListBox lb;
lb.AddHandler(Event::Click, [](int index) { DoSomethingWithItem(i); });
lb.AddHandler(Event::Edit, [](std::string old_name, std::string new_name)
{ DoSomethingWithItemNames(old_name, new_name); });
lb.AddHandler(Event::Edit, []() { DoSomeCleanup(); });
}
当Event::Edit
(类型为enum
)事件发生时,它会触发需要这两个参数的DoSomethingWithItemNames()
。紧接着,DoSomeCleanup()
完成了它的工作并处理了事件(至少从用户的角度来看)。
- 问题:对于每个LisBox事件,我必须定义一个单独的处理程序向量
- 问题:
ListBox::AddHandler
方法的第二个参数的类型应该是什么
有没有办法统一这些处理程序,以便用户只能使用一个函数AddHandler()
,而不能使用像AddClickHandler()
和AddEditHandler()
这样的单独函数?当然,应该静态地检查函数类型,并且任何类型不匹配都应该在编译时报告给用户。有没有一种标准的方法来处理整个概念?请记住,每个控件都有自己的事件,我的想法是,每个事件都可以分配一个不带参数的处理程序函数。谢谢
编辑
经过考虑,又出现了一个问题。图书馆的设计是这样的。事件由系统生成,然后系统调用预定义的框架方法,我需要从中调用用户的处理程序(例如,在Qt中,当项目在QListWidget内双击时,会发出信号itemDoubleClicked(QListWidgetItem *item)
)。因此,实现类(FrameWorkAListBox
)需要访问存储的处理程序(当前在包装类ListBox
中)。有更多的框架要使用,那么我应该在哪里以及如何存储它们,这样它们就不会重复,并且体系结构保持易于扩展?
编辑2
经过几次尝试和更多的研究,我最终得到了这个解决方案:
// first define some aliases
template<class... Args>
using Handler = std::function<void(Args...)>; // event handler
template<class... Args>
using VariableHandler = std::variant<Args...>;
template<class T>
using Event = std::list<T>; // list of handler variants
// this is for easier std::visit use, taken from
https://en.cppreference.com/w/cpp/utility/variant/visit
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...)->overloaded<Ts...>;
// ListBox.cpp - interface class, used by the user
void ListBox::AddDoubleClickHandler(
const VariableHandler<Handler<>, Handler<int>>& handler) {
// forwards the handler to the implementation class (pushes the
// handler into the ev_double_click_ vector since wrapper class cannot
// have any other private fields aside from pointer to the
// implementation (according to pimpl idiom)
}
// ListBoxWindowsImpl.h - native Windows class representing UI control
class ListBoxImpl : public SomeNativeWindowsListBoxClass {
public:
// double-click event handler can have 0 or 1int argument
Event<VariableHandler<Handler<>, Handler<int>>> ev_double_click_;
// this gets called automatically by the system, when the user
// double-clicks an item within the listbox
void OnItemDoubleClick(int index) {
for (const auto& h : ev_double_click_) {
std::visit(overloaded {
[](Handler<> arg) { arg(); },
[index](Handler<int> arg) { arg(index); },
}, h);
}
}
我想知道是否有一种方法可以在不需要向ListBox类添加新函数的情况下添加新的处理程序。如果添加双击处理程序和新处理程序(例如,选择更改、单击…)可以由一个函数完成,那就太好了,比如ListBox::AddHandler。最好的方法是在添加新的处理程序时不需要更改此方法的主体,这样就不需要重新编译库的接口(在本例中为ListBox类)。谢谢
您可以对每种可能性进行单独的重载,用两种变体进行两次重载,或者用类型为std::variant
的参数只定义一次,该参数可以接受所有三种可能的std::function
s。据我所知,这些是我们目前的选项,不涉及创建从一个基派生的包装器类,这将再次使它看起来像std::variant
。
编辑时回答
据我所知,您希望将用户处理程序公开到包装/抽象层类下的框架中。如果是这样的话,那么脑海中会出现三种不会导致数据重复的解决方案(在本例中为处理程序指针):
- 如果框架允许,通过向框架提供访问处理程序的方法(例如,指向返回处理程序列表/数组的函数的指针),从包装类中公开处理程序(这是一种罕见的情况)
- 将"添加处理程序"请求直接传递给框架处理,然后通过框架提供的(让我们称之为)"处理程序"(例如函数、公共成员等)访问它们(在我看来,这应该是一个足够常见的选项)
- 将处理程序保留在全局范围列表/数组中(这通常不是一个好的做法,也可能不是框架的选项)
希望这能有所帮助。如果没有,那么唯一的其他事情就是将处理程序存储在包装类中,并将它们传递给框架,以便框架也能够使用它们。这反过来又会导致数据重复,但这是我脑海中唯一的其他选择,如果以上这些都不可能的话。
第二次编辑时的回答
请注意,这是概念的证明,可以使用一些改进。
请注意,您不会更改签名,而是函数体本身和成员数据(模式)。
我为每个模式添加了额外的size_t
,但如果不需要,您可以仅用vector<const char*>
或任何其他容器替换map<size_t, vector<const char*>>
。
例如,您也可以将const char*
替换为string
。
请记住,它需要能够存储typeid(...).name()
。
请记住,在定义模式时,它们需要相同的类型,也就是说,函数参数类型是否可以隐式转换为定义的参数类型。
#include <vector>
#include <map>
#include <typeinfo>
#include <functional>
using namespace std;
struct TypeCheckHelper {
private:
template<class T>
static bool _type_check_helper(vector<const char*>::const_iterator type)
{
return typeid(T).name() == string(*type);
}
template<class T1, class T2, class... Args>
static bool _type_check_helper(vector<const char*>::const_iterator type)
{
if (typeid(T1).name() != string(*type)) return false;
return TypeCheckHelper::_type_check_helper<T2, Args...>(++type);
}
public:
template<class... Args>
static bool type_check_helper(const vector<const char*>& types)
{
if (types.size() != sizeof...(Args)) {
return false;
}
return TypeCheckHelper::_type_check_helper<Args...>(types.cbegin());
}
template<class... Args>
static bool type_check_helper(vector<const char*>&& types)
{
if (types.size() != sizeof...(Args)) {
return false;
}
return TypeCheckHelper::_type_check_helper<Args...>(types.cbegin());
}
};
template<>
bool TypeCheckHelper::type_check_helper<>(vector<const char*>&& types)
{
return !types.size();
}
class InternalListClass { /* ... */ }; // e.g.: Windows' List implementation
class ExposedListClass {
public:
// ...
enum EventType_e {
CLICK,
DOUBLE_CLICK,
// ...
};
template<class... Args>
bool AddHandler(EventType_e event_type, function<void(Args...)> callback) {
for (const pair<size_t, vector<const char*>>& pattern : patterns[event_type]) /* For each pattern for this event type */
{
if (TypeCheckHelper::type_check_helper<Args...>(pattern.second)) /* Does it have the SAME (no implicit casting included) parameters */ {
/* Pattern matched */
switch (event_type) {
case CLICK:
// Register callback
/* OR */
switch (pattern.first) /* Pattern's ID */
{
case 0:
// Register callback as one type
break;
case 1:
// Register callback as another type
break;
// ...
default: /* Unknown pattern ID */
return false; /* OR throw */
}
break;
case DOUBLE_CLICK:
// ...
break;
// case ...:
// Register callback
default: /* Unknown event type */
return false; /* OR throw */
}
return true;
}
}
return false; /* Didn't match any of the patterns */
/* Possible to "throw" to indicate error too */
}
// ...
private:
static const map<EventType_e, map<size_t, vector<const char*>>> patterns;
// ...
};
/* Can not be statically initialized within the class */
const map<ExposedListClass::EventType_e, map<size_t, vector<const char*>>> ExposedListClass::patterns {
{
/* Event type */
CLICK,
{
/* Patterns */
{
0, /* Pattern ID */
/* Variant 1 */
{
/* No parameters */
},
},
{
1, /* Pattern ID */
/* Variant 2 */
{
typeid(int).name(), /* First Parameter */
},
},
{
2, /* Pattern ID */
/* Variant 3 */
{
typeid(int).name(), /* First Parameter */
typeid(double).name(), /* Second Parameter */
},
},
}
},
{
/* Event type */
DOUBLE_CLICK,
{
/* Patterns */
{
0, /* Pattern ID */
/* Only one variant */
{
typeid(int).name(), /* First Parameter */
}
}
}
},
// ...
};