「C++の基礎 - クロージャ」の版間の差分

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動
(文字列「__FORCETOC__」を「{{#seo: |title={{PAGENAME}} : Exploring Electronics and SUSE Linux | MochiuWiki |keywords=MochiuWiki,Mochiu,Wiki,Mochiu Wiki,Electric Circuit,Electric,pcb,Mathematics,AVR,TI,STMicro,AVR,ATmega,MSP430,STM,Arduino,Xilinx,FPGA,Verilog,HDL,PinePhone,Pine Phone,Raspberry,Raspberry Pi,C,C++,C#,Qt,Qml,MFC,Shell,Bash,Zsh,Fish,SUSE,SLE,Suse Enterprise,Suse Linux,openSUSE,open SUSE,Leap,Linux,uCLnux,Podman,電気回路,電子回路,基板,プリント基板 |description={{PAGENAME}} - 電子回路とSUSE Linuxに関する情報 | This pag…)
1行目: 1行目:
== 概要 ==
== 概要 ==
C++では、クロージャを作成することができる。<br>
クロージャとは、関数オブジェクトの一種であり、その関数が定義された環境の変数をキャプチャして保持できる機能である。<br>
クロージャとは、環境(状態)を持った関数オブジェクトのようなものである。<br>
C++ 11から導入されたラムダ式を使用することにより、簡単にクロージャを作成できるようになった。<br>
<br>
<br>
作成のポイントとして、ラムダ式をコピーキャプチャとした上でmutableにすることである。<br>
クロージャの重要な特徴は、関数の外側にある変数を捕捉できることである。<br>
クラスのメンバ変数等を使用せずに状態を保持できるため、大変便利なものといえる。<br>
例えば、ある関数の中で定義された変数を、その関数内で作られたクロージャが参照し続けることができる。<br>
これにより、データと振る舞いを一緒にカプセル化することが可能になる。<br>
<br>
変数のキャプチャ方法には、主に以下に示すようなものがある。<br>
* 値キャプチャ
*: 変数のコピーを保持
* 参照キャプチャ
*: 変数への参照を保持
* デフォルトキャプチャ
*: 全ての変数を一括でキャプチャ
<br>
クロージャは特にアルゴリズムやイベントハンドリング、非同期処理等で重宝される。<br>
例えば、<code>std::sort</code>の比較関数として使用したり、コールバック関数として利用することができる。<br>
<br>
また、クロージャは状態を持つことができるため、関数型プログラミングの考え方を取り入れたコードを記述する場合にも便利である。<br>
関数オブジェクトとして扱えるため、STLアルゴリズムとも相性が良く、コードの可読性と保守性の向上に貢献する。<br>
<br>
ただし、参照キャプチャを使用する際は、参照先の変数のライフタイムに注意する必要がある。<br>
参照先の変数が破棄された後にクロージャを使用すると、未定義動作を引き起こす可能性がある。<br>
<br><br>
 
== 基本的な作成 ==
必ずしも、ラムダ式をコピーキャプチャかつミューテーブルにする必要はない。<br>
<br>
コピーキャプチャ <code>[=]</code> とは、キャプチャした変数の値をクロージャ内部にコピーして保持するものである。<br>
元の変数との関係が切れるため、安全に使用することができる。<br>
ただし、メモリ使用量が増える可能性がある。<br>
<br>
<code>mutable</code>キーワードとは、デフォルトでは、コピーキャプチャされた変数はクロージャ内で変更できない。<br>
mutableキーワードを使用する場合、コピーキャプチャした変数を変更できる。<br>
<u>ただし、これはクロージャ内部のコピーを変更するだけであるため、元の変数は変更されない。</u><br>
<br>
<u>以下に示す場合は、<code>mutable</code>キーワードは不要である。</u><br>
* キャプチャした変数を読み取りのみで使用する場合
* 参照キャプチャ <code>[&]</code> を使用している場合
* クロージャ内で状態を保持する必要がない場合
<br>
<u>ただし、以下に示す場合は<code>mutable</code>が必要になる。</u><br>
* コピーキャプチャした変数をクロージャ内で変更する場合
* クロージャ自体が状態を持つ必要がある場合
<br><br>
<br><br>



2024年10月23日 (水) 23:26時点における版

概要

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

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

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

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


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

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

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


基本的な作成

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

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

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

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

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


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

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



サンプルコード

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


出力
1
2
3
1
2
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
    f2(1); // 1
    
    f1(2); // 0
    f2(1); // 2
    
    f1(1); // 1
    f2(2); // 1
 
    f1(2); // 0
    f2(2); // 0
    
    f1(); // 0
    f2(); // 0
 }


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

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