【问题标题】:C Macro for type safe callbacks用于类型安全回调的 C 宏
【发布时间】:2018-05-31 04:58:32
【问题描述】:

我正在尝试用 c 创建一个非常简单的事件系统。我的界面是这样的:

typedef struct EventEmitter EventEmitter;

EventEmitter* emitter_create();
void emitter_subscribe(EventEmitter* emitter, void (*cb)(void*));
void emitter_publish(EventEmitter* emitter, void *payload);

一切正常,但是为了注册一个事件监听器,我需要提供一个函数指针,它接受一个 void *。

static void registerOnKeyDown(void (*cb)(void*)) {
  emitter_subscribe(keyDownEmitter, cb);
}

static void registerOnKeyUp(void (*cb)(void*)) {
  emitter_subscribe(keyUpEmitter, cb);
}

是否有任何方法,使用宏或其他方式,允许 EventEmitters 的用户提供类型化回调?比如:

void onKey(int keyCode) {
  printf("%d", keyCode);
}

代替:

void onKey(void *keyCode) {
  int code = (int)keyCode;
  printf("%d", code);
}

【问题讨论】:

  • 你觉得宏在这里有什么帮助?据我所知,你只有一个订阅功能,所以最终一切都必须遵守它的界面。
  • 我真的不知道。我是 c 的新手。我不确定是否有某种方法可以使订阅函数成为考虑类型的宏。即使我要这样做,我也不确定在发布事件时如何知道类型。
  • 是什么让您无法将第二个原型用于回调函数?
  • 你这是什么意思?
  • void onKey(void *keyCode) 没有错你不必用void onKey(int keyCode)

标签: c macros


【解决方案1】:

我最终通过在包装函数中根据需要简单地转换为 void (*cb)(void *) 来解决这个问题:

typedef void (*keyCallback)(int);
typedef void (*emitterCallback)(void*);

static void registerOnKeyDown(keyCallback cb) {
  emitter_subscribe(keyDownEmitter, (emitterCallback)cb);
}

【讨论】:

  • 演员表是大棒;您应该尽可能避免使用它们。宏也经常是大棒——尽可能避免使用它们。这些不是一揽子禁令;强制转换和宏都有合法用途。但是您应该尽量减少代码中两者的使用,尤其是强制转换。请注意,您尚未在问题的代码中为 emitter_create() 定义原型。您已经声明了该函数,但它可以接受任意数量的任何类型的参数(除了它不是像printf() 这样的可变参数函数)。避免非原型函数声明。
  • 也许你不...这是一个返回 EventEmitter * 的函数的声明,但它没有说明参数的数量或它们的类型。 C 不是 C++。要在 C 中说“这个函数不接受参数”,你必须写 EventEmitter *emitter_create(void);——现在你有了一个原型。为了对称,也用void 定义它。 (在 C++ 中,您编写的是一个不带参数的函数的原型。这种分歧有很强的历史原因。对于 C 标准的接受至关重要的是,无参数声明是开放式的,而不是原型。 )
  • 您显示的代码中没有宏,因此您知道您不需要宏。在我的书中,你应该根据回调类型来编写回调。如果那是void (*cb)(void *data),那么这就是函数的定义。是的,这意味着您执行int value = *(int *)data;(或int value = (int)data; — 这取决于数据在进行回调的站点如何传递)之类的操作,以从传递给回调的数据中获取值。请注意,这些是必要的演员表。 [...继续...]
  • […continuation…] 如果还需要直接使用回调,使用两个函数,一个用于常规使用,一个用于回调。回调函数解包参数并调用另一个函数,因此函数体只编写一次,回调只是一个“适配器”,它将回调约定适应其他函数的约定。
  • 是的——你或多或少是在正确的轨道上。不过,您应该以您希望避免的样式编写 onKey() 函数,然后不需要在答案中的代码中将函数指针转换为对 emitter_subscribe() 的调用。您可能(或可能不需要)需要keyCallback typedef;你的回调函数应该匹配emitterCallback typedef,所以不需要强制转换——但是registerOnKeyDown()函数的参数应该是emitterCallback而不是keyCallback
【解决方案2】:

如果您知道自己的类型是什么,则可以使用 C11 泛型选择来找出参数的类型,并将其作为枚举值提供。

#include <stdio.h>

typedef struct EventEmitter EventEmitter;
typedef void (*INT_CALLBACK)(int);
typedef void (*VOIDP_CALLBACK)(void *);

enum cbtype {
    _INT_CB,
    _VOIDP_CB
};

void _safe_emitter_subscribe(EventEmitter *emitter,
                             void (*callback)(),
                             enum cbtype type)
{
    printf("Registering a callback of type %d\n", type);
}

#define safe_emitter_subscribe(emitter, callback) \
    _safe_emitter_subscribe(                      \
        emitter,                                  \
        (void (*)())callback,                     \
        _Generic(callback,                        \
             INT_CALLBACK: _INT_CB,               \
             VOIDP_CALLBACK: _VOIDP_CB))

void func1(int a) {
}

void func2(void *a) {
}

int main(void) {
    safe_emitter_subscribe(NULL, func1);
    safe_emitter_subscribe(NULL, func2);
}

然后从枚举值中你会知道你需要如何再次转换函数:如果它是_INT_CB,它必须在调用之前重新转换为INT_CALLBACK_VOIDP_CBVOIDP_CALLBACK 等等。

【讨论】:

  • 这看起来可能是我正在寻找的东西。这可以与任意类型一起使用吗?我希望我的应用程序的不同子系统能够实例化发射器,这些发射器可以发出系统本身定义的自定义事件。
  • 复杂之处在于需要在通用选择函数中知道可能的类型:D
  • 是的,我就是这么想的。我想我现在会坚持选角。不过,我会阅读此内容。我还没有玩过 C11 功能。
  • @instantaphex 注意到这也进行了转换,您需要在调用者中重新转换它,但您可以用它做一些更有趣的事情。
【解决方案3】:

Software Engineering SE 上看到这个answer

鉴于您的 API:

typedef struct EventEmitter EventEmitter;

EventEmitter* emitter_create();
void emitter_subscribe(EventEmitter* emitter, void (*cb)(void*));
void emitter_publish(EventEmitter* emitter, void *payload);

如果您修改它以在该 API 上定义订阅宏,而不是推迟客户端代码,如下所示:

typedef struct EventEmitter EventEmitter;

EventEmitter* emitter_create();
void emitter_subscribe_impl(EventEmitter* emitter, void (*cb)(void*));
#define emitter_subscribe(emitter, xFunc) emitter_subscribe_impl((emitter), (void(*)(void*))(xFunc))

void emitter_publish_impl(EventEmitter* emitter, void *payload);
#define emitter_publish(emitter, xFunc) emitter_publish_impl((emitter), (void(*)(void*)(xFunc))

然后订阅者可以使用他们手头的类型调用它。与所有 API 宏一样,请确保完整记录预期的争论,以便消费者知道提供什么以及期望什么。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-07-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-29
    相关资源
    最近更新 更多