これは確かに良い質問です。ただし、C++ を非難する前に、何が起こっているのかを把握しましょう。ラムダがどのように実装されているかを考えてみてください。
最も単純なラムダは、データがキャプチャされない場合です。その場合、その基になる型は単純な単純な関数になります。たとえば、次のようなラムダです。
[] (int p0) {}
単純な関数と同等になります。
void foo(int p0)
{
}
そのラムダを関数ポインターにしたい場合、これは実際には完全に機能します。例えば:
#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;
}
ここまでは順調ですね。しかし、ここで、いくつかのデータをシグナル ハンドラーに関連付けたいと考えています (ちなみに、シグナル ハンドラー内でしかシグナル セーフ コードを実行できないため、上記のコードは未定義の動作です)。したがって、次のようなラムダが必要です。
#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++ ラムダのシンタックス シュガーのことはしばらく忘れましょう。C やアセンブラーでもラムダを「模倣」できることは周知の事実です。実際、それはどのように見えるでしょうか?C スタイルの「ラムダ」は次のようになります (これはまだ 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++ は "context" を "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;
}
上記の 2 つの例では、「データ」が常に渡されることに注意してください。これはとても重要です。コールバックを呼び出すはずの 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 をユーザー空間に提供することは、最善のアイデアではありませんでした。もう一度言いますが、シグナルハンドラでできることはごくわずかです。async-signal-safe functionsのみを呼び出すことができます。
もちろん、古い API は実際には POSIX 標準であるため、すぐになくなることはありません。ただし、開発者は問題を認識しており、シグナルを処理するためのより良い方法があります。たとえば Linux では、 を使用eventfd
してシグナル ハンドラをインストールし、それを任意のコンテキストに関連付けて、コールバック関数で必要なことを何でも行うことができます。
とにかく、遊んでいたラムダに戻りましょう。問題は C++ ではなく、グローバル変数を使用する以外にコンテキストを渡す方法がないシグナル API にあります。そうは言っても、ラムダでも機能します。
#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++ がここで行っていることは正しいことなので、恥ずかしいことではありません。