如何将lambda函数传递给带有模板类的类实例化

How to pass a lambda function an instantiantion of a class with a template class

本文关键字:实例化 lambda 函数      更新时间:2023-10-16

我试图创建一个包装POSIX timer functions的c++类,以便我有一个计时器类,可以灵活地使用并传递用户定义的数据(通常不能直接使用POSIX C计时器函数)。所以我有以下内容:

Timer类实现:

#include <functional>
#include <utility>
#include <sys/time.h>
#include <signal.h>
#include <time.h>
#include <string.h>

template<typename F>
class Timer
{
    public:
        struct sigaction SignalAction;
        struct sigevent signalEvent;
        struct itimerval   timer_ms;
        timer_t timerID;
        Timer(F callback, int milliseconds) : onTimeout(std::move(callback)) 
        {
            timer_ms.it_value.tv_sec    = milliseconds / 1000;
            timer_ms.it_value.tv_usec   = ( milliseconds % 1000 ) / 1000;
            timer_ms.it_interval.tv_sec = milliseconds / 1000;
            timer_ms.it_interval.tv_usec = ( milliseconds % 1000 ) / 1000;
            // Clear the sa_mask
            sigemptyset(&this->SignalAction.sa_mask);
            // set the SA_SIGINFO flag to use the extended signal-handler function
            this->SignalAction.sa_flags = SA_SIGINFO;
            // Define sigaction method
            // This function will be called by the signal
            this->SignalAction.sa_sigaction = Timer::alarmFunction;         
            // Define sigEvent
            // This information will be forwarded to the signal-handler function
            memset(&this->signalEvent, 0, sizeof(this->signalEvent));
            // With the SIGEV_SIGNAL flag we say that there is sigev_value
            this->signalEvent.sigev_notify = SIGEV_SIGNAL;
            // Now it's possible to give a pointer to the object
            this->signalEvent.sigev_value.sival_ptr = (void*) this;
            // Declare this signal as Alarm Signal
            this->signalEvent.sigev_signo = SIGALRM;
            // Install the Timer
            timer_create(CLOCK_REALTIME, &this->signalEvent, &this->timerID);
            sigaction(SIGALRM, &this->SignalAction, NULL);
        }
        void start()
        {
            // start the timer
            //timer_settime(this->timerID, 0, &this->timerSpecs, NULL);
            setitimer(ITIMER_REAL, &timer_ms, NULL);
            return;
        }

        static void alarmFunction(int sigNumb, siginfo_t *si, void *uc)
        {
            // get the pointer out of the siginfo structure and asign it to a new pointer variable
            Timer *ptrTimer = reinterpret_cast<Timer *> (si->si_value.sival_ptr);
            // call the member function
            ptrTimer->onTimeout();
        }

    private:
        F onTimeout;
};

template<typename F>
Timer<F> CreateTimer(int milliseconds, F callback)
{
    return Timer<F>(callback, milliseconds);
}

注意,在Timer类中,模板F onTimeoutTimer::alarmFunction()方法中通过使用存储在siginfo_t结构体中的指针来调用。

static void alarmFunction(int sigNumb, siginfo_t *si, void *uc)
{
    // get the pointer out of the siginfo structure and asign it to a new pointer variable
    Timer *ptrTimer = reinterpret_cast<Timer *> (si->si_value.sival_ptr);
    // call the member function
    ptrTimer->onTimeout();
}

And my main.cpp:

#include "Timer.h"
#include <stdlib.h>
#include <iostream>
class Generic
{
    private:
        int m_data;
    public:
        Generic( int data ) : m_data( data ) {}
        int getData() { return( m_data); }
};
void HandleTimer()
{
    std::cout << "HandleTimer " << std::endl;
    return;
}
int main(int argc, char *argv[])
{
    Generic obj(42);
    auto timer = CreateTimer(1000, [] { HandleTimer(); });
    timer.start();
    while(1)
    {
        sleep(5);
    }
    return( 0 );
}

在main.cpp中,您将看到创建了一个名为Generic的愚蠢的小类,并实例化了一个名为obj的类。

一些问题:

1-如何将obj传递给HandleTimer():

auto timer = CreateTimer(1000, [] { HandleTimer(); });

这样当定时器被触发时,HandleTimer()Timer类调用,并且可以访问obj ?

2-有什么更好的方法可以做到这一点?

我已经创建了一个简单得多的Timer类,它只接受频率和静态函数作为参数,以便在计时器到期时调用静态函数。这很好,但是静态函数不能访问任何类作用域,如果不借助于全局数据,就不能访问用户定义的数据。

编辑:我试过了:

void HandleTimer( Generic *obj )
{
    std::cout << "HandleTimer " << std::endl;
    return;
}

int main(int argc, char *argv[])
{
    Generic obj(42);
    auto timer = CreateTimer(1000, [&obj] { HandleTimer(&obj); });
    timer.start();
    while(1)
    {
        sleep(5);
    }
    return( 0 );
}

但是这会导致段错误。GDB会话如下:

(gdb) file blink
Reading symbols from /home/jrn/build_root/src/svn/arm/arm-gpio/Timer/blink...done.
(gdb) run
Starting program: /home/jrn/build_root/src/svn/arm/arm-gpio/Timer/blink 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Program received signal SIGSEGV, Segmentation fault.
0x000000000040298a in __lambda0::operator() (__closure=0x100) at main.cpp:28
28      auto timer = CreateTimer(1000, [&obj] { HandleTimer(&obj); });
(gdb) 

