15

我正在尝试使用函数signal(int,void(*)(int))from<csignal>来处理浮点异常 SIGFPE。我希望能够打印一些有用的诊断信息,除了一条消息说“浮点异常”或类似的东西。这意味着我作为处理程序传递的函数signal需要访问我的代码中的一些数据。问题就在于此。

该函数必须返回void并只接受 1 个类型的参数int。我不能使处理程序成为我的数据存储类的成员函数,因为那时该类型将void(Foo::*)(int)归因于隐藏this指针。

我想过使用 lambdas 来尝试制作这样的匿名函数;

void handler(int nSig, Foo data)
{
    // do something
}
// snip
Foo data;
signal(SIGFPE, [&](int nSig)->void{handler(nSig,data);});

然而,因为 lambda 从编译器外部捕获变量data,所以不会让它转换为指向的指针void(*)(int)(这很遗憾,因为这似乎是 lambdas 的理想用途)。

我可以简单地创建data一个全局变量,然后可以看到它,handler但出于明显的原因我不愿意这样做。

所以我的问题是这样的;在 C++ 中模仿匿名函数的最佳方法是什么

注意:我更喜欢本机 C++ 解决方案,而不必使用 boost 或等效的。

4

3 回答 3

13

这确实是一个好问题。在指责 C++ 之前,让我们先弄清楚发生了什么。想想 lambdas 是如何实现的。

最简单的 lambda 是在没有捕获数据时。如果是这样的话,它的底层类型就变成了一个简单的普通函数。例如,像这样的 lambda:

[] (int p0) {}

将相当于一个简单的函数:

void foo(int p0)
{
}

如果您希望该 lambda 成为函数指针,这实际上非常有效。例如:

#include <string>
#include <csignal>
#include <iostream>

int main()
{
    int ret;
    signal(SIGINT, [](int signal) {
            std::cout << "Got signal " << signal << std::endl;
        });
    std::cin >> ret;
    return ret;
}

到现在为止还挺好。但是现在您想将一些数据与您的信号处理程序相关联(顺便说一下,上面的代码是未定义的行为,因为您只能在信号处理程序中执行信号安全代码)。所以你想要一个像这样的 lambda:

#include <string>
#include <csignal>
#include <iostream>

struct handler_context {
    std::string code;
    std::string desc;
};

int main()
{
    int ret;
    handler_context ctx({ "SIGINT", "Interrupt" });
    signal(SIGINT, [&](int signal) {
            std::cout << "Got signal " << signal
                      << " (" << ctx.code << ": " << ctx.desc
                      << ")\n" << std::flush;
        });
    std::cin >> ret;
    return ret;
}

让我们暂时忘记 C++ lambda 的语法糖。即使在 C 或汇编程序中,您也可以“模仿” lambda,这已不是什么秘密。那么,实际上会是什么样子呢?C 风格的“Lambda”可能看起来像这样(这仍然是 C++):

#include <string>
#include <cstdlib>
#include <iostream>

/*
 * This is a context associated with our lambda function.
 * Some dummy variables, for the sake of example.
 */
struct lambda_captures {
    int v0;
    int v1;
};

static int lambda_func(int p0, void *ctx) // <-- This is our lambda "function".
{
    lambda_captures *captures = (lambda_captures *)ctx;
    std::cout << "Got " << p0 << " (ctx: "
              << captures->v0 << ", " << captures->v1
              << ")\n" << std::flush;
    return 0;
}

// Below is an example of API function provided to the user that can
// invoke a callback supplied by the user.
static void some_api_function(int (*callback)(int p, void *data), void *data)
{
    callback(12345, data);
    callback(98765, data);
}

int main()
{
    lambda_captures captures;
    captures.v0 = 1986;
    captures.v1 = 2012;

    some_api_function(lambda_func, (void *)&captures);

    return EXIT_SUCCESS;
}

以上是 C 风格,C++ 倾向于将“上下文”作为“this”传递,它始终是隐式的第一个参数。如果我们的 API 支持将“数据”作为第一个参数传递,我们可以应用指向成员转换 (PMF) 的指针并编写如下内容:

#include <string>
#include <cstdlib>
#include <iostream>

struct some_class {
    int v0;
    int v1;

    int func(int p0)
    {
        std::cout << "Got " << p0 << " (ctx: "
                  << v0 << ", " << v1
                  << ")\n" << std::flush;
        return p0;
    }
};

static void some_api_function(int (*callback)(void *data, int p), void *data)
{
    callback(data, 12345);
    callback(data, 98765);
}

int main()
{
    typedef int (*mpf_type)(void *, int);

    some_class clazz({ 1986, 2012 }); // <- Note a bit of a Java style :-)
    some_api_function((mpf_type)&some_class::func, (void *)&clazz);

    return EXIT_SUCCESS;
}

在上面的两个例子中,注意“数据”总是被传递的。这个非常重要。如果应该调用回调的 API 不接受以某种方式传回回调的“void *”指针,则无法将任何上下文与回调关联。唯一的例外是全局数据。例如,这个 API 很糟糕:

#include <string>
#include <cstdlib>
#include <iostream>

struct lambda_captures {
    int v0;
    int v1;
};

static int lambda_func(int p0)
{
/*
    // WHERE DO WE GET OUR "lambda_captures" OBJECT FROM????
    lambda_captures *captures = (lambda_captures *)ctx;
    std::cout << "Got " << p0 << " (ctx: "
              << captures->v0 << ", " << captures->v1
              << ")\n" << std::flush;
*/
    return 0;
}

// Below is an example of API function provided to the user that can
// invoke a callback supplied by the user.
static void some_api_function(int (*callback)(int p))
{
    callback(12345);
    callback(98765);
}

