带/不带类的C++回调函数指针

12
我卡住了。我试图编写一个函数,可以接受来自类和对象的无类函数指针。以下是我的当前代码,希望能更好地解释。

(它应该在Arduino上运行,所以我不能使用大型库。)

首先,我正在使用这个Arduino库:

/* SimpleTimer - A timer library for Arduino.
 * Author: mromani@ottotecnica.com
 * Copyright (c) 2010 OTTOTECNICA Italy
 */

这个函数接受一个函数作为参数,在一定时间间隔内循环调用该函数:

typedef void (*timer_callback)(void);
就我所知,这是一个无类函数。网页指向成员函数的指针让我受益匪浅,但还不够深入。可能是我的术语不足造成的。
现在,我已经制作了自己的类,并希望使用此SimpleTimer库。但如果我把我的类函数传递给SimpleTimer,它就不喜欢它们(据我理解)。那么,在不改变SimpleTimer库的情况下,如何实现这一点呢?
因此,有一个名为Robot的类,其中包含Robot::halt()。我想让机器人向前移动一定时间。就像这样:
void Robot::forward(int speed, long time) {
    reset();
    timer.setTimer(time, c_func, 1);

    analogWrite(l_a, speed);
    analogWrite(r_a, speed);
    isMoving(true);
}

void Robot::halt() {
    __isMoving = false;
    digitalWrite(r_a, LOW);
    digitalWrite(r_b, LOW);
    digitalWrite(l_b, LOW);
    digitalWrite(l_a, LOW);
}

c_func变量在这一点上是一个无类函数,但我想使用Robot::halt函数。 我已经查找、阅读、学习过了,但还没有成功。 我似乎无法理解这个问题,因为我缺少某些角度。

我尝试过:

timer.setTimer(time, (this->*halt), 1);
timer.setTimer(time, Robot::*halt, 1);
timer.setTimer(time, &Robot::halt), 1);

但这只会导致同样的问题/我在这里瞎猜...

编辑

早些时候,我说不想改变SimpleTimer库的代码。我想重新考虑这个问题,我想改变它似乎是更好的选择。

感谢所有当前的答案,我只被允许标记其中一个为可行答案,实际上我读到的每一条都非常有帮助。

为了继续下去,改变SimpleTimer代码。这个类需要引用持有我的“halt”函数的对象,对吗?所以,重载settimer函数以采用我的对象和我的函数作为两个单独的指针会起作用……?我想我已经理解了这个,但是我的脑子还没有完全明白。

编辑

我不知道这又是谁想出来的,但是任何找到这个线程的人都可以看到成员函数指针和最快的C++委托,它提供了一个非常好的函数指针和成员函数指针介绍。

编辑

搞定了,改变了SimpleTimer库以使用这个Delegate系统:http://www.codeproject.com/KB/cpp/FastDelegate.aspx

它整合得非常好,将一个标准的Delegate系统像这样放在Arduino库中可能很不错。

代码如下(工作)

typedef

typedef FastDelegate0<> FuncDelegate;

机器人类中的代码:

void Robot::test(){
    FuncDelegate f_delegate;
    f_delegate = MakeDelegate(this, &Robot::halt);

    timer.setTimerDelg(1, f_delegate, 1);
}

void Robot::halt() {
    Serial.println("TEST");
}

SimpleTimer类中的代码:

int SimpleTimer::setTimerDelg(long d, FuncDelegate f, int n){
    f();
}

Arduino会在控制台中打印TEST。

接下来是把它放进一个数组中,我认为不会有太多问题。感谢大家,我简直不敢相信我在两天内学到了这么多东西。

那是什么味道?是成功的味道吗?

对于那些感兴趣的人,所使用的Delegate系统不会导致内存容量问题:使用FastDelegate。

AVR Memory Usage
----------------
Device: atmega2560

Program:   17178 bytes (6.6% Full)
(.text + .data + .bootloader)

Data:       1292 bytes (15.8% Full)
(.data + .bss + .noinit)


Finished building: sizedummy

没有使用FastDelegate:

AVR Memory Usage
----------------
Device: atmega2560

Program:   17030 bytes (6.5% Full)
(.text + .data + .bootloader)

Data:       1292 bytes (15.8% Full)
(.data + .bss + .noinit)


Finished building: sizedummy

1
你能够修改库代码吗?看起来唯一的获取方式是从http://arduino.cc/playground/Code/SimpleTimer#GetTheCode复制。这是你正在做的吗? - Matthew Murdoch
是的,让我们更改SimpleTimer代码。 - Aduen
@Aduen,当你将FastDelegate集成到SimpleTimer中时,是否遇到了memcmp未声明的问题?你有修改后的SimpleTimer源代码可用吗? - Thomas Nadin
4个回答

8
你可以通过创建一个函数对象,作为计时器代码和你的代码之间的代理来实现此操作。
class MyHaltStruct
{
public:
    MyHaltStruct(Robot &robot)
        : m_robot(robot)
        { }

    void operator()()
        { robot.halt(); }

private:
    Robot &m_robot;
}

// ...

timer.setTimer(time, MyHaltStruct(*this), 1);

编辑

如果无法使用函数对象实现,您可以使用全局变量和函数,放在命名空间中:

namespace my_robot_halter
{
    Robot *robot = 0;

    void halt()
    {
        if (robot)
            robot->halt();
    }
}

// ...

my_robot_halter::robot = this;
timer.setTimer(time, my_robot_halter::halt, 1);

这仅适用于您只有一个机器人实例的情况。

