C ++ API design: использование void * плохая идея? - PullRequest
0 голосов
/ 26 апреля 2018

Я работаю над API, который мне нужен как можно более универсальный для вызывающей стороны. Основная идея проекта заключается в предоставлении реализации типа сигнал / слот, которая позволяет пользователю API подписаться на данный набор событий и прикрепить к ним определенные пользователем обратные вызовы.

Открытый интерфейс выглядит примерно так:
RetCallback subscribe(EventEnum& ev, std::function<void(void*)> fn) const; : обратите внимание на подпись void(void*) здесь. EventEnum указывается в общедоступном заголовочном файле, а также в определении типов.

Внутренние разработки API будут затем уведомлять своих подписанных наблюдателей о событии с помощью метода уведомления и предоставлять данные для пересылки клиенту:

void dummyHeavyOperation() const {
    std::this_thread::sleep_for(2s);
    std::string data = "I am working very hard";
    notify(EventEnum::FooEvent, &data);
}

Клиент подписывается и преобразует данные в (задокументированный) тип следующим образом:

auto subscriber = Controller->subscribe(EventEnum::FooEvent, callback);

, где

void callback(void* data) {
    auto* myData = (std::string*) data;
    std::cout << "callback() with data=" << *myData << std::endl;

    /// Do things
}

Это разумный замысел или это осуждается? Что говорит ваш опытный современный разработчик C ++?

[EDIT]
Я должен также добавить, что API поставляется в виде разделяемой библиотеки, загружаемой во время выполнения. Так что любое соединение времени компиляции (и генерация кода, если я не ошибаюсь) не обсуждается

Спасибо!

Ответы [ 5 ]

0 голосов
/ 26 апреля 2018

Вы можете избежать void * в API, используя шаблон для функций подписки и уведомления.

enum event {

    fooEvent,

};

type_index p(typeid(void));
void(*pf)(void*);

template<typename T> void Notify(event e, T& data)
{
    auto current_p = type_index(typeid(T));
    if (current_p == p)
        reinterpret_cast<void(*)(T&)>(pf)(data);

}

template<typename T> void Subscribe(event e, void(*f)(T&))
{
    p = type_index(typeid(T));
    pf = reinterpret_cast<void(*)(void*)>(f);
}

Затем в своем клиентском коде вы можете написать без использования преобразования из void:

void handler(string& d)
{


}

void test1()
{
    Subscribe(fooEvent, handler);
    ...
    string data = "hello";
    Notify(fooEvent, data);

}

Используя type_info / type_index, вы можете проверить во время выполнения, соответствует ли тип параметра. Внутренне вы можете продолжать использовать void *.

0 голосов
/ 26 апреля 2018

Да, void* - плохая идея. Тем более, когда задействованные типы исходят от вас, а не от пользователя!

Если различные события передают данные разных типов клиенту, то обеспечение безопасности типов очень полезно для пользователя. Это предотвращает такие вещи, как случайная передача обратного вызова, который ожидает строку, но вызывается с двойным. Вы хотите, чтобы вашим API было трудно злоупотреблять.

Например, вы можете сделать это:

template<class T>
RetCallback subscribe(EventEnum& ev, std::function<void(T)> fn) const;

Абоненты будут указывать тип на сайте вызова:

auto subscriber = Controller->subscribe<std::string>(EventEnum::FooEvent, callback);

Затем вы можете проверить subscribe, подходит ли EventNum к этой сигнатуре обратного вызова, или вы даже можете (в зависимости от того, сколько у вас событий и типов данных обратного вызова) иметь разные EventNum типы для каждого типа данных обратного вызова, так что невозможно даже подписаться на вызов с несовпадающим типом события и сигнатурой обратного вызова, например: https://godbolt.org/g/7xTGiM

notify должно быть сделано аналогично subscribe.

Таким образом, любое несоответствие либо невозможно (т. Е. Принудительно скомпилировано), либо сразу обнаруживается в вашем API, а не вызывает неожиданные сбои приведения в дальнейшем в коде пользователя.

Редактировать: Как обсуждалось в комментариях, если закрепление пользователя по значениям события во время компиляции в порядке, вы можете даже создать шаблон для самого номера события: https://godbolt.org/g/9NYVh3

0 голосов
/ 26 апреля 2018

Я работаю над базовой системой событий для c ++, и я использую void* для обработчиков событий, когда они вызывают объект, который вызвал событие и дополнительные данные события. Так что запуск общего события выглядит так:

void Invoke(void* sender, void* eventArgs);

С такой подписью обработчика событий пользователь API может разыграть

void* sender

к исходному объекту, потому что он всегда знает, какой тип объекта вызвал событие. По крайней мере, это моя реализация системы событий в C ++. Вы можете посмотреть это здесь: https://github.com/moritzrinow/MindEvent Я также реализовал базовый класс для дополнительных данных событий «MindEventArgs», который вдохновлен .NET «EventArgs».

Итак, чтобы ответить на ваш общий вопрос ... Я думаю, что void * почти неизбежен для универсальных API, которые должны быть совместимы со старыми версиями C ++. :)

0 голосов
/ 26 апреля 2018

Буквально все лучше, чем void*. Это неприятный хак из-за того, что люди все еще изучали то, что люди хотят от языка и как выполнять различные задачи, были только жизнеспособны с ним.

Идея непрозрачного указателя гораздо более понятна и понятна для API, который должен использоваться для C или C ++; или даже только один. Это обеспечивает безопасность типов и легко читается.

В заголовочном файле API у вас есть:

// forward declare a struct called MyThing, but make sure the contents of it aren't available so that we can do anything we want to it in the implementation.
struct MyThing;

// define a class that contains function pointers.  We have a requirement that these function pointers must be valid for the lifetime of them being registered with MyThing.
struct MyThingCallback {
    void (*eventA)(MyThing* sender, const char* someData);
    void (*eventB)(MyThing* sender, int someOtherData);
};

// Some helper functions to do things with MyThing
MyThing* createMyThing();
void deleteMyThing(MyThing*);
void registerCallback(MyThing*, MyThingCallback);

int getSomeProperty(const MyThing*);
void setSomeProperty(MyThing*, int);

В реализации (которая включает в себя этот файл) у вас есть

struct MyThing {
private:
    int property;
    std::vector<MyThingCallback> callbacks;
    // functions are allowed here
};

Вы заметите, что пользователю API не нужно ничего знать о MyThing, потому что он никогда не имеет доступа к его членам; они только когда-либо используют указатель на это. Идея void* или даже различных объектов, передаваемых для разных событий, теперь удалена, и каждый всегда может быть уверен, что их функция будет использоваться только в нужное время.

0 голосов
/ 26 апреля 2018

C ++ API design: использование void * плохая идея?

Да.

Для реализации эквивалентного API вы должны использовать std::any, что являетсяпроверенная версия типа стирания.std::any доступно только в стандартной библиотеке, начиная с текущей стандартной версии C ++ 17.Если у вас нет C ++ 17 (или вы не хотите, чтобы пользователь API зависел от C ++ 17), вы можете вместо этого использовать нестандартную реализацию.

Альтернативой являетсяне стирайте тип аргумента вообще, а используйте шаблоны полностью вместо этого.См. Boost Signals для примера такого API обратного вызова

...