2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

(続) C++ で python の enumerate, zip 表記を使う。

Last updated at Posted at 2024-08-26

Python の for 文は C++ と比べてかなり充実しています。C++ でも同じような表記ができると便利だろうということで、C++17 の範囲 for と構造化束縛を組み合わせて、enumerate と zip を用いたループ処理を簡潔に実現するクラスを作成したので、公開します。

前回の記事 https://qiita.com/kkoba775/items/1849eaecf47789be4921 では関数の返り値など、一時オブジェクトを渡すことができませんでしたが、これを可能にしました。

(1) zip を実現するクラス

zip.hpp
#include <utility>
#include <type_traits>

template<class T1, class T2>
struct Zip {
    T1 c1_;
    T2 c2_;

    using iterator1 = decltype(std::begin(c1_));
    using iterator2 = decltype(std::begin(c2_));
    using terminator1 = decltype(std::end(c1_));
    using terminator2 = decltype(std::end(c2_));

    Zip(T1 && c1, T2 && c2)  : 
        c1_(std::forward<T1>(c1)), c2_(std::forward<T2>(c2)) {}

    struct terminator {
        terminator1 t1_;
        terminator2 t2_;
    };

    struct iterator {
        iterator1 i1_;
        iterator2 i2_;

        using V1 = decltype(*i1_);
        using V2 = decltype(*i2_);

        bool operator!=(terminator const& a) const {
            return (i1_ != a.t1_) && (i2_ != a.t2_);
        }

        std::pair<V1, V2> operator*() const {
            return {*i1_, *i2_};
        }

        iterator& operator++() {
            ++i1_;
            ++i2_;
            return *this;
        }
    };

    auto begin() {
        return iterator {std::begin(c1_), std::begin(c2_)};
    }

    auto end() {
        return terminator {std::end(c1_), std::end(c2_)};
    }
};

template<class T1, class T2>
inline auto zip(T1 && a, T2 && b) {
    return Zip<T1, T2>(std::forward<T1>(a), std::forward<T2>(b));
}

使用例

#include <iostream>
#include <vector>
#include <set>
#include <string>

#ifndef __cpp_structured_bindings
#error C++17 is required for the structured_bindings feature.
#endif

#include "zip.hpp"

int main() {

    std::vector<int> A = {1, -2, 4};
    std::set<std::string> B;
    B.insert("a");
    B.insert("z");
    B.insert("b");

    for (auto [n, s] : zip(A, B)) {
        std::cout << n << ", " << s << std::endl;
    }    

    return 0;
}

n, s に A, B の要素の参照が順番に渡されてループします。
A, B のサイズが違う場合には、少ない方に合わせてループを終了します。

zip() の引数が左辺値参照(実体)だった場合には、Zip のメンバ変数に参照変数が用意されます。引数が右辺値参照(一時オブジェクト)だった場合には、Zip のメンバ変数に実変数が用意されて、ムーブコンストラクタによって初期化されます。

(2) enumerate を実現するクラス

enumerate.hpp
#include <utility>
#include <type_traits>

template<class T, class Int = int>
struct Enumerate {
    T c_;

    explicit Enumerate(T && c) : c_(std::forward<T>(c)) {}

    using Iterator = decltype(std::begin(c_));
    using Terminator = decltype(std::end(c_));

    struct iterator {
        Iterator i_;
        Int c_ = 0;

        using Value = decltype(*i_);

        bool operator!=(Terminator const& a) const {
            return (i_ != a);
        }

        auto operator*() const {
            return std::pair<Int, Value>{c_, *i_};
        }

        iterator& operator++() {
            ++i_;
            ++c_;
            return *this;
        }
    };

    auto begin() {
        return iterator {std::begin(c_), 0};
    }

    auto end() {
        return std::end(c_);
    }
};

template<class T>
inline auto enumerate(T && a) {
    return Enumerate<T>(std::forward<T>(a));
}

カウンタは int よりも size_t の方がいい場合もあるので、テンプレート引数にしました。(関数の方は用意していない。)

C++17 から、範囲for に渡すコンテナ(今回は Enumerate<>) の begin() と end()で型の不統一が許されているので、begin() が返す反復子にのみカウンタを用意しています。

使用例

#include <iostream>
#include <set>
#include <string>

#ifndef __cpp_structured_bindings
#error C++17 is required for the structured_bindings feature.
#endif

#include "enumerate.hpp"