您的代码至少在两个不同的地方有未定义的行为。

首先,CreateTimer函数按值返回Timer对象。这意味着它正在复制计时器对象,并且由于您将this指针传递给sigevent,因此在复制之后,main()timer对象的this指针将不同。因此,当回调触发时,您将尝试在某个被销毁的对象上调用onTimeout

大多数情况下,您可能会侥幸逃脱,因为由于副本省略,不会生成副本。但是,您可以在修复其他错误后,通过使用-fno-elide-constructors编译自己来验证错误行为。

您可以通过不使用CreateTimer()来解决这个问题,而是将以下行添加到main()中。我不知道有什么更好的办法来解决这个问题。这还演示了如何将obj按值传递给lambda。

auto l = [obj]() { HandleTimer(obj); };
Timer<decltype(l)> timer(l, 1000);
timer.start();

还要考虑让你的类不可复制/不可赋值

Timer(Timer const&) = delete;
Timer& operator=(Timer const&) = delete;

第二个问题也是与this指针,但我不知道足够的定时器你用来找出你做错了什么。将这些调试语句添加到代码中:

// Now it's possible to give a pointer to the object
this->signalEvent.sigev_value.sival_ptr = static_cast<void *>(this);
std::cout << "this: " << this << std::endl;

Timer *ptrTimer = static_cast<Timer *> (si->si_value.sival_ptr);
std::cout << "sival_ptr: " << si->si_value.sival_ptr << std::endl;
// call the member function
// ptrTimer->onTimeout();

您将看到两个语句打印的指针值是不同的。查看您注释掉的一些代码,以及这个答案,我做了以下更改:

添加数据成员并在构造函数中初始化

struct itimerspec  timerSpecs;
...
timerSpecs.it_value.tv_sec    = milliseconds / 1000;
timerSpecs.it_value.tv_nsec   = ( milliseconds % 1000 ) / 1000;  // this needs fixing
timerSpecs.it_interval.tv_sec = milliseconds / 1000;
timerSpecs.it_interval.tv_nsec = ( milliseconds % 1000 ) / 1000; // this needs fixing
...
void start()
{
    // start the timer
    timer_settime(this->timerID, 0, &this->timerSpecs, NULL);
    // setitimer(ITIMER_REAL, &timer_ms, NULL);
    return;
}

还稍微修改了HandleTimer

void HandleTimer(Generic g)
{
    std::cout << "HandleTimer " << g.getData() << std::endl;
    return;
}
现在你的代码运行并输出
HandleTimer 42

您可以像这样通过引用将obj传递给lambda:

auto timer = CreateTimer(1000, [&obj] { HandleTimer(obj); });

请注意,这会在堆栈上创建对对象的引用,这对于异步回调(如计时器)是危险的,因为计时器可能在对象离开作用域后回调到lambda。

我经常使用std::shared_ptr<>,因为这个原因:

#include <memory>
// ...
std::shared_ptr<Generic> obj(new Generic(42));
auto timer = CreateTimer(1000, [obj] { HandleTimer(obj); });

lambda(和计时器)将在计时器的生命周期内保存对obj的引用。

考虑用std::function代替你自己的模板形参作为回调函数。

#include <functional>
// ...
Timer(std::function<void()> callback, int milliseconds): onCallback(callback), // etc

正如我在评论中提到的,我认为使用std::chronostd::thread可能会简单得多。具体来说,你可以这样写:

#include <chrono>
#include <iostream>
#include <thread>
// Generic class and HandleTimer code goes here
int main()
{
    Generic obj(42);
    std::thread([&obj]() {
        while(1) {
            std::this_thread::sleep_for(std::chrono::milliseconds(1000));
            HandleTimer(obj);
        }
    }).detach();
    while(1)
    {
        ++obj;
        std::this_thread::sleep_for(std::chrono::seconds(5));
    }
    return 0;
}

请注意,std::thread中的lambda通过引用捕获obj,因此您必须注意它不会比传递的对象活得更长,但是这段代码正确地处理了这种情况。

std::thread将每1000毫秒调用HandleTimer(obj)(或您希望的任何函数)。

我已经修改了你的Generic对象,以添加一个自增运算符,并使getData() const:

Generic &operator++() { ++m_data; return *this; }
int getData() const { return m_data; }

我也改变了你的HandleTimer例程,像这样:

void HandleTimer(const Generic &obj)
{
    std::cout << "HandleTimer " << obj.getData() << std::endl;
}

在主循环中,每5秒调用一次。结果是HandleTimer将每秒被调用一次,并且它所指向的对象每5秒增加一次。当我运行程序时,我得到这样的输出,每秒打印一行。

HandleTimer 43
HandleTimer 43
HandleTimer 43
HandleTimer 43
HandleTimer 44
HandleTimer 44
HandleTimer 44
HandleTimer 44
HandleTimer 44
HandleTimer 45
HandleTimer 45
HandleTimer 45
HandleTimer 45
... 

请注意,如果您不想在其他线程也打印时导致输出混乱,那么确实应该有一个互斥锁来保护对std::cout的访问,但我将把这个问题留给您。

这个解决方案的好处是它是可移植的,而不依赖于Posix计时器的神秘细节,正如您所看到的,这些细节对于正确使用是非常重要的。