はじめに
C言語から派生したオブジェクト指向プログラミング言語であるC++は、21世紀に入ってまったく別物とも言えるプログラミング言語に成長していきました。それは、Modern C++と称されています。1990年代にC++を触っていたプログラマが現在の仕様を知れば、隔世感に苛まれるのではないでしょうか。本連載では、かつてはC++をたしなんでいたという方、今からC++言語を始めるという方に向けて、Modern C++らしい言語仕様をピックアップし紹介していくことで、今のC++言語の姿を理解していただきます。
対象読者
- かつてはC++をたしなんでいたという方
- 今からC++言語を始めるという方
- モダンなプログラミング言語のパラダイムに興味のある方
必要な環境
本記事のサンプルコードは、以下の環境で動作を確認しています。
-
macOS Sonoma / Windows 11
- Xcode Command Line Tools 2395
- MinGW GCC 9.2.0
ラムダ式って何よ?
20世紀のC++プログラマ(筆者)が、今(21世紀)のC++を知ってあっと驚く連載の第5回です。ムーブセマンティクスという山を越えたので、あとは楽になる一方だと楽観しています。そこでラムダ式です(単にラムダと呼ぶこともあるようです)。C++を最近はやっていなくても、他のプログラミング言語を触っていれば耳にしたことのある言葉ですね。知らなくても、大昔にLispとか触ったことがあれば、もしかしてアレに関係あるのかな?と思ったあなたは鋭いです。今回は、このラムダ式を中心に書きます。
[NOTE]C++プログラムのコンパイル
以降のサンプルは、基本的に「gcc -o 出力先ファイル名 ソースファイル名」でコンパイルできますが、環境によってはライブラリやC++バージョンの指定が必要になることがあります。コンパイルやリンクでエラーが発生するときには、以下のようにライブラリの指定(-lstdc++)、C++バージョンの指定(-std=c++11)の追加を検討してください。
% gcc -o sample sample.cpp -lstdc++ -std=c++11 // c++14, c++17, c++20など
まずは関数のポインタから
C言語からたしなんでいれば、関数のポインタというものを使ったことはあると思います。普通のポインタだけでも分からないのに関数のポインタなんて!と筆者も若いときにはそう思いましたが、今となってはなんて甘っちょろかったのだろうと自責の念にとらわれています。関数のポインタ、以下のリストみたいに宣言して使います(例によって#includeやnamespaceはスペースの都合で割愛しているので、全体は配布サンプルを参照してください)。
int func() { return 0; } // 関数 …略… int (*fp)() = &func; // 関数ポインタ (*fp)(); // ポインタを使った関数呼び出し
なんてことはなく、関数ポインタとはポインタ演算子と識別子をカッコで囲んで(こっちをカッコで囲むのは優先順位の問題です)、関数であることを示すカッコを付けて宣言するだけですね。そして初期値は関数の名前にアドレス参照演算子を付けると、簡単です。とはいえ、使いどころに悩むのも事実です。自身のプロジェクトで、関数のポインタにあまり活躍させると破綻への道まっしぐらなので、ここはライブラリ関数に登場してもらいましょう。よく知っているqsort関数です。クイックソートを実行する標準ライブラリの関数ですが、並び換えの際の順序比較を外部関数に行わせる仕様になっているんですね。qsort関数のプロトタイプ宣言はこんな感じです。
void qsort(void *base, size_t num, size_t size, int (*compare)(const void *, const void *))
baseをsizeの大きさでnum要素ある配列と見なし、最終引数のcompare関数(ポインタ)で比較して並び替える、というものです。今ならテンプレートとか使ってスマートにやるのでしょうが、C言語なので。筆者的にはこういう内部まる見えの感じは好きですが。これを呼び出すコードは、以下のリストのようになります。
// 比較関数(a > bなら正、a < bなら負、a == bなら0が返る) int compare_func(const void *a, const void *b) { return *(int *)a - *(int *)b; } …略… int array[] = {9, 6, 8, 4, 2, 5, 1, 10, 3, 7}; qsort(array, sizeof(array) / sizeof(int), sizeof(int), compare_func); for( int i = 0; i < sizeof(array) / sizeof(int); i++ ) { cout << array[i] << endl; }
並び替えにあたり、配列arrayの各要素の大小をcompare_func関数が比較して、0または正負の値で返す、という感じです。qsort関数自体は、どのような比較が行われているのは知るよしもなく、その結果に従って淡々と並び換えを実行するのです。逆を言えば、どんな並び替えもcompare_func関数次第で可能(昇順と降順の切り替えとか、あるいは未知の比較方法でも)ということで、関数の汎用性を上げることができます。
関数ポインタってこんな感じです。思い出しましたか? ちょっと危ないのは、compare_func関数へは各要素のサイズを渡すことができないので、どのようなサイズを想定して比較しているかも完全にお任せだということでしょう。
関数オブジェクトを定義するラムダ式
しかしながら、上記リストのコードは冗長ではないですか? qsort関数に渡すためだけの関数を名前まで付けて定義しているので、結構ラムダ……じゃなかった、ムダな感じがします。また、関数定義が離れたところにあるのも可読性という点ではイマイチな気がします。
そこで、関数そのものをオブジェクトとして扱えたら便利じゃない? という発想が各所で生まれました(生まれたというより元からあったんですが、それを手続き型のプログラミング言語に持ってこようという発想ですね)。
これが、C++ 11で導入されたラムダ式(lambda expressions)です。ラムダ式は、簡易的な関数オブジェクトをその場で定義できる機能、とあります。簡易的というと、その場限りとか、そういう意味でしょうね。イベントハンドラやqsortに渡す関数なんかは、その場限りというか相手が限定されますから、その場その場で定義できる方が便利でしょう。
[NOTE]なぜラムダ?
ラムダ式という名称は、ある数学者の考案したラムダ計算から来ていると言われます。ラムダ計算のモデルでは、関数を表すのにラムダ文字(λ)を使ったからとも言われます。Lisp言語でlambdaを使うのもここから来ているようです。なので、根っこは一緒のようです。
ラムダ計算では、例えば関数f(x)=x+1は、λx.x+1と表現されるようです(「ようです」が続くのは、筆者が数学を大の苦手としているからです)。しかし、これってずいぶんとなじみのある表現のような…、他言語でラムダとか匿名関数とかかじったなら、ピンと来そうな表現ですね。
高階関数と第一級関数
ついでですが、ここで高階関数についても紹介します。やだなぁ、こういうの。高層マンションの上の方に住んでいる関数?みたいな印象があって、あまり良い印象がないのですよね(偏見)。もちろん、そんな意味ではなく、高階関数とは「関数そのものを引数で受け取ったり、関数そのものを戻り値として返す関数」のことです。さらに、第一級関数(first-class function)という概念もあります。高階関数は、第一級関数をサポートするプログラミング言語で、さらに上記の条件を満たす関数、とちょっと複雑な話になってきました。第一級関数とは、オブジェクトと見なすことができる関数、ということです。なので、関数ポインタはあるがオブジェクトと見なすことはできない(そもそもオブジェクトという考え方がない)C言語などでは、第一級関数は存在しないということになります。必然的に高階関数もないというわけです。
高階関数は、関数型プログラミング言語では、普通に実装されているということになっています。