【问题标题】:Is there a way to modify class state from a function pointer passed to a C API?有没有办法从传递给 C API 的函数指针修改类状态?
【发布时间】:2015-05-16 11:45:48
【问题描述】:

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

这个类看起来像这样:

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

关于 GLFW 的背景

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

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

要将回调函数绑定到窗口,请使用名为 glfwSetKeyCallback 的函数。

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

我遇到的问题

我想通过一个关键回调函数修改我的类的内部状态,而不依赖于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)

我尝试过的

  • 不能将私有成员函数传递给 GLFW API。考虑到成员函数需要不可见的 this 参数,这是可以理解的。
  • 我也尝试使用 lambda 来捕获我想要的状态,但这也没有用。

有没有一种方法可以在不依赖全局状态的情况下从传递给 C API 的函数修改类状态?就像在这种情况下,从传递给glfwSetKeyCallback的函数中增加_member_variable

【问题讨论】:

  • 问题恰恰出在this 参数上,而且c++ 修改了函数名以允许函数重载这一事实,所以我认为如果可能的话,传递一个c++ 类是非常困难的成员函数,就好像它是一个 c 回调一样。当然有一些选择,AFAIK 新的 c++ 标准使做这类事情变得更容易,但我不是专家。
  • 只要 c-api 不提供传递用户数据的方法(如this),这几乎是不可能的。您可以维护GLFWwindow* 指针的静态映射作为键和指向相应类实例的指针。但这恐怕不容易维护。
  • @elias 如果你回答了这个问题,你会得到很多答案:当一个键被按下时,你将/如何找到成员函数的对象叫什么?

标签: c++ c class callback global-variables


【解决方案1】:

是的,这是可能的。该解决方案是 GLFW 的 API 支持的解决方案。

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

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

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

编辑

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

  • 不能将私有成员函数传递给 GLFW API。考虑到成员函数需要不可见的 this 参数,这是可以理解的。
  • 我也尝试使用 lambda 来捕获我想要的状态,但这也没有用。

确实可以传递一个私有的 static 成员函数,正如我和@derhass 所描述的(几乎同时)。但是,我确定您的意思是非静态成员函数。正如您所观察到的,您需要this 指针才能分派到正确的对象。看起来您可以使用glfwSetWindowUserPointer 设置this 指针,然后传递pointer to a private non-static member function。但是,这行不通。指向成员函数的指针通常被实现为“胖”指针,除了地址之外还有额外的数据。例如,请参阅此discussion。如果您设法将一个转换为普通指针并将其传递给 C 库,您将以一种或另一种方式截断。即使您可以避免强制转换的截断,我认为您做不到,C 库也没有足够的存储空间来存储胖指针,并且无法将其原封不动地返回给您。

同样,lambda 也是如此。

【讨论】:

    【解决方案2】:

    此答案特定于 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”类中并从中派生,覆盖各种 onEvent 处理程序,而无需关心 C-API GLFW 了。在您的对象中创建(或分配给)窗口之后,在注册 GLFW 回调之前,您只需 glfwSetWindowUserPointer(_window, this);

    【讨论】:

      【解决方案3】:

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

      注意线程问题。

      另外,预先考虑窗口创建和窗口销毁的责任是一个好主意。 GUI 窗口通常是自毁的,很可能你的 C 窗口 API 也是这样。我或多或少地了解了启动 API 级别自毁的技术,并让它导致 C++ 对象的破坏,而不是相反,因为产生最简单的代码,但无论你选择什么,真正思考是个好主意在做出设计选择之前,请提前了解这一点。

      【讨论】:

        【解决方案4】:

        我可能是错的,但我不明白如果没有 一些 全局状态,你如何能够完成你想要做的事情。您可以做的是创建一个全局变量来存储指向您的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 Object
        {
            ...
            friend void key_callback(GLFWwindow *window, int key, int scancode, int action, int mods);
        };
        

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-07-25
          • 1970-01-01
          • 2013-04-28
          相关资源
          最近更新 更多