【C++17】範囲for文と構造化束縛によるコーディングテクニック ～Pythonライクを求めて～


# 0. はじめに

C++17は2020年6月現在使える最も新しいC++のバージョンです[^1]。C++は1983年に公開されてから継続的にアップデートが行われていて「昔のC++・今のC++」と呼び分けても良いほど、進化を続けています。

[^1]: C++20というバージョンが2020年中に改訂される予定です。`-std=c++2a`オプションで有効にできる場合があります。

gccやclangを利用している場合は以下のコマンドでC++17のコードをコンパイルすることができます。

```sh
$ 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文を簡単にする構文です。

```cpp
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であれば次の文と同じですね。

```py
print(type(values))  # <class 'list'>
for value in values: value
```

C++は型の宣言が必要な言語なので、Pythonほど簡潔にはかけませんが、ほぼほぼ同じコード量で同じ処理が実現できています。

また、Pythonで見かけるこんな書き方

```list-loop.py
for i in [2, 3, 5, 7, 11]: print(i, end=' ')  # 2 3 5 7 11
```

これ、C++でもできます。

```list-loop.cpp
# 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などの言語ではこんな風に書けるから。

```py
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++でもなんとかできないでしょうか……

<br>

さて、連番を作る関数としてC++11から`std::iota`関数がサポートされています。

```cpp
vector<int> ar(5);
iota(ar.begin(), ar.end(), 1);  // ar : {1, 2, 3, 4, 5}
```

これを使うと次のような**rangeライブラリ**を簡単に作ることができます。

```range-for.cpp
# 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では次のコードが許されたと思います。

```py
def func():
    p = 10
    q = 20
    return p, q

a, b = func()
print(a, b)  # 10 20
```

これをC++でも実現しようじゃないかというのが構造化束縛です。具体的には次のように書きます。

```cpp
# 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の型を明示**しなければなりません。そこに目をつぶればバッチリ再現できていますね！

ちなみに構造化束縛を使えば他にこんなコードが書けます。

```cpp
# 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`とは**リストの要素**と**インデックス**を**同時**に得るための組み込み関数です。

```py
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文と構造化束縛を組み合わせれば、次のようなコードを書くことができます。

```cpp
# 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関数を実装します。これまでの知識を使いこなせば書けますね！

```enumerate.cpp
# 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の型を明示的に指定しなければならないかどうかは状況によります。

[^2]: 任意の型に対して関数やクラスを定義できる強力な機能です。

```例.cpp
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を試してみてください！