C++20の新機能Rangesを使ってみたい! [ソフトウェア/PC関係]
先日,数十年ぶりに本業のプログラム開発でC++言語を使う機会を得た。この間はJavaやらPythonやらを使った開発仕事しかなかったのだ。今回は単発仕事とは言え,使用する言語仕様も自由にしてよいという制限の少ないものだったので,心置きなく新しい標準仕様(世間ではModern C++と呼ばれることが多いようだ)を使わせてもらった。とは言え新しい機能は私自身よく理解できていなかったものも多く,そういう意味では限定的ではあったが。しかも使用するライブラリがVC++の/stdc:c++17オプション(ISO C++17規格)でコンパイルできないものがあったせいで最新のものは使えなかったのだが。
C++の標準化に関わっている身としては最新規格の機能を全てちゃんと理解しておくべきなのだが,仕事でガッツリ使う機会がないとなかなか理解は深まらないものなのである。実際今回の仕事を通して理解が深まったものも多いし,ついでにちょっと学習意欲が高まったこともあり,以来いろいろ文献を読んだりしている。そんな中,今ちょっと興味が高まっているのはRangesである。これは文字通りC++で範囲を扱うための仕様で,2020年発行の規格改訂版C++20で新規に取り込まれたものだ。C++の標準化の進め方として,ある程度規模の大きい機能は,まずTechnical Specification(TS)という独立した仕様(といっても現行のC++の拡張仕様という形)にまとめて,単体で議論し,実際に使ってみてから,それを元にブラッシュアップしてC++本体仕様に取り込むという手順になっている。そんな訳でRangesが最初に提案されたのはだいぶ前でTSの時点で目にはしていたのだったがちゃんと読んでなかったということか,どうも内容を間違って認識していた事がわかった。お恥ずかしい限りだが,この際ちゃんと理解して使えるようにしておきたい。で,範囲を扱うというのは間違いではないのだが,単に範囲を扱うクラスを導入すると言うだけでなく,C++の標準ライブラリを巻き込んだ影響度の大きい仕様だったのだ。
ざっくり説明するとコンテナ・クラスとそれを扱うためのアルゴリズム・ライブラリの改訂ということになる。コンテナクラスというのはvector,list,map,setなど,複数の同じタイプのデータをまとめて管理・処理するためのクラスである。C++でアルゴリズムというとコンテナのインスタンスに含まれるデータを対象に何らかの処理を行うライブラリである。代表的なものにはソートがある。アルゴリズムがコンテナを処理するためには普通コンテナに含まれるデータに順次アクセスできる必要がある。しかしながらコンテナと呼ばれる各クラスはそれぞれ目的もデータの管理の仕方も別々であるし,必ずしも継承関係にないので,そのままでは全部別々のアクセスの仕方をする必要がある。つまりソート一つとっても,コンテナ・クラス別に用意しなければならなくなってしまう。これはあまりに非効率だ。ソートのアルゴリズムは共通なのにデータのアクセスの仕方が違うというだけで,アルゴリズムを実装したコードがコンテナ・クラスの数だけ重複して存在することになる。それでは,メンテナンスの観点でもよろしくない。これをどのように解決したかと言うとIterator(反復子)である。Iteratorは各コンテナ・クラスとは別のクラスであって,大雑把に言うとポインタである。典型的にはコンテナの中の1つのデータを指すポインタであって,*演算子を適用することでデータの値を読める。そして++演算子を適用することで次のデータを指すように変化する。そしてコンテナ内のデータの末端を表すもう一つのIteratorの値と同値になるまで++演算子の適用を繰り返すことでコンテナ内のすべてのデータを読めるようになっている。Iteratorは各コンテナクラス毎に,演算子オーバーロードを駆使して定義されるが全て同じインターフェースを継承しているのでIteratorに対応したアルゴリズムは,異なる種類のコンテナ・クラスを共通して扱えるというわけだ。このようにIteratorは先頭のデータを指すものと,末端を指すもの(通常は最後のデータの次を指す)の2つを組にして利用するようになっている。先頭と末端で囲まれたデータ,ということはこれは範囲にほかならない。Rangesが組み込まれる以前はIteratorのペアとして範囲を扱っていたのだ。例えば整数のvectorをソートすることを考えてみる。Ranges以前は次のようになっていた。
#include <algorithm> #include <iostream> #include <vector> using namespace std; int main() { vectorvec {10, 5, 3, 7, 1, 4}; sort(vec.begin(), vec.end()); for(auto x : vec){ cout << x << endl; } return 0; }
begin関数はvectorクラスの先頭の要素を指すIteratorを返す関数,end関数は末尾を指すIteratorを返す関数である。そしてsort関数がIteratorのペアを引数にソートを実行するアルゴリズム関数である。これをコンパイルして実行すると次のように出力される。
user@hostname ~/cpp $ g++ sort_ex.cpp -std=c++20 -o sort_ex user@hostname ~/cpp $ ./sort_ex 1 3 4 5 7 10
これはこれで,そういうものだと思えば納得なのだが,いちいちbeginとendを自分で呼び出してやるのは煩雑である。こんな決まりきったコードをプログラマに書かせないで済ませるのがオブジェクト指向の良いところだろう。シンプルにコンテナのインスタンスをポンと渡してやって済ませられないだろうか。Rangesはこの問題を解決してくれる。仕組みとしてはbeginとendの2つのIteratorを1つにまとめて扱うためのデータ型のようなものを導入しているのだが,endの部分が少し変更になっている。Ranges以前はbeginとendが必ず同じクラスのメンバーでないといけないという制限があったがそれがなくなり,単に番兵(sentinel)と呼ばれる,末端を判断するためにbeginと等値比較ができれば良いようになった。なんだかよくわからないかも知れないが,これはとりあえず自分でコンテナ・クラスを定義したりしない限り余り用がない。普通に使うならbeginとsentinelは表に出てくることはないからだ。先程のsortの例は,Rangesバージョンだと次のように書ける。
#include <algorithm> #include <iostream> #include <vector> using namespace std; int main() { vectorvec {10, 5, 3, 7, 1, 4}; ranges::sort(vec); for(auto x : vec){ cout << x << endl; } return 0; }
違いはstd名前空間のsort関数ではなくstd::ranges名前空間のsort関数を使うことと,2つのIteratorの代わりにコンテナ・インスタンスそのものを引数に渡すところでだけある。これだけでもだいぶシンプルになったのが分かるだろう。このようにRangesではもともと存在していたアルゴリズム・ライブラリをRanges対応に置き換えたライブラリが提供されるのである。RangesがSTL(Standard Template Library) v2とも呼ばれているのはそうした理由からであろう。C++の最初のISO標準であるC++98から存在しているSTLがより近代的に改訂されたのである。とは言え,sort関数の引数が1つになったくらいではそれほど大した話ではないと思うだろう。実際Rangesの恩恵はそんなものではないのだ。そうしたことについてはまた次回。
コメント 0