int main() {

    std::set<std::string> B;
    B.insert("a");
    B.insert("z");
    B.insert("b");

    for (auto [i, s] : enumerate(B)) {
        std::cout << "[" << i << "] " << s << std::endl;
    }

    return 0;
}

i にはループカウンタの値 0, 1, 2 が、s には B の要素が順に渡されてループします。 std::set<> の内容は昇順に読み出されるので、出力は "a", "b", "z" の順になります。s の型は const std::string& なので、これを介して B の内容を書き換えることはできません。

(3) 一時オブジェクトの使用

C++23 より前では範囲 for の右側の引数が参照によって一時変数を受け取っていても、一時変数の寿命の引き伸ばしが行われません。

このために、前回のバージョンでは enumerate() や zip() に一時オブジェクト(関数の返り値など)を渡すことができませんでした。今回は、右辺値参照を移動によって保管しているので、関数の返り値を渡したり、 zip や enumerate を入れ子にして使うことができます。

#include <iostream>
#include <vector>

#include "zip.hpp"
#include "enumerate.hpp"

std::vector<int> iota(int n) {
    std::vector<int> r;
    r.resize(n);
    for (int i=0; i < n; i++) r[i] = i;
    return r;
}

int main() {
    double D[] = {1.0, -2.1, 3.14};

    for (auto [i, z] : enumerate(zip(D, iota(5)))) {
        auto [a, n] = z;
        std::cout << i << " = " << n << ", " << a << std::endl;
    }
    return 0;
}

(4) 可変数入力への対応

を参考にして、複数の入力に対応しました。上記に比べて簡素な実装になっていますが、実用上は問題ないと思います。

また、enumerate() に複数の入力ができるようにしてあります。enumerate(A, B) は enumerate(zip(A, B)) の代わりになります。

zipn.hpp
#include <tuple>

template<typename ... T>
struct ZipN {
    std::tuple<T...> c_;

    using iterators = std::tuple<decltype(std::begin(std::declval<T&>())) ...>;
    using terminators = std::tuple<decltype(std::end(std::declval<T&>())) ...>;
    using values = std::tuple<decltype(*std::begin(std::declval<T&>())) ...>;

    ZipN(T && ... c)  : c_{std::forward<T>(c) ...} {}

    struct iterator {
        iterators i_;

        template<size_t ... Index>
        bool ok(terminators const& e, std::index_sequence<Index...>) const {
            return ((std::get<Index>(i_) != std::get<Index>(e)) && ...);
        }

        bool operator!=(terminators const& a) const {
            return ok(a, std::index_sequence_for<T...>{});
        }

        auto operator*() const {
            return std::apply([](auto && ... args){
                return values{*args...};
            }, i_);
        }

        iterator& operator++() {std::apply([](auto && ... args){
                ((++args), ...); 
            }, i_);
            return *this;
        }
    };

    auto begin() {
        return iterator{std::apply([](auto && ... args){
            return iterators{std::begin(args)...};
        }, c_)};
    }

    auto end() {
        return std::apply([](auto && ... args){
            return terminators{std::end(args)...};
        }, c_);
    }
};

template<typename ... T>
inline auto zip(T && ... t) {
    return ZipN<T...> {std::forward<T>(t)...};
}

template<typename Int = int>
struct counter {
    struct iterator {
        Int c_ = 0;
        auto operator*() const {return c_;}
        void operator++() {++c_; }
        bool operator!=(iterator) const {return true;}
    };
    auto begin() const {return iterator{0};}
    auto end() const {return iterator{};}
};

template<typename ... T>
inline auto enumerate(T && ... t) {
    return ZipN<counter<>, T...> {{}, std::forward<T>(t)...};
}

使用例

#include <vector>
#include <iostream>

#include "zipn.hpp"

int main() {
    const int a[] = {1, 2, 3, 4, 5, 6};
    auto b = std::vector<int>(std::begin(a), std::end(a));
    auto c = std::vector<int>(5);

    for (auto && [x, y, z] : zip(a, b, c)) {
        z = x + y;
    }

    for (auto [i, x, y] : enumerate(b, c)) {
        std::cout << i << ":" << x << ", " << y << std::endl;
    }
    return 0;
}

ループの回数はサイズが最小の c にあわせられるので、出力は

0:1, 2
1:2, 4
2:3, 6
3:4, 8
4:5, 10

となります。

Visual Studio 2022, GCC 9.4.0 で動作確認。

(5) CMake 対応

ヘッダファイル1つのみのライブラリではありますが、CMake から利用できるようにしました。

2
1
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?