C++ ラムダ式は、C++ 11 で C++ に追加された構造であり、C++ 標準の各バージョンで進化し続けています。今日の言語のコア部分であるラムダ式を使用すると、プログラマーは C++ で無名関数とクロージャーを表現できます。この投稿では、ラムダ式とは何かを説明し、いくつかの基本的な使用法を提供し、その利点を概説します.
基本的な使い方
関数をパラメーターとして渡して関数の動作をカスタマイズすることは、プログラミングの一般的なタスクです。たとえば、標準アルゴリズム ライブラリの構想以来、<algorithm>
の多くのアルゴリズムは 呼び出し可能なエンティティをコールバックとして受け取ることができます。ただし、C++11 より前は、C++ で呼び出し可能なエンティティの種類は、関数ポインターと関数オブジェクトのみでした。どちらもかなりのボイラープレートを必要とし、この煩雑さは実際の標準アルゴリズム ライブラリの適応を妨げることさえあります.
その間、多くのプログラミング言語が無名関数の機能をサポートしています。 C++11 より前では、このような機能はメタプログラミングによって模倣されていました。たとえば、Boost C++ ライブラリはその boost.lambda ライブラリを提供しました。これらのメタプログラミング ハックはコンパイルが遅く、実行時のパフォーマンスに影響を与えるものもあります。さらに、必要以上のボイラープレートが必要です。したがって、C++11 では、ラムダ式が言語拡張として追加されます。例として、ISO C++ 標準は、sort
のコンパレータとしてラムダ式の使用法を示しています。 アルゴリズム:1
#include <algorithm>
#include <cmath>
void abssort(float* x, unsigned n) {
std::sort(x, x + n,
[](double a, double b) {
return (std::abs(a) < std::abs(b));
});
}
関数内 abssort
、ラムダ式のインスタンスを std::sort
に渡しました コンパレーターとして。同じ目的を達成するために通常の関数を書くことができます:
#include <algorithm>
#include <cmath>
bool abs_less(double a, double b) {
return (std::abs(a) < std::abs(b));
}
void abssort(float* x, unsigned n) {
std::sort(x, x + n, abs_less);
}
他の言語のラムダ式に精通している場合は、不可解な []
以外はすべて理解できるはずです。 構文。それが次のトピックです。
キャプチャ
上記の例は、ラムダ式の基本的な使用方法を示していますが、ラムダ式ではさらに多くのことができます。ラムダ式と通常の関数の主な違いは、状態を「キャプチャ」できることであり、ラムダ本体内でキャプチャされた値を使用できます。たとえば、以下の関数は、しきい値を超える要素を古いベクターから新しいベクターにコピーします。
// Get a new vector<int> with element above a certain number in the old vector
std::vector<int> filter_above(const std::vector<int>& v, int threshold) {
std::vector<int> result;
std::copy_if(
std::begin(v), std::end(v),
std::back_insert_iterator(result),
[threshold](int x){return x > threshold;});
return result;
}
// filter_above(std::vector<int>{0, 1, 2, 4, 8, 16, 32}, 5) == std::vector<int>{8, 16, 32}
上記のコードは threshold
をキャプチャします 値によって。 []
構文は キャプチャ句 と呼ばれます .キャプチャには、値によるキャプチャと参照によるキャプチャの 2 種類があります ([&]
)。例:[x, &y]
- x
をキャプチャ 値と y
で 参考までに。デフォルトのキャプチャ句を指定することもできます:[=]
現在の環境のすべてを値と [&]
でキャプチャします 参照によってすべてをキャプチャします。
環境を格納する関数を クロージャー と呼びます;ほぼすべての最新のプログラミング言語がクロージャーをサポートしています。ただし、C++ を除いて、私が知っているすべての言語では、キャプチャ リストは暗黙的です。これらの言語では、クロージャーは現在の環境からすべてのバインディングを取得します。
参照によってすべてをキャプチャすることで、これらの言語の動作を模倣できます ([&]
);ラムダが使用する環境の変数のみをキャプチャします。ただし、デフォルトのキャプチャは C++ では危険な場合があります。ラムダがキャプチャされたオブジェクトよりも長く存続する場合、ダンドリングの問題が発生します。たとえば、コールバックを非同期関数に渡し、参照によってリソースをキャプチャできます。
auto greeter() {
std::string name{"Lesley"};
return std::async([&](){
std::cout << "Hello " << name << '\n';
});
}
上記のコードは name
以降未定義の動作です 非同期操作を実行すると、破棄される可能性があります。経験則では、ラムダの有効期間が短い場合にのみ、参照によるデフォルトのキャプチャを使用します。たとえば、ラムダを STL アルゴリズムに渡す場合。
暗黙的なキャプチャ戦略は、ガベージ コレクションされた言語で機能します。 Rust は、借用チェッカーのおかげで、暗黙的なキャプチャーを回避できます。逆に、プログラマーが所有権について明示する必要があるため、C++ のアプローチは、他のプログラミング言語のアプローチよりも柔軟性が高くなります。
内部のラムダ式
これまで、ラムダの使用法についてかなり議論してきました。しかし、好奇心旺盛な読者は、正確には何なのか疑問に思うかもしれません。 は C++ ラムダ式ですか?関数型言語のクロージャのような原始的な言語構造ですか?ラムダの内部について話す前に、まず C++98 時代にさかのぼる構造体、関数オブジェクトについて話します。 .
一部の C++ プログラマーは、関数オブジェクトを「ファンクター」と呼んでいます。避けるべき誤称です。圏論では、関手は圏間の写像です2 "そして、特定の「関手法則」を満たします。
関数型プログラミング言語は、この概念を言語構造に利用していましたが、この用語はあまりにもオーバーロードされていました。 Standard ML と OCaml では、ファンクターは高階モジュールです。モジュールを別のモジュールにマップするメタ関数と考えることができます。より一般的な使用法は、Haskell およびさまざまなインスピレーションを受けた言語とライブラリから来ており、functor はマッピング操作を定義する型クラスです。ファンクターの Haskell 定義は、C++ でも役立ちます。たとえば、標準の範囲アダプターは、範囲をマップするファンクターと見なすことができます。また、型 std::optional
の場合 そしてexpected
「マップ」操作をサポートすると、ファンクターになります。さまざまなライブラリがすでにそれを実装しており、いくつかの標準的な提案がこの方向に進んでいます 3
.
関数オブジェクト
関数オブジェクトは、呼び出すことができる通常のオブジェクトです。それらは、クラスの operator()
をオーバーロードすることによって実装されます オペレーター。以下は abs_less
です 関数オブジェクトとしての例:
#include <algorithm>
#include <cmath>
class abs_less {
bool operator()(double a, double b) {
return (std::abs(a) < std::abs(b));
}
};
void abssort(float* x, unsigned n) {
std::sort(x, x + n, abs_less{});
}
関数オブジェクトは、通常のオブジェクトのようにデータを格納できるため、通常の関数よりも柔軟です。以前の filter_above
を実装しましょう 関数オブジェクトの例:
template <typename T>
class GreaterThan {
public:
explicit GreaterThan(T threshold): threshold_{threshold} {
}
bool operator()(const T& other) noexcept {
return other > threshold_;
}
private:
T threshold_;
};
std::vector<int> filter_above(const std::vector<int>& v, int threshold) {
std::vector<int> result;
std::copy_if(std::begin(v), std::end(v), std::back_insert_iterator(result), GreaterThan{threshold});
return result;
}
このスニペットでは、クラス テンプレート引数推定 (CTAD) を使用しています。 CTAD は C++17 の機能です。以前のバージョンでは、 GreaterThan<int>{threshold}
と書く必要がありました テンプレート パラメータ int
を使用
ラムダ式に戻る
C++ のラムダ式は、operator()
を持つクラスのシンタックス シュガーです。 実行時に、これらのラムダ式は関数オブジェクトに評価されます。 C++ Insights のすばらしい Web サイトから、abssort
の脱糖バージョンを見ることができます。 例:
#include <algorithm>
#include <cmath>
void abssort(float * x, unsigned int n)
{
class __lambda_6_9
{
public: inline /*constexpr */ bool operator()(float a, float b) const
{
return (std::abs(a) < std::abs(b));
}
...
};
std::sort(x, x + n, __lambda_6_9{});
}
ご覧のとおり、ラムダ式は、ローカル クラスのデフォルトの構築済みオブジェクトを作成します。したがって、C++ ラムダ式は、他の言語の無名関数ではできない多くのことを実行できます。たとえば、ラムダから継承し、ラムダから変更可能な状態を持つことができます。どちらもあまり使い道がありませんが.
コンパイラはラムダ式の型を生成します。ただし、プログラム内の標準的な方法でそのような型を名前で使用する方法はありません。それにもかかわらず、型推論とテンプレートはそれらの型に対して正常に機能します。また、これらの型を decltype
で明示的に使用できます .以下は cppreference の例です:
auto f = [](int a, int b) -> int
{
return a * b;
};
decltype(f) g = f;
このような匿名型は「ヴォルデモートの型」と呼ばれます " C++ と D プログラミング言語の世界では、それらは直接名前を付けることができないためですが、コードは引き続きこの型を使用できます。
イニシャライザによるキャプチャ
これで、ラムダ式がクラスに対する構文糖衣であることがわかりました。ローカル スコープから値をキャプチャするだけでなく、ラムダ式が任意の値を格納することを期待する場合があります。幸いなことに、C++14 では、ラムダ式は initializer でキャプチャすることにより、本体に新しい変数を導入できます。 4 .
[x = 1]{ return x; /* 1 */ }
移動キャプチャ
Rust クロージャーは、環境内の値の所有権を取得できます。 C++ ラムダ式は、そのようなムーブ キャプチャを特別にサポートしていません。 、しかし、C++14 の一般化されたキャプチャはそのようなユース ケースをカバーします:
// a unique_ptr is move-only
auto u = std::make_unique<some_type>(
some, parameters
);
// move the unique_ptr into the lambda
go.run( [u=std::move(u)] {
do_something_with(u);
});
即時呼び出しラムダ式
ラムダ式は、作成した場所と同じ場所で呼び出すことができます。
[]() { std::puts("Hello world!"); }(); // Same as what is inside the curly braces
JavaScript の世界では、JavaScript プログラマーがスコープを導入するために関数式を使用することがあるため、すぐに呼び出される関数式はいたるところにあります。 C++ では、この種の策略は必要ありません。その結果、C++ プログラマーは、すぐに呼び出されるラムダを使用することに消極的になります。たとえば、CppCon 2018 での講演で、Kate Gregory は、このイディオムに慣れていない人にとって、すぐに呼び出されるラムダ式の読みやすさについて懸念しています。
それにもかかわらず、より多くの const
として宣言するベスト プラクティスに従えば、 可能な限り、すぐに呼び出されるラムダ式は利点を提供します。一部のオブジェクトは、コンストラクターの能力を超えた複雑な構築を必要とします。変異は、オブジェクトの構築中にのみ発生します。構築が完了すると、オブジェクトが再び変更されることはありません。そのような構築が再利用可能な場合、ビルダー クラスまたはファクトリ関数を作成することは賢明な選択です。ただし、そのような構成がコードベースで 1 回しか発生しない場合、多くの人が const
を削除します。 代わりに修飾子。たとえば、 stdin
から数行を読み取りたい場合を考えてみましょう ベクトルに:
std::vector<std::string> lines;
for (std::string line;
std::getline(std::cin, line);) {
lines.push_back(line);
}
lines
にする方法はないようです ループ内で変更する必要があるため、一定です。すぐに呼び出されるラムダ式は、このジレンマを解決します。これにより、両方の const
を持つことができます 定型文なし:
const auto lines = []{
std::vector<std::string> lines;
for (std::string line;
std::getline(std::cin, line);) {
lines.push_back(line);
}
return lines;
}();
<オール>