This is indeed a good question. Let's figure out what is going before blaming C++ though. Just think about how lambdas are implemented.
The most simple lambda is when no data is captured. If that is the case, its underlying type becomes a simple plain function. For example, a lambda like this:
[] (int p0) {}
will be an equivalent of a simple function:
void foo(int p0)
{
}
That actually perfectly works in case you want that lambda to become a function pointer. For example:
#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;
}
So far so good. But now you want to associate some data with your signal handler (by the way, the code above is undefined behavior as you can only execute signal-safe code inside a signal handler). So you want a lambda like:
#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;
}
Let's forget for a moment about a syntactic sugar of C++ lambdas. It is no secret that you can "mimic" lambda even in C or assembler. So how would that look, actually? "Lambda" in C-style could look like this (this is still 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;
}
Above is a C style, C++ tends to pass "context" as "this", which is always an implicit first argument. If our API supported passing "data" as first argument, we could apply pointer to member conversion (PMF) and write something like this:
#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;
}
In the above two examples, note that "data" is always passed around. This is very important. If the API that is supposed to invoke your callback does not accept a "void *" pointer that is passed back to your callback somehow, there is no way you can associate any context with the callback. The only exception is global data. For example, this API is bad:
#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;
}
That being said, an old signal API is exactly like that. The only way to work around the problem is to actually put your "context" into a global scope. Then signal handler function can access it because the address is well known, for example:
#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;
}
This is what people have to deal with. Not only in case with signals API. This applies to other things as well. For example, interrupt handler processing. But that low-level programming where you have to deal with hardware. Of course, providing this sort of API in the user-space was not the best idea. And I will mention it again - there is only a small set of things you can do in a signal handler. You can only call async-signal-safe functions.
Of course, old API is not going away anytime soon because it is actually a POSIX standard. However, developers recognize the problem and there are better ways to handle signals. In Linux, for example, you can use eventfd
to install a signal handler, associate it with arbitrary context and do whatever you want in the callback function.
At any rate, let's get back to the lambda you were playing with. The problem is not with C++, but with signals API that leaves no way for you to pass a context except using a global variable. That being said, it works with lambdas too:
#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;
}
Therefore, there is no shame in what C++ is doing here as it does a right thing.