C++の基礎 - クロージャ

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動

概要

クロージャとは、関数オブジェクトの一種であり、その関数が定義された環境の変数をキャプチャして保持できる機能である。
C++ 11から導入されたラムダ式を使用することにより、簡単にクロージャを作成できるようになった。

クロージャの重要な特徴は、関数の外側にある変数を捕捉できることである。
例えば、ある関数の中で定義された変数を、その関数内で作られたクロージャが参照し続けることができる。
これにより、データと振る舞いを一緒にカプセル化することが可能になる。

変数のキャプチャ方法には、主に以下に示すようなものがある。

  • 値キャプチャ
    変数のコピーを保持
  • 参照キャプチャ
    変数への参照を保持
  • デフォルトキャプチャ
    全ての変数を一括でキャプチャ


クロージャは特にアルゴリズムやイベントハンドリング、非同期処理等で重宝される。
例えば、std::sortの比較関数として使用したり、コールバック関数として利用することができる。

また、クロージャは状態を持つことができるため、関数型プログラミングの考え方を取り入れたコードを記述する場合にも便利である。
関数オブジェクトとして扱えるため、STLアルゴリズムとも相性が良く、コードの可読性と保守性の向上に貢献する。

ただし、参照キャプチャを使用する際は、参照先の変数のライフタイムに注意する必要がある。
参照先の変数が破棄された後にクロージャを使用すると、未定義動作を引き起こす可能性がある。


基本的な作成

必ずしも、ラムダ式をコピーキャプチャかつミューテーブルにする必要はない。

コピーキャプチャ [=] とは、キャプチャした変数の値をクロージャ内部にコピーして保持するものである。
元の変数との関係が切れるため、安全に使用することができる。
ただし、メモリ使用量が増える可能性がある。

mutableキーワードとは、デフォルトでは、コピーキャプチャされた変数はクロージャ内で変更できない。
mutableキーワードを使用する場合、コピーキャプチャした変数を変更できる。
ただし、これはクロージャ内部のコピーを変更するだけであるため、元の変数は変更されない。

以下に示す場合は、mutableキーワードは不要である。

  • キャプチャした変数を読み取りのみで使用する場合
  • 参照キャプチャ [&] を使用している場合
  • クロージャ内で状態を保持する必要がない場合


ただし、以下に示す場合はmutableが必要になる。

  • コピーキャプチャした変数をクロージャ内で変更する場合
  • クロージャ自体が状態を持つ必要がある場合



クロージャの使用例 : 変数の保持

以下の例では、func関数内でローカル変数xを定義して、それをラムダ式でキャプチャしている。
ラムダ式は、変数xの値をコピーキャプチャ [=] して保持する。

mutableキーワードにより、キャプチャしたxの値を変更可能にしている。
このラムダ式 (クロージャ) は、func関数の実行が終わった後も、キャプチャしたxの値を保持し続ける。

f1とf2は別々のクロージャインスタンスとなり、それぞれが独自のxのコピーを持つ。
そのため、f1の呼び出しではf1が保持するxが1から3まで増加、f2の呼び出しでは新しいクロージャインスタンスが生成されて、そのxが1から3まで増加する。

このように、クロージャは状態 (この場合はxの値) を保持でき、その状態は他のクロージャインスタンスとは独立しているという特徴を示している。

 #include <iostream>
 
 auto func()
 {
    int x = 0;
    return [=]() mutable -> void {
              x++;
              std::cout << x << std::endl;
           };
 }
  
 int main()
 {
    auto f1 = func();
    f1();  // 出力: 1
    f1();  // 出力: 2
    f1();  // 出力: 3
 
    auto f2 = func();
    f2();  // 出力: 1
    f2();  // 出力: 2
    f2();  // 出力: 3
 }


以下の例のように、環境を共有する複数の処理を行う場合は、引数の値により条件分岐することができる。
なお、参照キャプチャは、オブジェクトを複数生成する場合は使用できない。

 #include <iostream>
 
 auto func()
 {
    int x = 0;
    return [=](std::size_t mode = 0) mutable -> void {
              switch(mode) {
                 case 1:
                    ++x;
                    std::cout << x << std::endl;
                    break;
                 case 2:
                    --x;
                    std::cout << x << std::endl;
                    break;
                 default:
                    std::cout << x << std::endl;
                    break;
              }
           };
 }
 
 int main()
 {
    auto f1 = func();
    auto f2 = func();
 
    f1(1);  // 出力: 1
    f1(2);  // 出力: 0
    f1(1);  // 出力: 1
    f1(2);  // 出力: 0
    f1();   // 出力: 0
 
    f2(1);  // 出力: 1
    f2(1);  // 出力: 2
    f2(2);  // 出力: 1
    f2(2);  // 出力: 0
    f2();   // 出力: 0
 }


