有没有办法从传递给 C API 的函数指针修改 class 状态?

Is there a way to modify class state from a function pointer passed to a C API?

我有一个 class 有一些内部状态。 class 使用名为 GLFW 的 C 库来处理 window 管理和键盘输入。

class 看起来像这样:

class Object {
public:
    Object(...);
    ...
private:
    int _member_variable; // Internal state
    GLFWwindow *_window;
    ...
}

关于 GLFW 的背景

GLFW 是一个处理 window 处理和键盘输入的 C 库。当 window 获得焦点时用户按键时,它使用回调函数来报告。

void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods)
{
    printf("User pressed key #%d\n", key);
}

要将回调函数绑定到 window,您可以使用名为 glfwSetKeyCallback.

的函数
// This will bind the `key_callback` function to `window`.
glfwSetKeyCallback(window, key_callback);

我遇到的问题

我想通过键回调函数修改 class 的内部状态,而不依赖于 static 存储的状态。我不能拥有全局状态,因为我希望能够创建 Object 的多个实例并让它们彼此独立工作。

假设在这种情况下,我想在每次用户按键时递增 _member_variable

我想做这样的事情:

// Private method for dealing with key presses.
void Object::key_callback(GLFWwindow *window, int key, int scancode, int action, int mods)
{
    if (action == GLFW_PRESS)
        ++_member_variable; // Increment the internal state
}

Object::Object(GLFWwindow *window, ...)
: _window(window), ...
{
    glfwSetKeyCallback(window, key_callback);
}

但这不起作用,因为 Object::key_callbackObject 的成员函数并且需要不可见的 this 参数。 (Object::*)(GLFWwindow *, int, int, int, int) 无法转换为 (*)(GLFWwindow *, int, int, int, int).

我试过的

有没有办法在不依赖全局状态的情况下从传递给 C API 的函数修改 class 状态?就像在这种情况下,从传递给 glfwSetKeyCallback?

的函数递增 _member_variable

回调的第一个参数是指向 GLFWwindow 的指针。您可以使用它来识别您的 C++ window 对象,例如通过 std::unordered_map (然后需要从静态存储中获取或引用)。然后将调用传递给合适的方法。

注意线程问题。

此外,预先考虑 window 创建和 window 销毁的责任是个好主意。 GUI windows 通常是自我毁灭的,很可能你的 C windowing API 也是这样。我或多或少地采用了启动 API 级自毁的技术,并让它导致 C++ 对象的破坏,而不是相反,因为产生最不复杂的代码,但无论你选择什么,它都是一个很好的选择在做出设计选择之前真正考虑这一点的想法。

我可能是错的,但我看不出在没有 some 全局状态的情况下如何完成你想要做的事情。您 可以 做的是创建一个全局变量来存储指向您的 Object 实例的指针,然后在 key_callback 运行时调用它们的成员函数。这样一来,您至少可以控制损失,因为您将只有一个全局变量,这样您就不必将 Object 的每个实例都设为全局变量。

这里有一些代码演示了我要说的内容:

// the only global variable in the code:
std::list<Object *> registeredObjects;

// register your callback to a free function instead of a member function
void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods)
{
    // however, make this function call the member function of your Objects
    for (Object *registeredObject : registeredObjects)
        registeredObject->key_callback(window, key, scancode, action, mods);
}

// call this function to make your Objects able to react to key presses
void registerKeyCallback(Object *objectToRegister)
{
    for (Object *obj : registeredObjects)
        if (obj == objectToRegister) // is the given object already registered?
            return; // yes, so don't register again
    registeredObjects.push_back(objectToRegister);
}

有了这段代码,您可以调用 registerKeyCallback 函数,传入您希望能够对按键做出反应的 Object 地址,然后 Object 将能够在对按键做出反应时更改其自身的内部状态。如果 Object::key_callback 函数是私有的,那么你可以让 key_callback 函数成为你的 class 的朋友,就像这样:

class Object
{
    ...
    friend void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods);
};

然后,key_callback 函数将能够调用私有 Object::key_callback 函数。

此答案特定于 GLFW 库,并不试图回答更一般的编程语言问题。但是,您可能会发现这很有用,因为它解决了您真正想要解决的问题。

GLFW 确实支持一种允许通过 user pointer which can be specified per window.

将用户特定数据传递给回调的方法

一些使用方法可能是这样

class Object {
public:
    Object(...);
    ...
private:
    static void keyboard_callback_glfw(GLFWwindow *win_glfw, int key, int scancode, int action, int mods); // raw GLFW callback
    int _member_variable; // Internal state
    GLFWwindow *_window;
    ...
protected:
    void onKeyPress(int key, int scancode, int action, int mode); // object-oriented callback that does the work
}

void Object::keyboard_callback_glfw(GLFWwindow *win, int key, int scancode, int action, int mode)
{
     Object *obj=static_cast<Object*>(glfwGetWindowUserPointer(win));
     // call the Objects onKey() handler
     obj->onKeyPress(key, scancode, action, mode);
}

这个方案也适用于继承,所以你基本上可以将 GLFW 及其所有回调包装在一些 "Window" class 中并从中派生,覆盖各种 onEvent 处理程序,无需再关心 GLFW 的 C-API。您只需 glfwSetWindowUserPointer(_window, this); 在 window 创建(或分配给)您的对象之后,在注册 GLFW 回调之前。

是的,这是可能的。此解决方案是 GLFW API.

支持的解决方案

典型的 C 库允许您在注册回调时将不透明的指针传递给用户数据。 glfwSetKeyCallback 没有。

相反,在 GLFW 中,您调用 glfwSetWindowUserPointerGLFWWindow 中设置一个用户指针字段。在 Object 构造函数中或实际创建 window 的任何地方将其设置为 this。然后,将静态成员函数添加到您的 Object class。这个函数可以是私有的。将其作为回调传递给 glfwSetKeyCallback。调用此回调时,使用 glfwGetWindowUserPointerGLFWWindow 检索用户指针,将其转换为 Object*,并使用它来分派对该对象的非静态成员函数的调用或修改直接数据成员。

http://www.glfw.org/docs/latest/window.html#window_userptr

编辑

为了使这个答案更完整,让我评论一下您尝试解决此问题的方法。

  • It is not possible to pass a private member function the GLFW API. That is understandable considering that the member functions require the invisible this argument.
  • I have also tried using a lambda to capture the state I want but that also did not work.

确实可以传递私有 static 成员函数,正如我和@derhass 所描述的(几乎同时)。但是,我确定您的意思是非静态成员函数。如您所见,您需要 this 指针来分派到正确的对象。看起来您可以使用 glfwSetWindowUserPointer 设置 this 指针,然后传递 pointer to a private non-static member function. This won't work, however. Pointers to member functions are often implemented as "fat" pointers with extra data besides just an address. For example, see this discussion。如果您设法将一个转换为普通指针并将其传递给 C 库,您将以某种方式被截断。即使您可以避免被强制转换截断(我认为您做不到),C 库也没有足够的存储空间来存储胖指针,并且无法 return 将它原封不动地提供给您。

同样,lambdas 通常也是如此。