C++入門/制御3

柴田祐樹,東京都立大学,情報科学科

ここではC++に用意されている制御機能の発展的な事柄を説明する.匿名関数(ラムダ式)が扱われる.C++では関数もその関数に対応する型の変数へ入れることができる.この型は,戻り値の型をA,引数の型をBとしたとき,function<A(B)>と表される.B が複数からなる場合は,カンマ「,」で区切る.これにより,次のように関数を変数に代入して,変数の内容に応じて呼び出す関数が変わる,といったコードを記述できる.

関数の型とその変数の利用例
#include <iostream>
#include <functional>
using namespace std;
void f(){
    cout << "call f\n";
}
void g(){
    cout << "call g\n";
}
int main(){
    function<void(void)> a;

    a = f;
    a();
    a();
    a = g;
    a();
    return 0;
}

上記をl.cpp として保存し,コンパイルから実行まで行った結果を以下に示す.3回目の出力が call g と変わっていることから,呼び出す関数が変化している,つまり意図した動作となっていることがわかる.

[shibata@127 cpp]$ g++ l.cpp [shibata@127 cpp]$ ./a.out call f call f call g

関数は基本的にCからの伝統でグローバル領域(最上位範囲)に定義しなければならなかったが,C++ではより一般的に,どこでも定義できるようになっている.既存の関数との混乱を避けるためか,しかしC++ではこのどこでも定義できる関数をラムダ式(関数)とよび,普通の関数と区別する.使い方は次のとおりである.ラムダ式は関数を定義した瞬間に変数へ代入する必要があるため,g の初期化部に定義を書いている.このコードはと全く同じ動作をする.

#include <iostream>
#include <functional>
using namespace std;
void f(){
    cout << "call f\n";
}
int main(){
    function<void(void)> a;

    function<void(void)> g = [&](){
        cout << "call g\n";
    };

    a = f;
    a();
    a();
    a = g;
    a();
    return 0;
}

[&] の部分は,参照する変数を指定する箇所であるが,基本は定義された範囲(スコープ)の変数はすべて参照できたほうが良く,このことを示す[&]を記載しておけば良い.もし,特定の変数だけ関数内で参照できるようにしたい場合は,次のように書く.このコードはコンパイルに失敗するので注意してもらいたい.10行目を削除すれば動くようになる.関数 g 内で参照できるのは,[]内に指定した a だけであるから,この挙動となる.繰り返しになるがよほどのことがなければ[&]と記載して,すべて参照できるようにすることを勧める.

#include <iostream>
#include <functional>
using namespace std;

int main(){
    int a=1;
    int b=1;
    function<void(void)> g = [&a](){
        cout << "call g, " << a << "\n";
        cout << "call g, " << b << "\n"; // NG
    };

    g();

    return 0;
}

また,ラムダ関数は,定義して変数に格納することなくその場で呼び出すことが可能である.ラムダ式の定義直後に,関数を呼び出す演算子である() が記載されている点に注意してもらいたい.

#include <iostream>
#include <functional>
using namespace std;

int main(){
    [](){
        cout << "call λ\n";
    }();
    return 0;
}

この性質がどのように役立つかを説明する.第一に,ラムダ関数は内では,return を使って,関数の処理をすべて中断することが可能であるため,以下のように多重の繰り返しを一度に終了することが可能となる.

#include <iostream>
#include <functional>
using namespace std;

int main(){
    int r = [](){
        int i=1;
        while(i < 100){
            int j=1;
            while(j < 100){
                j++;
                if(i*i + j*j == 25){
                    return i + j;
                }
            }
            i++;
        }
    }();
    cout << r << "\n";
    return 0;
}

これは,平方の和が25となる整数の組を探すプログラムである.13行目に書いてあるreturnにより,6行目のrへ値が返される.returnが書いてあったとしても,main関数が終了することなく,このラムダ式が終了するにとどまる.また,本来 while を2つ抜けるためには,break を2回書かなければならないが,この方法であれば,returnを一箇所記載するだけですべての繰り返しを終了することが可能である.多重の繰り返しを終了したい場合は,このようにラムダ式をその場で呼び出すやり方を覚えておくと良い.

以下のように,if に指定する条件の判定に論理式だけでなく手続きを必要とする場合にも使える.ラムダ式はint型の引数を一つ受け取るように定義され,定義した場所で直ちにint型の引数を与えられて呼び出されている.ただこの書き方は2022年現在一般に普及しているコードと比べると高度で読み手が限られるように思えるので,使う場面に注意するのが良いだろう.

#include <iostream>
#include <functional>
using namespace std;

int main(){
    int N = 10000;
    if([&](int a){
        int i=1;
        while(i <= N){
            if(i*i == a){
                return true;
            }
            i ++;
        }
        return false;
    }(2209)){
        cout << "条件を通過\n";
    }
    else{
        cout << "条件が不成立\n";
    }
    return 0;
}