Сохраните указатель на функцию и вызовите ее с безопасной проверкой типов в c ++ - PullRequest
0 голосов
/ 23 марта 2020

Я пытаюсь выяснить, как решить эту проблему наиболее элегантно, используя c ++ 17 (без повышения!). Я работаю над библиотекой, предоставляющей элементы управления пользовательским интерфейсом. Элементы управления должны иметь возможность реагировать на события, а пользователь (используя библиотеку) должен иметь возможность добавлять свои обработчики для этих событий. Я хочу избежать шаблона Command , поскольку это слишком сложно для пользователя - он просто хочет определить одну функцию и назначить ее для некоторого элемента управления события и пользовательского интерфейса. Вот некоторый пример кода:

// 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() выполняет свою работу, и событие обрабатывается (по крайней мере, с точки зрения пользователя).

  1. Проблема: для каждого события LisBox мне нужно определить отдельный вектор-обработчик .
  2. Проблема: каким должен быть тип второго аргумента метода ListBox::AddHandler?

Есть ли способ объединить эти обработчики, чтобы пользователь мог использовать только один функция AddHandler(), а не отдельные функции, такие как AddClickHandler() и AddEditHandler()? Конечно, типы функций должны проверяться статически, и о любых несоответствиях типов следует сообщать пользователю во время компиляции. Есть ли стандартный способ лечения всей этой концепции? Имейте в виду, что каждый элемент управления имеет свои события, и я считаю, что каждому событию может быть назначена функция-обработчик без аргументов. Спасибо.

РЕДАКТИРОВАТЬ

После рассмотрения возникла другая проблема. дизайн библиотеки выглядит так: это . События генерируются системой, которая затем вызывает предопределенные каркасные методы, из которых мне нужно вызывать обработчики пользователя (например, в Qt сигнал itemDoubleClicked(QListWidgetItem *item) испускается, когда элемент дважды щелкается внутри QListWidget). Следовательно, классу реализации (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) не нужно было перекомпилировать. Спасибо.

1 Ответ

1 голос
/ 23 марта 2020

Вы можете сделать отдельную перегрузку для каждой возможности, сделать две перегрузки с двумя вариантами или определить ее только один раз с аргументом типа std::variant, который может принимать все три возможных std::function с. Насколько я знаю, это наши текущие опции, которые не включают создание классов-обёрток, производных от одной базы, что опять-таки заставило бы его действовать как std::variant.

Ответ на правку

Насколько я понимаю ваш вопрос, вы хотите представить пользовательские обработчики каркасу под вашим классом слоя упаковки / абстракции. Если это так, то на ум приходят три решения, которые не приведут к дублированию данных (в данном случае указатели на обработчики):

  1. Предоставление обработчиков из класса обертки путем Фреймворк - способ доступа к ним (например, указатель на функцию, которая возвращает список / массив обработчиков), если это разрешено фреймворком (что встречается редко)
  2. Передача «обработчика добавления» запросы непосредственно обрабатываются платформой, а затем обращаются к ним с помощью (давайте назовем это) «дескриптора» (например, функции, publi c member, et c.), предоставляемой платформой (которая, по моему мнению, должна быть достаточно часто используется в качестве опции)
  3. Храните обработчики в глобальных списках / массивах (что обычно не является хорошей практикой и, вероятно, не является опцией для фреймворка)

Надеюсь это помогает. Если нет, то единственное, что нужно - это сохранить обработчики в классе-обёртке и передать их в каркас, чтобы он тоже мог их использовать. Это, в свою очередь, приводит к дублированию данных, но это единственный другой вариант, который приходит мне в голову, если вышеописанные варианты невозможны.

Ответ при втором редактировании

Обратите внимание, что это является доказательством концепции и может использовать некоторые уточнения. Обратите внимание, что вы не будете изменять сигнатуру , но тело самой функции и данные элемента (шаблоны). Я добавил дополнительные size_t для каждого шаблона, но при необходимости вы можете заменить map<size_t, vector<const char*>> на 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 */
                }
            }
        }
    },
    // ...
};
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...