这确实是一个好问题。在指责 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++ 在这里所做的事情并没有什么可耻的,因为它做了一件正确的事情。