0. はじめに
C++17は2020年6月現在使える最も新しいC++のバージョンです1。C++は1983年に公開されてから継続的にアップデートが行われていて「昔のC++・今のC++」と呼び分けても良いほど、進化を続けています。
gccやclangを利用している場合は以下のコマンドでC++17のコードをコンパイルすることができます。
$ g++ -std=c++17 main.cpp
$ clang++ -std=c++17 main.cpp
本記事では範囲for文とC++17で追加された構文「構造化束縛」の紹介と、それを用いたまるでPythonのような使い心地のforループ(rangeとenumerate)の実現方法を紹介します。
1. 範囲for文
1.1. 基礎
範囲for文とは配列やコンテナ(vector
, list
, map
など)のfor文を簡単にする構文です。
using std::vector;
vector<int> values;
// 従来
for (int i = 0; i < (int)values.size(); i++) { values[i]; }
for (auto it = values.begin(); it != values.end(); ++it) { *it; }
// 範囲for文
for (auto& value : values) { value; }
ループの処理とは関係ない変数や式がなくなり、とてもスッキリします。また複数形→単数形という自然な命名がしやすいのも魅力的です。
Pythonであれば次の文と同じですね。
print(type(values)) # <class 'list'>
for value in values: value
C++は型の宣言が必要な言語なので、Pythonほど簡潔にはかけませんが、ほぼほぼ同じコード量で同じ処理が実現できています。
また、Pythonで見かけるこんな書き方
for i in [2, 3, 5, 7, 11]: print(i, end=' ') # 2 3 5 7 11
これ、C++でもできます。
# include <iostream>
using namespace std;
int main() {
for (auto& i : {2, 3, 5, 7, 11}) cout << i << " "; // 2 3 5 7 11
cout << endl;
return 0;
}
{2, 3, 5, 7, 11}
でイテラブルな配列をfor文の中で定義することができます。ただ、型が違うものを入れ込むことはできないので注意してください({3, '.', 1, 4}
など)。
1.2. 連番でループ~rangeを実現~
C++のfor文は初期化式・条件式・更新式を毎回書かなければならず、面倒くさいだの、分かりにくくて初心者に優しくないだの散々言われてきました。だってPythonなどの言語ではこんな風に書けるから。
for i in range(5): print(i, end=' ') # 0 1 2 3 4
for i in range(1, 6): print(i, end=' ') # 1 2 3 4 5
for i in range(0, 7, 2): print(i, end=' ') # 0 2 4 6
C++でもなんとかできないでしょうか……
さて、連番を作る関数としてC++11からstd::iota
関数がサポートされています。
vector<int> ar(5);
iota(ar.begin(), ar.end(), 1); // ar : {1, 2, 3, 4, 5}
これを使うと次のようなrangeライブラリを簡単に作ることができます。
# include <numeric>
# include <vector>
# include <iostream>
using namespace std;
vector<int> range(int n) { // [0, n)
vector<int> ar(max(0, n));
iota(ar.begin(), ar.end(), 0);
return ar;
}
vector<int> range(int a, int b) { // [a, b)
vector<int> ar(max(0, b - a));
iota(ar.begin(), ar.end(), a);
return ar;
}
vector<int> range(int a, int b, int s) { // [a, b)
vector<int> ar;
for(int i=0;a+i*s<b;i++){ ar.push_back(a+i*s); }
return ar;
}
int main()
{
for (int i : range(5)) cout << i << " "; // 0 1 2 3 4
cout << endl;
for (int i : range(1, 6)) cout << i << " "; // 1 2 3 4 5
cout << endl;
for (int i : range(0, 7, 2)) cout << i << " "; // 0 2 4 6
cout << endl;
return 0;
}
そう技巧的なことはせずとも実現できることがわかると思います。
将来的にはC++20でfor (int i : std::iota(1, 6)) { ... }
が実装され、標準で今のようなfor文が書けるようになります。
2. 構造化束縛
構造化束縛とは簡単に言うと2つ以上の返り値を実現するための構文です。
Pythonでは次のコードが許されたと思います。
def func():
p = 10
q = 20
return p, q
a, b = func()
print(a, b) # 10 20
これをC++でも実現しようじゃないかというのが構造化束縛です。具体的には次のように書きます。
# include <tuple>
# include <iostream>
using namespace std;
tuple<int, int> func() {
int p = 10;
int q = 20;
return {p, q}
}
int main() {
auto [a, b] = func();
cout << a << " " << b << endl; // 10 20
return 0;
}
C++は静的型付け言語であるため、返り値のtupleの型を明示しなければなりません。そこに目をつぶればバッチリ再現できていますね!
ちなみに構造化束縛を使えば他にこんなコードが書けます。
# include <tuple>
# include <vector>
# include <iostream>
using namespace std;
pair<int, int> pfunc() {
return {2, 3};
}
tuple<int, char, vector<int>> t3func() {
return {3, 'P', {2, 3, 5}};
}
struct Human {
Human() = default;
Human(int a, char s, string n):age(a),sex(s),name(n){}
int age;
char sex;
string name;
};
int main() {
auto [fst, snd] = pfunc();
auto [n, type, ar] = t3func();
auto [age, sex, name] = Human(12, 'F', "Mary");
return 0;
}
構造化束縛は、複数の返り値を実現するための構文とも言えますし、クラス・構造体のメンバ変数を分解するための構文とも言えるでしょう。
3. 範囲for文と構造化束縛の合わせ技~enumerateを実現する~
Pythonにおけるenumerate
とはリストの要素とインデックスを同時に得るための組み込み関数です。
for i, fib in enumerate([1, 1, 2, 3, 5]):
print('F({}) = {}'.format(i+1, fib))
### F(1) = 1
### F(2) = 1
### F(3) = 2
### F(4) = 3
### F(5) = 5
これをC++で実現することを考えます。
3.1. タブル配列の範囲for文
範囲for文と構造化束縛を組み合わせれば、次のようなコードを書くことができます。
# include <vector>
# include <tuple>
# include <iostream>
using namespace std;
int main() {
vector<tuple<int, int>> tpls = {{1, 1}, {2, 4}, {3, 9}, {4, 16}};
for (auto [a, b] : tpls) {
cout << a << " " << b << endl;
}
return 0;
}
1 1
2 4
3 9
4 16
レガシーC++と比べるとびっくりしちゃいますね。
3.2. enumerateの実装
enumerate関数を実装します。これまでの知識を使いこなせば書けますね!
# include <vector>
# include <tuple>
# include <numeric>
# include <iostream>
using namespace std;
vector<int> range(int n) {
vector<int> ar(max(0, n));
iota(ar.begin(), ar.end(), 0);
return ar;
}
template <class Type>
vector<tuple<int, Type>> enumerate(vector<Type> ar) {
int n = ar.size();
vector<int> indexes = range(n);
vector<tuple<int, Type>> res(n);
for (int i : range(n)) res[i] = make_tuple(indexes[i], ar[i]);
return res;
}
int main() {
for (auto [i, value] : enumerate<int>({1, 1, 2, 3, 5})) {
printf("L(%d) = %d\n", i, value);
}
return 0;
}
int型の配列だけでなく、そのほかの型でも使えるようにするためテンプレートを使いました2。
enumerateの型を明示的に指定しなければならないかどうかは状況によります。
vector<int> ar = {1, 1, 2, 3, 5};
for (auto [i, value] : enumerate(ar) { ... } // ok
for (auto [i, value] : enumerate({1, 1, 2, 3, 5})) { ... } // ng
for (auto [i, value] : enumerate<int>({1, 1, 2, 3, 5})) { ... } // ok
for (auto [i, value] : enumerate(vector<int>{1, 1, 2, 3, 5})) { ... } // ok
可変長引数テンプレートを使えば更に汎用的なenumerateを実装できるのですが、それは別記事にしたいと思います。
4. おわりに
C++17を使えばこんなにもモダンなコードを書くことができます。C++に対するイメージも変わったのではないでしょうか?
レガシーなC言語っぽい書き方もできれば、モダンな言語っぽい書き方もできるのがC++の面白いところだと思います。今回は使っていませんが、ラムダ式を使えばさらに表現力が高まります。
ぜひ、C++17を試してみてください!