処理を関数ごとに分ける場合、bind関数等は新しく関数を生成しているため、環境が共有されず使用できない。
処理を分ける場合は、上記のf1関数を生成した後、以下の処理を記述する。

 auto inc = [&]() { return f1(1); };
 auto dec = [&]() { return f1(2); };
 
 inc();
 dec();



クロージャの使用例 : 非同期処理のコールバック

以下の例では、非同期処理のコールバックをクロージャで実装、および、参照キャプチャを使用してカウンタを更新して、
成功・失敗のカウントを保持している。

 #include <iostream>
 #include <functional>
 #include <stdexcept>
 #include <string>
 
 // 非同期処理を模擬した関数
 void simulateAsyncOperation(const std::string& data, std::function<void(bool, const std::string&)> callback)
 {
    try {
       // データの検証
       if (data.empty()) {
          throw std::invalid_argument("データが空です");
       }
 
       // 処理成功時のコールバック実行
       callback(true, "処理結果: " + data);
    }
    catch (const std::exception& e) {
       // エラー発生時のコールバック実行
       callback(false, std::string("エラー: ") + e.what());
    }
 }
 
 int main()
 {
    int successCount = 0;  // 成功回数をカウント
    int failureCount = 0;  // 失敗回数をカウント
 
    // クロージャを作成
    // 成功・失敗のカウントを保持
    auto resultHandler = [&successCount, &failureCount](bool success, const std::string& result) {
       if (success) {
          successCount++;
          std::cout << "成功 (" << successCount << "回目): " << result << std::endl;
       }
       else {
          failureCount++;
          std::cout << "失敗 (" << failureCount << "回目): " << result << std::endl;
       }
    };
 
    // クロージャの実行
    simulateAsyncOperation("テストデータ1", resultHandler);
    simulateAsyncOperation("テストデータ2", resultHandler);
    simulateAsyncOperation("", resultHandler);             // エラーケース
  
    return 0;
 }



クロージャの使用例 : カスタムソート

以下の例では、複雑なソート条件をクロージャで実装している。
複数の条件を組み合わせたソート、値キャプチャによる設定値の保持、構造体の入力値検証している。

 #include <iostream>
 #include <vector>
 #include <algorithm>
 #include <stdexcept>
 
 struct Product {
    std::string name;
    double      price;
    int         stock;
 
    Product(const std::string &n, double p, int s) : name(n), price(p), stock(s) {
       // 入力値の検証
       if (p < 0) throw std::invalid_argument("価格は0以上を入力");
       if (s < 0) throw std::invalid_argument("在庫数は0以上を入力");
    }
 };
 
 int main()
 {
    try {
       // 商品データの作成
       std::vector<Product> products{
          Product("商品A", 1000, 5),
          Product("商品B", 2000, 3),
          Product("商品C", 1500, 0)
       };
 
       // ソート条件の優先順位を設定
       bool   prioritizeStock = true;  // 在庫優先フラグ
       double minPrice        = 1200;  // 価格の閾値
 
       // クロージャでソート条件を定義
       auto sortCondition = [prioritizeStock, minPrice](const Product& a, const Product& b) {
          if (prioritizeStock) {
             // 在庫がある商品を優先
             if ((a.stock > 0) != (b.stock > 0)) {
                return a.stock > 0;
             }
          }
 
          // 価格閾値との関係で比較
          bool a_above_threshold = a.price >= minPrice;
          bool b_above_threshold = b.price >= minPrice;
          if (a_above_threshold != b_above_threshold) {
             return !a_above_threshold;  // 閾値未満を優先
          }
 
          // 同条件の場合は価格の安い順
          return a.price < b.price;
       };
 
       // ソート実行
       std::sort(products.begin(), products.end(), sortCondition);
 
       // 結果表示
       std::cout << "ソート結果:" << std::endl;
       for (const auto &product : products) {
          std::cout << "商品名: " << product.name << ", 価格: " << product.price << ", 在庫: " << product.stock << std::endl;
       }
    }
    catch (const std::exception &e) {
       std::cerr << "エラーが発生: " << e.what() << std::endl;
       return -1;
    }
 
    return 0;
 }