みなさん、ラムダ式を関数ポインタに変換したいですよね??
ラムダ式を関数ポインタに変換できると、関数ポインタを受け取る関数に直接ラムダ式を渡せて便利です。
うれしいことに、キャプチャを含まないラムダ式は関数ポインタに暗黙変換できます。
std::atexit([] { std::println("Goodbye!"); });
しかし、ラムダ式がキャプチャを含んでいると関数ポインタに変換することはできません。
かなしい!
std::string name = "Jack";
std::atexit([=] { std::println("Goodbye, {}!", name); });
// error: cannot convert 'main()::<lambda()>' to 'void (*)()'
これちょっと不便ですよね? ね??
というわけで、この記事では、なんかいろいろどうにかしてキャプチャを含むラムダ式を関数ポインタに変換します。
キャプチャせずに使える変数
ラムダ式で使うすべての変数にキャプチャが必要なわけではありません。
static 変数などはキャプチャせずともラムダ式から使えると cppreference に書いてます1。
A lambda expression can use a variable without capturing it if the variable
- is a non-local variable or has static or thread local storage duration (in which case the variable cannot be captured), or
- is a reference that has been initialized with a constant expression.
Reference: https://en.cppreference.com/w/cpp/language/lambda
これを利用して、キャプチャありのラムダ式を static 変数に代入し、それをキャプチャなしのラムダ式から呼び出します。
これによりキャプチャがなくなるので、関数ポインタに変換することができます。
やったね!
#include <print>
#include <string>
#include <type_traits>
#include <utility>
#include <cstdlib>
// lambda to function の tofu の部分
template <class F>
std::add_pointer_t<void()> tofu(F&& f) {
static auto fu = std::forward<F>(f); // static に置く
return [] static { fu(); }; // キャプチャなしで呼び出せる!
}
int main() {
std::string name = "Jack";
std::atexit(tofu([=] { std::println("Goodbye, {}!", name); }));
}
ただし、 static 変数への代入は最初の呼び出し時にしか行われないので、同じ型のラムダ式に対して繰り返し使うことはできません。
int main() {
for (int i = 0; i < 3; ++i) {
tofu([=] { std::println("i: {}", i); })();
}
// i: 0
// i: 0
// i: 0
}
関数ポインタに変換されたラムダ式が持つオブジェクトは、プログラム終了まで破棄されません。
すべての引数と戻り値に対応
先ほど示したコードは void()
にしか対応していなかったので、テンプレートをぐねぐねしてすべて2の引数と戻り値の組み合わせに対応します。
変換先の関数の型は &F::operator()
の型を使って求めます3。
#include <type_traits>
#include <utility>
template <class F, class R, class... Args>
R (*tofu_impl(F&& f, R (std::remove_cvref_t<F>::*)(Args...)) noexcept(std::is_nothrow_constructible_v<std::remove_cvref_t<F>, F>))(Args...) noexcept(std::is_nothrow_invocable_v<F&, Args...>) {
static auto fu = std::forward<F>(f);
return [](Args... args) static noexcept(std::is_nothrow_invocable_v<F&, Args...>) {
return fu(std::forward<Args>(args)...);
};
}
template <class F, class R, class... Args>
R (*tofu_impl(F&& f, R (std::remove_cvref_t<F>::*)(Args...) const) noexcept(std::is_nothrow_constructible_v<std::remove_cvref_t<F>, F>))(Args...) noexcept(std::is_nothrow_invocable_v<const F&, Args...>) {
static const auto fu = std::forward<F>(f);
return [](Args... args) static noexcept(std::is_nothrow_invocable_v<const F&, Args...>) {
return fu(std::forward<Args>(args)...);
};
}
template <class F>
auto tofu(F&& f) noexcept(std::is_nothrow_constructible_v<std::remove_cvref_t<F>, F>) {
return tofu_impl(std::forward<F>(f), &std::remove_cvref_t<F>::operator());
}
R (*tofu_impl(F&& f, R (std::remove_cvref_t<F>::*)(Args...)) noexcept(std::is_nothrow_constructible_v<std::remove_cvref_t<F>, F>))(Args...) noexcept(std::is_nothrow_invoable_v<F&, Args...>)
とか読みにくすぎて草。
どれがどれの noexcept
だよ。
みんなは読めるかな?()
使用例
例1
int main() {
std::string section;
std::set_terminate(tofu([&] {
std::println("Error during {}", section);
std::cout << std::flush;
std::abort();
}));
section = "initialisation";
std::string text = "--42";
section = "parsing";
int value = std::stoi(text);
section = "output";
std::println("stoi({:?}) = {}", text, value);
}
Error during parsing
例2
int main() {
const std::vector<int> a{ 3, 1, 4, 1, 5, 9, 2 };
std::vector<std::size_t> indices(std::from_range, std::views::iota(0uz, a.size()));
std::qsort(indices.data(), indices.size(), sizeof(indices[0]), tofu([&](const void* x, const void* y) {
const auto lhs = a[*static_cast<const std::size_t*>(x)];
const auto rhs = a[*static_cast<const std::size_t*>(y)];
if (lhs < rhs) return -1;
if (lhs > rhs) return 1;
return 0;
}));
for (std::size_t i : indices) {
std::print(" [{}] ", i);
}
std::println();
std::println("{::3}", indices | std::views::transform([&](std::size_t i) { return a[i]; }));
}
[1] [3] [6] [0] [2] [4] [5]
[ 1, 1, 2, 3, 4, 5, 9]
std::ranges::sort()
使えばよくね?
例3
int main() {
const std::vector<int> a{ 2, 7, 1, 8, 2, 8, 1, 8, 2, 8 };
int sum1 = 0;
int sum2 = 0;
const std::size_t mid = a.size() / 2;
pthread_t t;
pthread_create(&t, nullptr, tofu([&](void*) -> void* {
for (std::size_t i = 0; i < mid; ++i) {
sum1 += a[i];
}
return nullptr;
}), nullptr);
for (std::size_t i = mid; i < a.size(); ++i) {
sum2 += a[i];
}
pthread_join(t, nullptr);
std::println("sum: {}", sum1 + sum2);
}
sum: 47
std::thread
使えばよくね?
おわりに
いかがでしたか?
static 変数を使うことでキャプチャを含むラムダ式も関数ポインタに変換することができました。
これでコーディング効率爆アゲ間違いなし!
ぜひ活用してみてください!