int main()
{
    lambda_captures captures;
    captures.v0 = 1986;
    captures.v1 = 2012;

    some_api_function(lambda_func /* How do we pass a context??? */);

    return EXIT_SUCCESS;
}

话虽如此,旧的信号 API 就是这样。解决该问题的唯一方法是将您的“上下文”实际放入全局范围。然后信号处理函数可以访问它,因为地址是众所周知的,例如:

#include <string>
#include <cstdlib>
#include <iostream>

struct lambda_captures {
    int v0;
    int v1;
};

lambda_captures captures({ 1986, 2012 }); // Whoa-la!!!

static int lambda_func(int p0)
{
    std::cout << "Got " << p0 << " (ctx: "
              << captures.v0 << ", " << captures.v1
              << ")\n" << std::flush;
    return 0;
}

// Below is an example of API function provided to the user that can
// invoke a callback supplied by the user.
static void some_api_function(int (*callback)(int p))
{
    callback(12345);
    callback(98765);
}

int main()
{
    some_api_function(lambda_func);

    return EXIT_SUCCESS;
}

这是人们必须面对的。不仅在使用信号 API 的情况下。这也适用于其他事物。例如,中断处理程序处理。但是你必须处理硬件的低级编程。当然,在用户空间提供这种 API 并不是最好的主意。我将再次提到它 - 您只能在信号处理程序中做一小部分事情。您只能调用异步信号安全函数

当然,旧的 API 不会很快消失,因为它实际上是 POSIX 标准。然而,开发人员认识到了这个问题,并且有更好的方法来处理信号。例如,在 Linux 中,您可以使用eventfd安装信号处理程序,将其与任意上下文相关联,并在回调函数中执行您想要的任何操作。

无论如何,让我们回到你正在使用的 lambda。问题不在于 C++,而在于信号 API,除了使用全局变量外,您无法传递上下文。话虽如此,它也适用于 lambda:

#include <string>
#include <cstdlib>
#include <csignal>
#include <iostream>

struct some_data {
    std::string code;
    std::string desc;
};

static some_data data({ "SIGING", "Interrupt" });

int main()
{
    signal(SIGINT, [](int signal) {
            std::cout << "Got " << signal << " (" << data.code << ", "
                      << data.desc << ")\n" << std::flush;
        });
    return EXIT_SUCCESS;
}

因此,C++ 在这里所做的事情并没有什么可耻的,因为它做了一件正确的事情。

于 2012-05-30T13:31:31.763 回答
8

C 中没有匿名函数之类的东西(C++ 在这里无关紧要,因为函数必须遵守 C 调用约定)。

您唯一能做的就是从处理程序中颤抖访问全局变量,可能是全局变量(而不是常量,这很好)。

我建议将这些全局变量设为本地线程以避免多线程问题,但从全局变量使应用程序更脆弱的意义上说,它仍然很糟糕。


如何 ?

注意:正如 Luc Danton 耐心地向我解释的那样,信号可能会中断任何非原子活动,因此只有当它是无锁原子(或其他一些东西)时,从全局读取才是安全的。不幸的是std::function可能不是这样,这取决于您的实现,我仍然会留下这段代码来解释如果访问是原子的,它是如何完成std::function

可以创建一个蹦床来调用有状态的东西,隔离线程并允许重入调用。

typedef std::function<void(int)> SignalHandlerType;

extern thread_local ignalHandlerType SignalHandler;

我们创建以下访问器(传递给信号):

void handle_signal(int const i) {
    if (SignalHandler) { SignalHandler(i); }
}

以及以下 RAII 设置器:

class SignalSetter: boost::noncopyable {
public:
    SignalSetter(int signal, SignalHandlerType&& sh):
        signal(signal), chandler(0), handler(sh)
    {
        chandler = std::signal(signal, &handle_signal<T>);
        swap(SignalHandler, handler);
    }

    ~SignalSetter() {
        std::signal(signal, chandler);
        swap(SignalHandler, handler);
    }

private:
    typedef void(*CHandlerType)(int);

    int signal;
    CHandlerType chandler;
    SignalHandlerType handler;
};

注意:全局变量和handle_signal可能都是privateSignalSetter......但因为std::signal不是......

预期用途:

int main(int argc, char* argv[]) {
    SignalSetter setter(SIGFPE, [argc, argv]() {
        std::cout << argc << ": " << argc << std::endl;
    });

    // do what you want.
}
于 2012-05-30T12:25:54.683 回答
0

您无法在运行时轻松创建新的静态函数,一些 JIT 编译器库能够做到这一点。如果您只需要合理数量的指针,您可以通过专门模板来创建一些静态函数池。

所以最简单的方法是用静态函数包装 C++ Functor。这里的问题是没有像用户数据参数这样的东西。只有一个参数,即信号数。由于只有 64 个信号,您可以创建一个静态数组std::function< void(int) >并根据信号编号调用每个信号。一些简单的例子:

typedef std::function< void(int) > SignalFunc;

static std::array< SignalFunc, 64 > signalsFunc;

static void cHandler(int nSig)
{
    signalsFunc.at(nSig)(nSig);
}

SignalFunc RegisterSystemSignal( int sig, SignalFunc func )
{
    if( signal( sig, func ? &cHandler : (sighandler_t)SIG_DFL ) != SIG_ERR )
    { 
        func.swap( signalsFunc.at( sig ) );
        return func;
    }
    throw some_error();
}

所以现在你可以这样做:

RegisterSystemSignal(SIGFPE, [&](int nSig)->void{handler(nSig,data);});

还有一个sigaction女巫有更多的特点。

于 2012-05-30T15:38:13.123 回答