【问题标题】:Callback to non-static method回调非静态方法
【发布时间】:2010-01-14 20:58:08
【问题描述】:

想想您的基本 GLUT 程序。它们只是从一个主方法运行并包含像 `glutMouseFunc(MouseButton) 这样的回调,其中 MouseButton 是方法的名称。

我所做的是将主文件封装到一个类中,这样MouseButton就不再是一个静态函数而是有一个实例。但是这样做会给我一个编译错误:

错误 2 错误 C3867:'StartHand::MouseButton':函数调用缺少参数列表;使用 '&StartHand::MouseButton' 创建指向成员 c:\users\angeleyes\documents\visual studio 2008\projects\capstone ver 4\starthand.cpp 388 IK Engine 的指针

由于类非常庞大,无法提供代码示例。

我尝试过使用this->MouseButton,但这给出了同样的错误。不能给回调一个实例函数的指针吗?

【问题讨论】:

  • 您是否阅读了错误信息?

标签: c++ opengl glut


【解决方案1】:

正如错误消息所说,您必须使用&StartHand::MouseButton 语法来获取指向成员函数(ptmf)的指针;这只是语言的一部分。

当使用 ptmf 时,您正在调用的函数(在这种情况下为 glutMouseFunc)也必须期望获得 ptmf 作为回调,否则使用非静态 MouseButton 将无法工作。相反,一种常见的技术是让回调与用户提供的void* 上下文一起工作,该上下文可以是实例指针——但执行回调的库必须明确允许此参数。确保与外部库(下面的 handle_mouse 函数)预期的 ABI 匹配也很重要。

由于 glut 不允许允许用户提供上下文,您必须使用另一种机制:将您的对象与 glut 的当前窗口相关联。但是,它确实提供了一种获取“当前窗口”的方法,我使用它来将void* 与窗口相关联。然后你只需要创建一个trampoline 来进行类型转换并调用方法。

机械:

#include <map>

int glutGetWindow() { return 0; } // make this example compile and run  ##E##

typedef std::pair<void*, void (*)(void*,int,int,int,int)> MouseCallback;
typedef std::map<int, MouseCallback> MouseCallbacks;
MouseCallbacks mouse_callbacks;
extern "C" void handle_mouse(int button, int state, int x, int y) {
  MouseCallbacks::iterator i = mouse_callbacks.find(glutGetWindow());
  if (i != mouse_callbacks.end()) { // should always be true, but possibly not
                                    // if deregistering and events arrive
    i->second.second(i->second.first, button, state, x, y);
  }
}

void set_mousefunc(
  MouseCallback::first_type obj,
  MouseCallback::second_type f
) {
  assert(obj); // preconditions
  assert(f);
  mouse_callbacks[glutGetWindow()] = MouseCallback(obj, f);
  //glutMouseFunc(handle_mouse); // uncomment in non-example  ##E##
  handle_mouse(0, 0, 0, 0); // pretend it's triggered immediately  ##E##
}

void unset_mousefunc() {
  MouseCallbacks::iterator i = mouse_callbacks.find(glutGetWindow());
  if (i != mouse_callbacks.end()) {
    mouse_callbacks.erase(i);
    //glutMouseFunc(0); // uncomment in non-example  ##E##
  }
}

例子:

#include <iostream>

struct Example {
  void MouseButton(int button, int state, int x, int y) {
    std::cout << "callback\n";
  }
  static void MouseButtonCallback(
    void* self, int button, int state, int x, int y
  ) {
    static_cast<Example*>(self)->MouseButton(button, state, x, y);
  }
};

int main() {
  Example obj;
  set_mousefunc(&obj, &Example::MouseButtonCallback);

  return 0;
}

请注意,您不再直接调用 glutMouseFunc;它作为 [un]set_mousefunc 的一部分进行管理。


以防万一它不清楚:我已经重写了这个答案,所以它应该对你有用,这样它就可以避免 C/C++ 链接问题被争论。它将按原样编译和运行(没有 glut),它应该与 glut 一起工作,只需稍作修改:注释或取消注释标记为 ##E## 的 4 行。

【讨论】:

  • 你不应该使用静态函数作为回调。从技术上讲,该库需要一个 C-Function。静态成员方法不是 C-Functions,你很幸运你的编译器对两者都使用了相同的 ABI。没有要求它这样做。只是为了磨合它,使用高度优化的 ABI 的更昂贵的编译器(并在寄存器中而不是在堆栈中传递参数)将与以下代码中断。
  • 其实这个例子是标准的 C++ 并且保证在 C++ 中工作。问题在于语言和编译器互操作,您的观点在这些情况下是有效的。 OpenGL 正是在这种情况下。然而,在声明函数时完全回答如何指定编译器/平台特定的 ABI 超出了这个问题的范围。
  • 我发布了一个问题(链接到后续),以便您可以更彻底地讨论 ABI 问题。我对此非常感兴趣,因为我经常使用静态方法来完成 Roger 的“蹦床”技巧(呵呵,我喜欢这个词,Roger)。 stackoverflow.com/questions/2068022/…
  • 我没有说出它的名字。它指的是任何目的是委托给另一个函数的函数,在某些情况下,它指的是一个专门为此优化的特定程序集构造。
  • Martin:我相信你会在一些编译器中引起问题,但现在我很好奇是哪些。愿意分享吗?
【解决方案2】:

不,不能将指向实例函数的指针提供给期望具有特定签名的函数指针的回调函数。他们的签名不同。它不会编译。

通常,此类 API 允许您将 void* 作为“上下文”参数传入。你在那里传递你的对象,并编写一个将上下文作为回调的包装函数。包装器将其转换回您正在使用的任何类,并调用适当的成员函数。

【讨论】:

    【解决方案3】:

    您不能将静态回调替换为实例回调。当调用者调用您的回调时,它会在哪个实例上调用?换句话说,调用者如何传入正式的 'this' 参数?

    解决方案是有一个静态回调存根并将实例作为参数传递,这意味着被调用者必须接受一个任意的 pvoid,该 pvoid 将在调用回调时传回。然后可以在存根中调用非静态方法:

    class C {
        void f() {...}
        static void F(void* p) {
          C* pC = (C*)p;
          pC->f();
        }
      }
    
      C* pC = ...;
      someComponent.setCallback(&C::F, pC);
    

    【讨论】:

      【解决方案4】:

      与大家似乎所说的相反,您绝对可以使用非静态成员函数作为回调方法。它需要专门为获取指向非静态成员的指针而设计的特殊语法,以及在类的特定实例上调用该函数的特殊语法。有关所需语法的讨论,请参阅 here

      以下是说明其工作原理的示例代码:

      #include <cstdlib>
      #include <string>
      #include <iostream>
      #include <vector>
      #include <sstream>
      #include <algorithm>
      using namespace std;
      
      
      class Operational
      {
      public:
          Operational(int value) : value_(value) {};
      
          string FormatValue() const ;
      
      private:
          int value_;
      
      };
      
      string Operational::FormatValue() const
      {
          stringstream ss;
          ss << "My value is " << value_;
          return ss.str();
      }
      
      typedef string(Operational::*FormatFn)() const; // note the funky syntax
      
      Operational make_oper(int val)
      {
          return Operational(val);
      }
      
      int main()
      {
          // build the list of objects with the instance callbacks we want to call
          Operational ops[] = {1, 2, 3, 5, 8, 13};
          size_t numOps = sizeof(ops)/sizeof(ops[0]);
      
          // now call the instance callbacks
          for( size_t i = 0; i < numOps; ++i )
          {
              // get the function pointer
              FormatFn fn = &Operational::FormatValue;    
      
              // get a pointer to the instance
              Operational* op = &ops[i];  
      
              // call the callback on the instance
              string retval = (op->*fn)();
      
              // display the output
              cout << "The object @ " << hex << (void*)op << " said: '" << retval << "'" << endl;
          }
      
      
          return 0;
      }
      

      当我在我的机器上运行这个程序时,它的输出是:

      The object @ 0017F938 said: 'My value is 1' 
      The object @ 0017F93C said: 'My value is 2' 
      The object @ 0017F940 said: 'My value is 3' 
      The object @ 0017F944 said: 'My value is 5' 
      The object @ 0017F948 said: 'My value is 8' 
      The object @ 0017F94C said: 'My value is 13'
      

      【讨论】:

      • 大概 OP 无法控制库来修改它以使用 ptmfs,所以如果它还不支持它们,那么它们就不能使用。由于我有最古老的答案并且已经说明了这一点,我不确定您所说的“与每个人似乎在说什么相反”是什么意思。
      • @Roger Pate:有人误解了——要么是我,要么是 OP,要么是其他受访者。我的理解是,OP 在问“成员函数可以用作回调函数”,而其他人都说“不”。我很可能误解了 OP 和受访者。
      • @Roger - 您的示例显示了使用静态函数的绑定。所以我不认为你的答案是正确的。您需要使用 ::* 将 ptr 获取到任何对象实例的方法。
      • 科坦:你什么意思? ::* 不是运算符。我的答案怎么不正确?
      • 约翰:这就是我最初想要评论的,我从来没有说过“不”,但是“如果你使用的 api 支持它,你可以直接使用 ptmf,但因为它是最可能 api 不支持它们,这里是 ..."
      【解决方案5】:

      在这种情况下,您不能使用非静态成员函数。 基本上 glutMouseFunc 期望的参数类型是

      void (*)(int, int, int, int)
      

      而你的非静态成员函数的类型是

      void (StartHand::*)(int, int, int, int)
      

      第一个问题是类型并不真正匹配。 其次,为了能够调用该方法,回调必须知道您的方法属于哪个对象(即“this”指针)(这就是首先类型不同的原因)。 第三,我认为您使用错误的语法来检索方法的指针。正确的语法应该是:&StartHand::MouseButton。

      因此,您必须将该方法设为静态或使用其他静态方法,该方法会知道使用哪个 StartHand 指针来调用 MouseButton。

      【讨论】:

        【解决方案6】:

        以下在 c++ 中用于定义 c 回调函数,例如在您只需要此类的单个实例时使用 glut(glutDisplayFunc、glutKeyboardFunc、glutMouseFunc ...)时很有用:

        MyClass * ptr_global_instance = NULL;
        
        extern "C" void mouse_buttons_callback(int button, int state, int x, int y) {
        
            // c function call which calls your c++ class method
        
            ptr_global_instance->mouse_buttons_cb(button, state, x, y);
        }
        
        void MyClass::mouse_buttons_cb(int button, int state, int x, int y) {
        
            // this is actual body of callback - ie.  if (button == GLUT_LEFT_BUTTON) ...
            // implemented as a c++ method
        }
        
        void MyClass::setup_glut(int argc, char** argv) { // largely boilerplate glut setup
        
            glutInit(&argc, argv);
        
            // ... the usual suspects go here like glutInitWindowSize(900, 800); ...
        
            setupMouseButtonCallback(); // <-- custom linkage of c++ to cb
        
            // ... other glut setup calls here
        }
        
        void MyClass::setupMouseButtonCallback() {
        
            // c++ method which registers c function callback
        
            ::ptr_global_instance = this;
            ::glutMouseFunc(::mouse_buttons_callback);
        }
        

        在您的 MyClass 标题中,我们添加:

        void mouse_buttons_cb(int button, int state, int x, int y);
        
        void setupMouseButtonCallback();
        

        这也适用于使用相同的逻辑流程来设置您的过剩 调用 glutDisplayFunc(display)

        【讨论】:

          猜你喜欢
          • 2013-03-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-09-08
          • 2016-10-04
          相关资源
          最近更新 更多