コンテンツにスキップ

C++/ジェネリックラムダ

出典: フリー教科書『ウィキブックス(Wikibooks)』
< C++

ジェネリックラムダの概要

[編集]

C++11から導入されたラムダ式は、無名の関数オブジェクトを簡潔に記述できる機能です。C++14ではさらに、その型推論の仕組みが拡張され、ジェネリックラムダが実現されました。

ジェネリックラムダとは、パラメータの型を自動的に推論し、複数の型に対応できるラムダ式のことです。これにより、テンプレート関数のような汎用的なコードを、よりシンプルに記述することができます。

ジェネリックプログラミングの本質は、異なる型に対して同じ処理を行うことです。ジェネリックラムダはその考え方を組み込んでおり、コードの重複を避けて抽象化を実現する手段となります。

auto lambda = [](auto x, auto y) { return x + y; };
std::cout << lambda(1, 2.3) << std::endl; // 3.3
std::cout << lambda(4, 5) << std::endl;   // 9

上の例では、autoキーワードによりパラメータ型が推論されています。これにより、整数や浮動小数点数など、様々な型の組み合わせで同じラムダ式を使用できます。

ジェネリックラムダの構文

[編集]

続きまして、ジェネリックラムダの構文について説明します。

パラメータ型の推論

[編集]

ジェネリックラムダでは、autoキーワードを使ってパラメータの型を推論します。

[](auto x, auto y) { /* 処理 */ }

上記のように、パラメータ名の前にautoキーワードを付けると、そのパラメータの型が呼び出し時の実引数から推論されます。

複数のパラメータ型の推論

[編集]

1つのラムダ式で複数の型を扱う場合、すべてのパラメータにautoを付ける必要があります。

auto lambda = [](auto x, auto y, auto z) { return x + y + z; };
lambda(1, 2, 3.4); // エラー: 型の不一致
lambda(1.2, 3.4, 5.6); // OK

戻り値型の推論

[編集]

C++14では、ラムダ式の戻り値の型推論もサポートされました。-> autoを使うと、戻り値型が式から推論されます。

auto lambda = [](auto x, auto y) -> auto { return x + y; };

この場合、xyの型から戻り値型が決定されます。

テンプレートパラメータの制約

[編集]

ジェネリックラムダ内で、パラメータの型を制約することもできます。

auto lambda = [](auto& x, auto& y) -> auto& { 
    return (x + y) ? x : y; 
};

可変引数の扱い

[編集]

ジェネリックラムダは可変長引数をサポートしており、これが大きな利点の1つです。可変長引数を受け取るには、ラムダ式の引数リストにauto&&...を使用します。

auto lambda = [](auto&&... args) {
    // 可変長引数argsを処理する
};

この構文では、argsstd::tupleの様な集合を表し、渡された引数がパックされた形で受け取れます。

例えば、以下のようにフォールディング式を使えば、可変長引数の集合を処理できます。

auto sum = [](auto&&... args) {
    return (args + ... + 0); // 引数の合計値を返す
};

std::cout << sum(1, 2.3, 4, 5.6) << std::endl; // 12.9

ここでは(args + ... + 0)という二項フォールディング式により、argsの各要素を足し合わせています。 + 0は冗長なように見えますが、引数が1つもなかった場合への配慮です。

可変引数ラムダは様々な用途に役立ちます。

  • 任意の個数の引数を取る汎用的な関数オブジェクト
  • コンテナの展開呼び出し
  • 可変引数テンプレートの代替
  • ロギングやデバッグ用のトレースハンドラ

さらに、可変引数ラムダは部分適用にも使えます。

auto bindLambda = [](auto&&func, auto&&... args) {
    return [&](auto&&... moreArgs) {
        return func(std::forward<decltype(args)>(args)..., 
                    std::forward<decltype(moreArgs)>(moreArgs)...);
    };
};

auto add = [](auto... vals) { return (vals + ... + 0); };
auto add3 = bindLambda(add, 1, 2.3); 
std::cout << add3(4, 5.6) << std::endl; // 12.9

可変引数の扱いは、ジェネリックラムダの大きな魅力です。しかし、コードが複雑になる可能性もあるため、注意深く使う必要があります。適切な使い分けが肝心です。

実例によるジェネリックラムダの活用

[編集]

ジェネリックラムダは、簡単な処理から複雑なアルゴリズムまで、さまざまな場面で役立ちます。

簡単な数値計算

[編集]
auto add = [](auto x, auto y) { return x + y; };
std::cout << add(1, 2.3) << std::endl; // 3.3
std::cout << add(4, 5) << std::endl; // 9

コンテナアルゴリズムでの利用

[編集]

ジェネリックラムダは、STLアルゴリズムと組み合わせて利用できます。

std::vector<int> nums = {1, 5, 3, 7, 2};
int sum = std::accumulate(nums.begin(), nums.end(), 0, [](int x, int y) { return x + y; });

多態的オブジェクトの振る舞いカスタマイズ

[編集]

基底クラスのメンバ関数をジェネリックラムダでオーバーライドすれば、derivedクラスごとに異なる振る舞いを実装できます。

struct Base {
    virtual int process(int x) const {
        return processImpl(x);
    }
    std::function<int(int)> processImpl;
};

struct Derived1 : Base {
    Derived1() { processImpl = [](int x) { return x * 10; }; }
};

struct Derived2 : Base {
    Derived2() { processImpl = [](int x) { return x * x; }; }
};

このようにジェネリックラムダは、コードの重複を避けた汎用的な実装を可能にします。

続けて、ジェネリックラムダとテンプレート、ラムダ式オブジェクトの利用について説明します。

ジェネリックラムダとテンプレート

[編集]

