SNSへはこちら

C++でrangeを用いたPython風の繰り返し処理を実装する

C++ で繰り返し処理をするのって、面倒ですよね。わざわざ初期化式、ループ継続式、変化式を書く必要があります。
これらは IDE の力を借りる等、スニペットによってその手間はいくらかは省けますが、それでもめんどいし、見た目が良くないし...

C++ の N 回ループはこちらです。

for(int i = 0; i < N; i++) { // ただの回数繰り返しなのに見た目がうるさい
    hoge();
}

一方で Python では楽に書くことができます。見た目もよろしい。

for i in range(N): # rangeでスッキリ
    hoge()

本記事では、このようなスッキリとした繰り返しの表記にできるだけ近づけることを目的とします。
なお、C++14 以前は古代であるという考えを持っているので(C++14 は中世)、それ以前の規格は知りません。

完成形が見たい方は記事一番下までどうぞ

テンプレートにおけるパラメータパック

パラメータパックとは、テンプレートにおいて任意長の引数を詰めたものと考えられます。C で使われていた va_list の代わりです。以下ではその使用例を見ていきましょう。

パラメータパックを用いた例

引数の数値を全て足し上げる sum 関数の実装例です。

#include <iostream>

template <typename T>
T sum(T first) {
    return first;
}
template <typename T, typename ... Args>
T sum(T first, Args ... others) {
    return first + sum(others...);
}

int main(){
    std::cout << sum(1, 2, 3, 4, 5); // 15
    return 0;
}

ポイントは2つ目の sum 関数の定義です。typename ... Args とすることで、Args には可変の「型でできた配列(のようなもの)」が格納されます。これがパラメータパックです。main 関数での呼び出しだと、T = int, Args = {int, int, int, int} になっています。
直後の仮引数では Args ... others としていますが、これはテンプレート上で宣言した Args を展開するという感じです。パラメータパックに ... を後置するとパック展開の意味になります... の後ろに後置した名前は、数値がパックされたもの(others = {2, 3, 4, 5})になります。
C++14 まで、パラメータパックの展開は再帰しか方法がないので、上のように中身を記述しています。そしていよいよ最後の sum(5) が呼び出されると、上部に書いてある T sum(T) が優先的に適用され、めでたく有限回で再帰が終わる訳です。

なお C++17 以降だと畳み込み(folding)が使えるのでより簡潔に記述できます。下では T を返り値型として採用しています。もっといいやり方があるかも...

template <typename T, typename ... Args>
T sum(T first, Args ... args) {
    return first + (args + ...);
}

int main(){
    std::cout << sum(1, 2, 3, 4, 5); // 15
    return 0;
}

std::make_integer_sequence

これがループ実装のキモになります。これは型名なのですが、実体化すると 0 から指定した数までの連番が含まれた std::integer_sequence という constexpr な構造体を生成します。ということは、この std::integer_sequence のテンプレート引数をパラメータパックで取得してしまえば...というわけです。

std::make_integer_sequence<int, 5>{}; // std::inter_sequence<int, 0, 1, 2, 3, 4> のインスタンスが生成される

ところで連番をどう使いましょうか?そうですね、こうすればループ変数も使えていい感じでしょうか(白々しい顔)。

for( auto i : {0, 1, 2, 3, 4} ) { // 連番そのものを要素とした配列(std::array<int, 5>)を作ってしまう
    std::cout << i << std::endl;
}

そんなわけで、std::integer_sequence の引数をパラメータパックに詰めて、そのパックを関数の返り値に展開してしまえばいいですね。さぁやってみましょう。

#include <iostream>
#include <utility>
#include <array>

template <typename T, T ... args> // argsは型Tのパック
auto makeArray(std::integer_sequence<T, args ...>) -> std::array<T, sizeof...(args)> {
    return {{args ...}}; // 初期化子リストで記述してみた
}

int main(){
    for( auto i : makeArray(std::make_integer_sequence<int, 5>{}) ) {
        std::cout << i << std::endl; // 0, 1, 2, 3, 4 が各行に表示
    }
    return 0;
}

makeArray ではテンプレート部にて値のパラメータパック args を展開してしまっています。sizeof... を使うとパラメータパックの要素数を返してくれるので、std::array の要素数指定部分で使用しているのです。
あとはここの std::make_integer_sequence<int, 5>{} という長ったらしい記述をラップする関数を追加するだけです。ということで、C++14 以降で使える完成形を示しましょう。

// For C++14 (C++17以降向けはこの下)

#include <iostream>
#include <utility>
#include <array>

template <typename T, T ... args>
constexpr auto range_(std::integer_sequence<T, args ...>) -> std::array<T, sizeof...(args)> {
    return {{args ...}};
}
template <int N>
constexpr auto range() {
    return range_(std::make_integer_sequence<decltype(N), N>{});
}

int main(){
    for( auto item : range<5>() ) {
        std::cout << sizeof(item) << " " << item << std::endl;
    }
    return 0;
}

range<5>() という記述が奇妙ですが仕方がない。テンプレート引数が関数の引数に適用できても、逆は成り立たないからです。
また、C++14 以前では非型パラメータ引数の型で auto を利用できないため int でハードコードしてしまっています。

C++17 以降では以下のコードをおすすめします。

// For C++17 or later(C++14は上のコードを使用する)

#include <iostream>
#include <utility>
#include <array>

template <typename T, T ... args>
constexpr auto range_(std::integer_sequence<T, args ...>) -> std::array<T, sizeof...(args)> {
    return {{args ...}};
}
template <auto N>
constexpr auto range() {
    return range_(std::make_integer_sequence<decltype(N), N>{});
}

int main(){
    for( auto item : range<5>() ) {
        std::cout << sizeof(item) << " " << item << std::endl;
    }
    return 0;
}

初の C++ の記事です。C++勢はコワイ人が多いなあなんて思っていて記事を渋っていたのですが、本日記事書いちゃいました。説明に間違いがあるならぜひツッコミをどうぞ。

参考サイト