Menu
Coddy logo textTech

ラムダ式を深く理解する

CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 85/104。

C++11で導入されたラムダ式は、インラインで定義できる匿名関数です。STLアルゴリズムを使った基本的なラムダはすでに見てきましたが、その完全な構文を理解すると、変数をキャプチャし、そのアクセス方法を制御する強力な機能を利用できるようになります。

lambda の完全な構文は次のとおりです: [capture](parameters) mutable -> return_type { body }capture clause は、lambda が外部のどの変数に、どのようにアクセスできるかを決定します:

#include <iostream>

int main() {
    int x = 10;
    int y = 20;
    
    auto byValue = [x]() { return x * 2; };        // xのコピー
    auto byRef = [&y]() { y += 5; };               // yへの参照
    auto allByValue = [=]() { return x + y; };     // すべてをコピー
    auto allByRef = [&]() { x++; y++; };           // すべてを参照
    auto mixed = [x, &y]() { y += x; };            // 両方をミックス
    
    byRef();
    std::cout << y << "\n";  // 25
}

デフォルトでは、値でキャプチャされた変数はラムダ内で const になります。mutable キーワードを使用すると、これらのコピーを変更できます。

int counter = 0;
auto increment = [counter]() mutable {
    return ++counter;  // ラムダのコピーを変更する
};

std::cout << increment() << "\n";  // 1
std::cout << increment() << "\n";  // 2
std::cout << counter << "\n";      // 0 - オリジナルは変更されない

C++14では初期化キャプチャが追加され、新しい変数を作成したり、オブジェクトをラムダに移動したりできるようになりました:

auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
    return *p;
};  // 所有権がラムダに移譲された

ラムダは、別個の関数オブジェクトを定義せずに、コールバック、カスタム比較関数、イベントハンドラーなどのために振る舞いをパラメーターとして渡す必要がある場合、OOPで特に便利です。

challenge icon

チャレンジ

簡単

異なるキャプチャモードでラムダ式の威力を示すイベントハンドラーシステムを構築しましょう。コールバックを保存して呼び出すシンプルなイベントディスパッチャーを作成し、ラムダが外部状態をさまざまな方法でキャプチャできることを示します。

コードを3つのファイルに分けて整理します。

  • EventDispatcher.h: イベントcallbackを管理するEventDispatcherクラスを定義します。

    dispatcherは、std::function<void()>std::vectorを使ってcallbackを保存します。次のメソッドを含めます。

    • addCallback(std::function<void()> callback): callbackをリストに追加する
    • fireAll(): 保存されているすべてのcallbackを順番に呼び出す
    • clear(): すべてのcallbackを削除する

    <functional><vector>をインクルードする必要があります。

  • EventDispatcher.cpp: dispatcherのメソッドを実装します。fireAll()メソッドでは、すべてのcallbackを単純に反復処理し、それぞれを呼び出します。
  • main.cpp: 2つの入力を読み取ります。
    1. 基数(整数)
    2. 乗数(整数)

    EventDispatcherを作成し、3つのcallbackを追加して異なるラムダキャプチャ手法を示します。

    1. 基数を値でキャプチャし、Base value: [base]と出力するラムダ
    2. 乗数を参照でキャプチャし、1増加させてからMultiplier after increment: [multiplier]と出力するラムダ
    3. カウンター変数(0で初期化)を値でキャプチャし、呼び出されるたびに増加させ、Call count: [counter]と出力するmutableラムダ

    すべてのcallbackを追加した後、fireAll()を2回呼び出して、異なるキャプチャモードが複数回の呼び出しでどのように動作するかを確認します。2回のfireAll()呼び出しの間に、区切り文字として---を出力します。

    最後に、両方のラウンドが終わった後、参照キャプチャがどのように影響したかを示すため、main内の乗数変数の最終値をFinal multiplier: [multiplier]と出力します。

たとえば、入力が105の場合:

Base value: 10
Multiplier after increment: 6
Call count: 1
---
Base value: 10
Multiplier after increment: 7
Call count: 1
Final multiplier: 7

入力が420の場合:

Base value: 42
Multiplier after increment: 1
Call count: 1
---
Base value: 42
Multiplier after increment: 2
Call count: 1
Final multiplier: 2

重要な動作に注目してください。値によるキャプチャでは元の基数が変更されず、参照によるキャプチャではmain内の実際の乗数変数が変更され(呼び出しのたびに累積され)、mutableラムダのカウンターは毎回のラウンドで1になります。これは、fireAll()がcallbackを値で反復処理するためです。各ラウンドでは、保存されたstd::functionの新しいコピーが呼び出されます。このコピーのキャプチャされたカウンターは0から始まり、1に増加するため、保存されたcallback自体の状態は決して進みません。

自分で試してみよう

#include <iostream>
#include "EventDispatcher.h"

using namespace std;

int main() {
    int base;
    int multiplier;
    cin >> base;
    cin >> multiplier;
    
    EventDispatcher dispatcher;
    
    // TODO: base を値でキャプチャするラムダを追加する
    // "Base value: [base]" を出力すること
    
    // TODO: multiplier を参照でキャプチャするラムダを追加する
    // It should increment multiplier by 1, then print: "Multiplier after increment: [multiplier]"
    
    // TODO: カウンタ(0 で初期化)を値でキャプチャする MUTABLE なラムダを追加する
    // カウンタをインクリメントして "Call count: [counter]" を出力すること
    
    // TODO: すべてのコールバックを呼び出すために fireAll() を呼び出す
    
    // TODO: 区切りとして "---" を出力する
    
    // TODO: 再度 fireAll() を呼び出す
    
    // TODO: Print the final multiplier value: "Final multiplier: [multiplier]"
    
    return 0;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: C++オンラインコンパイラ