7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++Advent Calendar 2024

Day 5

キャプチャを含むラムダ式を関数ポインタ化

Last updated at Posted at 2024-12-04

みなさん、ラムダ式を関数ポインタに変換したいですよね??

ラムダ式を関数ポインタに変換できると、関数ポインタを受け取る関数に直接ラムダ式を渡せて便利です。

うれしいことに、キャプチャを含まないラムダ式は関数ポインタに暗黙変換できます。

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 変数を使うことでキャプチャを含むラムダ式も関数ポインタに変換することができました。
これでコーディング効率爆アゲ間違いなし!
ぜひ活用してみてください!

  1. 規格書からこれに関する記述は見つけられませんでした。どこに書いてあるんだ?

  2. 「すべて」は嘘です。 ... に対応してないので。

  3. ジェネリックラムダを返すようにすれば引数や戻り値を気にせず実装できるんですが、なんか楽しくなっちゃって……。

7
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?