ジェネリックラムダは一種のテンプレートメタプログラミングの機能ですが、従来のテンプレートとは異なる点があります。

テンプレートパラメータの制約

[編集]

関数テンプレートでは、テンプレート引数に制約を付けることができますが、ジェネリックラムダではできません。ただし、パラメータにリファレンスなどの修飾を付加することで、ある程度の制約は可能です。

// 関数テンプレート
template <typename T, typename std::enable_if<std::is_integral<T>::value, int>::type = 0>
void foo(T x) {...}

// ジェネリックラムダ 
auto lambda = [](auto& x) -> typename std::enable_if<std::is_integral<decltype(x)>::value>::type {...}

ジェネリックラムダと関数テンプレートの使い分け

[編集]

一般に、ジェネリックラムダは小規模な処理に適しています。一方、大規模で複雑なジェネリックコードには関数テンプレートを使うことが推奨されます。ジェネリックラムダは構文が簡潔ですが、テンプレートに比べて機能面での制限があるためです。

ラムダ式オブジェクトの利用

[編集]

ラムダ式を評価すると、その式に対応する一時的なファンクタオブジェクト(ラムダ式オブジェクト)が生成されます。

auto lambda = [](int x) { return x * 10; };
int result = lambda(42); // ラムダ式オブジェクトを呼び出す

関数オブジェクトの代替

[編集]

ラムダ式オブジェクトは、std::functionや関数ポインタと同様の役割を果たしますが、よりシンプルに記述できます。

// 従来の関数オブジェクト
struct Multiplier {
    int operator()(int x) const { return x * 10; }
};
Multiplier mult;
std::cout << mult(5) << std::endl; // 50

// ラムダ式による書き換え
auto lambda = [](int x) { return x * 10; };
std::cout << lambda(5) << std::endl; // 50

関数ポインタとの違い

[編集]

ラムダ式オブジェクトは、外部変数のキャプチャが可能です。関数ポインタではこの機能はありません。

int x = 42;
auto lambda = [x](int y) { return x * y; }; // xをキャプチャ
std::cout << lambda(10) << std::endl; // 420

ジェネリックラムダは単なるラムダ式の拡張ですから、ラムダ式オブジェクトとしての性質は変わりません。

はい、続けてジェネリックラムダの注意点とパフォーマンスについて説明します。

ジェネリックラムダの注意点

[編集]

ジェネリックラムダは便利ですが、いくつかの注意点があります。

名前付き変数のキャプチャ

[編集]

ラムダ式内でキャプチャした変数が変更された場合、その変更は反映されるか否かはキャプチャの方法によって異なります。

int x = 1;
auto get_x_val = [&x] { return x; };  // x を参照でキャプチャ
x = 2;
std::cout << get_x_val() << std::endl; // 2

auto get_x_copy = [x] { return x; };  // x をコピーでキャプチャ 
x = 3;
std::cout << get_x_copy() << std::endl; // 1

無名変数のキャプチャ

[編集]

ラムダ式内で参照するが定義されていない変数は、外側のスコープから探索され、自動的にキャプチャされます。このことを意識しないと、意図しないキャプチャによってバグの原因になるおそれがあります。

int x = 1; 
auto lambda = [=](int y) { return x + y; }; // x は自動キャプチャ
auto lambda2 = [](int y) { return x + y; }; // エラー、x が見つからない

ムーブキャプチャ

[編集]

C++14から導入されたムーブキャプチャにより、一時オブジェクトをムーブ代入してキャプチャできます。ただし、ムーブ後のオブジェクトの状態には注意が必要です。

再代入可能なキャプチャ

[編集]

ジェネリックラムダ内でキャプチャした変数に対する操作は、基本的にはコピーキャプチャと同様に扱われます。キャプチャした変数を変更するには、ムーブセマンティクスやラムダ式オブジェクトを介した方法が必要になります。

ジェネリックラムダのパフォーマンス

[編集]

インライン化の影響

[編集]

ラムダ式オブジェクトは通常はインライン化の対象となるため、実行時パフォーマンスへの影響は小さいと考えられています。ただし、最適化レベルによっては、インライン化されない可能性もあります。

過剰なコード膨張への対処

[編集]

ジェネリックラムダを多用するとコード膨張が生じる可能性があります。この場合、関数テンプレートを使うか、ラムダ式を別の関数に切り出すなどの対策が必要になります。

コンパイル時間と実行時パフォーマンスはトレードオフの関係にあるため、用途に応じて適切な設計を心がける必要があります。

続けて、ジェネリックラムダの章の最後にまとめを加えます。

まとめ

[編集]

ジェネリックラムダは、C++14で導入された強力な機能です。型推論とラムダ式の組み合わせにより、柔軟で汎用的なコーディングが可能になりました。

ジェネリックラムダの主な利点は以下の通りです。

  • コードの簡潔化と重複の削減
  • コンテナアルゴリズムとの親和性
  • 多態的な振る舞いのカスタマイズ
  • テンプレートメタプログラミングの一種としての活用

一方で、ジェネリックラムダの使用には以下の点に注意が必要です。

  • キャプチャした変数の扱い
  • 無名変数の意図しないキャプチャ
  • ムーブキャプチャの影響
  • 過剰なコード膨張の可能性

また、小規模な処理であれば便利ですが、複雑なジェネリックコードには関数テンプレートを使うことが推奨されます。

総じてジェネリックラムダは、ジェネリックプログラミングの恩恵を手軽に享受できる有用な機能です。一方で、その便利さゆえに注意点も多いため、適切な使い分けが重要になります。

C++がさらに発展を遂げるなかで、ジェネリックラムダの役割がどのように変化していくのか、今後の動向に注目する必要があります。