使用函数指针类,该函数指针可以在不同的实例化中调用多种类型的函数

Class with function pointer that can call multiple kind of functions in different instantiations

本文关键字:函数 指针 实例化 调用 种类 类型      更新时间:2023-10-16

我正在SDL中写一个类似按钮的东西,如果我与它交互,它可以做一些事情。我想将每个按钮存储在一个容器中(在列表或deque中),这样我就可以调用它们自己的draw函数,也可以检查我点击了哪一个。所以,我认为最简单的方法是从泛型类("button")实例化它们,这样所有按钮都会有一个位置,颜色等,还有一个函数指针,当我实例化它们时,我可以将其设置为自定义函数。

我的问题是,当我在类中定义函数指针时,我必须声明一个参数列表,但有时我需要一个不同的参数列表(例如:

MyButton button() 
button.fooptr=quit; //quit is a void function with no arguments
button.fooptr();
//another case
button.fooptr=print; //print is a void function which takes an SDL_Surface* as an argument.
button.fooptr(screen);

我曾尝试将函数指针定义为void (*fooptr)(...),如果我的函数有(…)作为参数,它会非常有效,但如果我尝试用fooptr(screen)调用函数,函数就看不到我传递给它的屏幕变量。有什么解决方案可以将自定义数量的参数传递给特定函数,而将零参数传递给没有参数的函数吗?

您描述的定义和调用函数的方式不是很安全。这是因为正如Oli在评论中指出的那样,很容易错误地调用函数。如果目标函数需要3个参数,而您最终只传递了1个,则会得到未定义的行为,任何事情都可能发生。在这种情况下,"任何事情"都将是坏事。

如果提前知道参数,可以使用std::functionstd::bind和参数的早期绑定以更安全的方式调用函数。这将允许您在您[应该]知道需要传递哪些参数的位置创建一个函数对象,绑定参数,然后设置fooptr

下面的示例使用问题中的代码,并使用std::bindstd::function对其进行扩展,以安全统一的方式调用函数。它还包括使用std::reference_wrapper允许通过引用传递参数的示例。

#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <functional>
struct SDLSurface 
{
    std::string str;
};
struct Button
{
    std::function<void(void)> fooptr;
};
void quit()
{
    std::cout << "called quit()" << std::endl;
}
void print(SDLSurface *surface)
{
    std::cout << "called print("" << surface->str << "")" << std::endl;
}
void print_string(const std::string& str)
{
    std::cout << "called print_string("" << str << "")" << std::endl;
}

int main()
{
    std::vector<std::unique_ptr<Button>> buttons;
    std::unique_ptr<Button> b;
    // Create a button and assign a function that takes no parameters
    b.reset(new Button());
    b->fooptr = quit;
    buttons.push_back(std::move(b));
    // Create a button and assign a function
    b.reset(new Button());
    SDLSurface  surface;
    b->fooptr = std::bind(print, &surface);
    buttons.push_back(std::move(b));
    // Create a button and assign a function taking a parameter by reference
    b.reset(new Button());
    std::string string_param;
    // Since we are passing by reference and want to bind to an existing variable
    // we need to use a reference wrapper.
    b->fooptr = std::bind(
        print_string,
        std::reference_wrapper<const std::string>(string_param));
    buttons.push_back(std::move(b));
    // Call all functions setting the value of string_param before we do.
    surface.str = "hello";
    string_param = "world";
    for(auto it = buttons.begin(); it != buttons.end(); ++it)
    {
        (*it)->fooptr();
    }
}

通常的C方法是使用一个void*参数,该参数可以为NULL,也可以指向内容所在的结构。被调用的函数将其强制转换为正确的结构类型并访问元素。

对于C++来说,std::bind和std::函数可能会对您有所帮助。

如果你真的想使用简单的函数,你可以检查一下va_arg的正确用法,(我认为)你至少需要一个参数才能开始访问其余的,而函数可能很难处理它们。请注意。。。仅限于POD参数类型。

按下按钮是一个应该包含基本信息的事件(看看现有框架/语言中的回调/事件)。基本按钮的界面应该只支持特定的用例。如果您需要某种特定类型的按钮来调用不同类型的函数,则需要以不同的方式处理。一种选择是使用std::bind来调整触发的事件(以最简单的形式,没有参数)和要调用的函数(注入曲面)的接口。

struct Button {
   std::function<void()> callback;
   void pressed() { callback(); }
   void register(std::function<void()> const &cb) {
      callback = cb; 
   }
};
struct ButtonWithSurface : Button {
...
    SDLSurface *surface;
    void register(std::function<void(SDLSurface*)> const &cb) {
       Button::register(std::bind(cb,surface);
    }

对于基本Button的所有用户,可以执行的操作是按下按钮,尽管在ButtonWithSurface的情况下,这将触发对采用SDLSurface*参数的函数的调用。尽管如此,用户不需要知道那个细节,因为这只是在使用具体类型完成的回调注册过程中处理的。

如果您不熟悉闭包(也不想了解它们),请定义函数对象:

class BtnFunctional {
public:
    virtual void Call () = 0;
    virtual ~BtnFunctional () {
    }
};
class QuitBtnFunctional : public BtnFunctional {
public:
    virtual void Call () {
        // call quit function
    }
};
class PrintBtnFunctional : public BtnFunctional {
private:
    ScreenType * theScreen;
public:
    PrintBtnFunctional (ScreenType * screen) : theScreen (screen) {
    }
    virtual void Call () {
        // call print function, e.g. print (theScreen);
    }
};
// ...
btn.fooptr = new QuitBtnFunctional ();
btn.fooptr.Call ();
btn.fooptr = new PrintBtnFunctional (screen);
btn.fooptr.Call ();

小心:对象应该被销毁在某个地方,也就是说,你应该使用智能指针来存储它们!