1
如果那个可以运行,那么 std::bind(&Robot::halt, this) 也能运行,对吗? - Kerrek SB
2
如果setTimer是一个模板并接受functors,那么你可以这样做。但是在我看来,它似乎只接受void (*timer_callback)():http://arduino.cc/playground/Code/SimpleTimer#GetTheCode - Useless
@KerrekSB Arduino的后端(avr-gcc)不支持任何标准模板库,因此我怀疑在这种情况下std:bind不是一个选项。 - Matthew Murdoch
看起来这是一个可行的解决方案,我需要再多了解一下函数对象和它们之间的区别,但我认为这应该可以在Arduino上工作。唯一的问题是,我是否需要为每个要用作回调的函数都创建这样的结构体? - Aduen
1
如果你修改SimpleTimer库,使其可以使用函数对象...那么你就可以创建一个函数对象,它可以接受成员函数指针和对象指针,这将给你带来很多相同的灵活性。 - Useless
显示剩余2条评论

3
由于计时器回调函数的签名不接受任何参数,所以您需要使用一些全局变量(或静态变量)来实现:
Robot *global_robot_for_timer;
void robot_halt_callback()
{
    global_robot_for_timer->halt();
}

您至少可以将这些内容封装到自己的文件中,但这并不美观。正如Matthew Murdoch所建议的那样,最好编辑SimpleTimer本身。更常规的接口可能是:

typedef void (*timer_callback)(void *);

SimpleTimer::setTimer(long time, timer_callback f, void *data);

void robot_halt_callback(void *data)
{
    Robot *r = (Robot *)data;
    r->halt();
}

比如,当你调用setTimer时,你会提供一个参数,该参数会传递给回调函数。

对SimpleTimer进行的最小更改可能是:

SimpleTimer.h

typedef void (*timer_function)(void *);
struct timer_callback {
    timer_function func;
    void *arg;
};
// ... every method taking a callback should look like this:
int SimpleTimer::setTimeout(long, timer_function, void *);

SimpleTimer.cpp

// ... callbacks is now an array of structures
callbacks[i] = {0};

// ... findFirstFreeSlot
if (callbacks[i].func == 0) {

// ... SimpleTimer::setTimer can take the timer_callback structure, but
// that means it's callers have to construct it ...
int SimpleTimer::setTimeout(long d, timer_function func, void *arg) {
    timer_callback cb = {func, arg};
    return setTimer(d, cb, RUN_ONCE);
}

好的,谢谢。我会尝试这个东西并报告我的发现。 - Aduen
请确保你仔细区分 timer_functiontimer_callback 之间的区别,因为我上面的补丁远未完成。你也可以想出更好的名称 :-) - Useless

2

您不能传递非静态成员函数 - 只能传递静态成员函数。签名应该像这样:

static void halt()
{
    //implementation
}

原因在于每个非静态成员函数都有一个隐式的Robot*参数,称为this指针,它方便了对当前对象的访问。由于回调函数签名没有这样的Robot*参数,除非它是静态的,否则你不可能传递一个Robot类的成员函数。因此,在你的实现中,需要将该成员函数改为静态函数。
void halt();

生效。
static void halt( Robot* thisPointer );

而当你这样做时

void Robot::halt() {
    __isMoving = false;
}

你实际上拥有这个:

你有效地拥有这个:

void Robot::halt( Robot* thisPointer ) {
    thisPointer->__isMoving = false;
}

当然,halt(Robot*)函数指针不能替换C回调函数的void(*)(void)

如果需要在回调内部访问class Robot的非静态成员变量,您需要从其他地方检索指向class Robot实例的指针 - 例如将其存储为静态成员变量,以便不依赖于this指针。


通常C库都有一个“void * user”参数,您可以在其中存储其他数据(例如此指针,之后可以进行强制转换)。 - nob
@nob:是的,那很典型,但看起来在这种情况下没有类似的东西。 - sharptooth
@sharptooth:然而,由于代码并没有真正打包成库(http://arduino.cc/playground/Code/SimpleTimer#GetTheCode),因此很容易进行更改以支持这样做... - Matthew Murdoch
好的,我觉得我明白了,解释得非常好。所以,我想修改一下我的先前论点,不想改变SimpleTimer代码。 - Aduen

1

理解函数指针和类成员指针的不同之处很重要,这并非是任意的原因,而是因为实例方法有一个隐式的this参数(此外,它们必须与继承和虚函数一起工作,这增加了更多的复杂性;因此它们可能会占用16个或更多字节的空间)。换句话说,类成员函数指针只有在类的实例存在时才有意义。

正如当前排名最高的答案所说,你最好选择使用函数对象。虽然setTimer函数可能只接受函数指针,但可以编写一个模板函数来包装调用并接受两者。为了进行更精细的处理,你可以编写一个模板元程序(Boost.TypeTraits具有is_pointeris_function甚至is_member_function_pointer)来处理不同的情况。

如何制作函数对象是另一回事。你可以选择手动编写它们(这意味着为每个函数对象实现一个带有operator()的类),但根据你的需求,这可能会很繁琐。以下是几个选项:

  • std::bind: 你可以使用它来创建一个函数对象(functor),其中第一个参数将绑定到你指定的值上——对于成员函数,它将是实例。
  • 根据你的编译器,你可能无法访问 std::bind —— 在这种情况下,我建议你使用 boost::bind。它是一个仅有头文件的库,并提供相同的功能。
  • 你可以使用 另一种委托实现。我没有使用过这个,但它声称比其他实现(包括 std::function)更快。

这些提到的库都是仅有头文件的,所以它们可能不算作“大型库”。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接