【问题标题】:Store function pointer and call it with safe type checking in c++存储函数指针并在 C++ 中使用安全类型检查调用它
【发布时间】:2020-03-23 08:24:02
【问题描述】:

我正在尝试找出如何使用 c++17 最优雅地解决这个问题(没有提升!)。我正在开发一个提供 UI 控件的库。控件应该能够对事件做出反应,并且用户(使用库)应该能够为这些事件添加他的处理程序。我想避免使用Command pattern,因为这对用户来说太复杂了——他只想定义一个函数并将其分配给一些事件和 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::Editenum 类型)事件发生时,它会触发需要两个参数的DoSomethingWithItemNames()。在那之后,DoSomeCleanup() 完成了它的工作并处理了事件(至少从用户的角度来看)。

  1. 问题:对于每个 LisBox 事件,我必须定义一个单独的处理程序向量。
  2. 问题:ListBox::AddHandler 方法的第二个参数的类型应该是什么?

有没有办法统一这些处理程序,让用户只能使用一个函数AddHandler(),而不是像AddClickHandler()AddEditHandler()这样的单独函数?当然,函数类型应该静态检查,任何类型不匹配都应该在编译时报告给用户。有没有标准的方法来处理整个概念?请记住,每个控件都有其事件,我的想法是,每个事件都可以分配一个不带参数的处理函数。谢谢。

编辑

经过考虑,又出现了一个问题。图书馆的设计看起来像this。事件由系统生成,然后调用预定义的框架方法,我需要从中调用用户的处理程序(例如,在 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 类)。谢谢。

【问题讨论】:

  • 您可以提供两个或更多AddHandlers - 用于功能一的每种风格。但是,为了防止 Event::Clickstd::function&lt;void(std::string, std::string)&gt; 结合使用,您需要一种不同的方法 - 两个 enums(每个只有一个值)。 (我曾在 gtkmm 中看到过这个技巧,以区分具有其他相同参数的构造函数。)您可以自行决定您更喜欢什么:AddClickHandler(f)AddHandler(ClickEvent, f)。恕我直言,这是个人品味的问题......
  • 我已经更新了我的答案。希望这会有所帮助,祝你好运。 :)
  • 你好。我再次更新了我的答案。第三次是他们所说的魅力。希望这一次,我能解决你的问题。 :)

标签: c++ events c++17 function-pointers variant


【解决方案1】:

您可以为每种可能性创建单独的重载,使用两个变体创建两个重载,或者使用std::variant 类型的参数定义一次,它可以采用所有三个可能的std::functions。 据我所知,这些是我们当前的选项,不涉及创建从一个基派生的包装类,这将再次使其表现得像 std::variant

编辑时回答

据我了解您的问题,您希望将用户处理程序公开给包装/抽象层类下方的框架。如果是这种情况,那么可以想到三种不会导致数据重复的解决方案(在这种情况下是处理程序指针):

  1. 在框架允许的情况下,通过为框架提供访问它们的方法(例如,指向返回处理程序列表/数组的函数的指针)从包装类中公开处理程序(这是一种罕见的事情)
  2. 将“添加处理程序”请求直接传递给框架处理,然后通过框架提供的(我们称之为)“句柄”(例如函数、公共成员等)(在我的作为一种选择,意见应该足够普遍)
  3. 将处理程序保留在全局范围的列表/数组中(这通常不是一个好的做法,可能不是框架的选项)

希望这会有所帮助。如果不是,那么唯一的另一件事是将处理程序存储在包装类中并将它们传递给框架以便它也能够使用它们。这反过来会导致数据重复,但如果上述选项不可行,这是我想到的唯一其他选择。

第二次修改时回答

请注意,这是概念验证,可以进行一些改进。
请注意,您不会更改签名而是函数体本身和成员数据(模式)。
我为每个模式添加了额外的size_t,但如果不需要,您可以将map&lt;size_t, vector&lt;const char*&gt;&gt; 替换为vector&lt;const char*&gt; 或任何其他容器。
例如,您也可以将const char* 替换为string
请记住,它需要能够存储typeid(...).name()
请记住,当模式被定义时,它们期望 same 类型,也就是函数参数类型,检查它们是否可以隐式转换为定义的参数类型。

#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 */
                }
            }
        }
    },
    // ...
};

【讨论】:

  • 感谢您的回答。我可能会选择单一定义,但首先我需要解决另一个问题,我将其添加到我的问题中,请您检查一下吗?提前致谢。
  • 跟进您的编辑:第一个选项是不可能的,因为我可能会实现包装器和它作为 pimpl 习惯用法之间的关系 - 那么包装器类不能有任何私有字段,除了 pimpl (指向实现的指针)。正如您已经提到的,第三个选项不是一个好习惯,所以我可能会选择第二个。完成后我会尽快接受您的答复,非常感谢!
  • 我实施了类似于您的第二点的解决方案。我编辑了这个问题,你能检查一下并给我一些见解吗?非常感谢。
猜你喜欢
  • 2020-08-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-14
相关资源
最近更新 更多