【C++】Pythonのzip()関数(っぽいもの)を実装する

C++

はじめに

C++11で追加された範囲for(range-based for)文は、コンテナや配列の全要素にアクセスするために簡潔に記載できる便利な記法です。

std::vector<int> a = [1, 2, 3];

for(auto & i : a) {
    std::cout << i << std::endl;
}

範囲for文は便利な記法ですが、1つのコンテナや配列に対してしか使うことができません。Pythonなどを使っていると、以下のように複数のリストやディクショナリなどを1つのfor文で扱えるzip()関数の存在が便利に感じることがあります。

a = [1, 2, 3]
b = ["aaa", "bbb", "ccc"]

for i, j in zip(a, b):
  print(f"{j}: {i}")

一方、C++にはC++17で追加された構造化束縛というものがあります。構造化束縛を使うと、タプルの要素を簡単に分割して代入できるようになります。

std::tuple<int, std::string> a = {1, "aaa"};

auto [i, j] = a;
std::cout << j << ": " << i << std::endl;

これを利用すれば、似たようなことができそうです。そこで、タプルを使用して複数のコンテナをひとまとめにするコンテナZipperクラスと、Zipperクラスのインスタンスを返すZip()関数を作成してみました。

実装と使用例

実装

実際の実装はGitHubにアップしたソースコードをご覧ください。

独自のコンテナに範囲for文を適用できるようにするためには、コンテナのメンバ関数として、
begin()end()を実装する必要があります。そして、begin()end()はイテレータを返さなければなりません。

これらの関数が返すイテレータは最低限次の演算子が定義されている必要があります。

  1. !=演算子
  2. ++演算子
  3. *演算子

すなわち、次のような構造になります。

template <typename T>
class MyContainer
{
public:
    class Iterator
    {
    public:
        bool operator !=(Iterator) { ... };
        Iterator & operator ++() { ... };
        T & operator *() { ... };
    };

    Iterator begin() { ... }
    Iterator end() { ... }
};

Zipperクラスには、これらをすべて実装してあります。

Zipperクラスが可変引数テンプレートとしてコンテナを取るのに対し、イテレータの*演算子の戻り値は、各コンテナの要素のタプルとなります。コンテナの要素の型はT::iterator::value_typeもしくはT::iterator::reference(参照型の場合)で得られますが、これは配列には使うことができません。そこで、配列にも対応するため、前エントリで作成したGetReference<>を使い、要素の型を取得しています。

また、イテレータの!=演算子にも工夫をしています。Zipper::Iteratorクラスはメンバ変数として、各コンテナのイテレータのタプルを持っています。これで実際の現在位置を管理しています。タプル自体は比較演算子を持っているので直接これを比較することも可能ですが、これだと各コンテナの要素数が異なる場合にZipper::end()との判定で問題が出てきます。そこで、Zipper::end()の戻り値をZipper::EndIteratorという別のクラスとし、Zipper::IteratorZipper::EndIteratorとの比較という形で専用の処理で対処しました。ひとつでも終端に到達していたらループを抜ける必要があるため、==演算子を実装してこちらでZipper::end()との判定を行い、それを否定する形で!=演算子を実装しました。

Zipper::end()との比較はパラメータパックの展開を利用します。以下のようにするとパラメータパックの展開でboolの配列が作成できます。

bool IsEnd(const EndIterator & it, std::index_sequence<N...>) const
{
    bool chk[] = {(std::get<N>(iter_) == std::get<N>(it.iter_))...};

    return std::any_of(std::begin(chk), std::end(chk), [](bool x) { return x; });
}

この配列をstd::any_of()アルゴリズムを使って判定します。

使用例

以下に使用例を示します。zipper.hが同じディレクトリにあるとします。

#include <iostream>
#include <list>
#include <map>
#include <string>
#include <vector>

#include "zipper.h"

int main()
{
    std::vector<int> a = {1, 2, 3};
    std::list<std::string> b = {"a", "b", "c"};
    std::map<int, std::string> c = {{0, "abc"}, {1, "def"}, {2, "ghi"}};

    for (auto [s, t, u] : Zip(a, b, c)) {
      std::cout << s << " " << t << " " << u.first << " " << u.second << std::endl;
    }

    return 0;
}

まとめ

タプルを利用してPython風のZip()関数を自作してみました。複数のコンテナに対して1つの範囲for文でまとめて扱えるようになります。

なお、C++23ではstd::views::zip()関数<ranges>で標準化されています。C++23が使える環境ではそちらを使ったほうが良いです。

std::vector<int> a = {1, 2, 3};
std::list<std::string> b = {"aaa", "bbb", "ccc"};

for(auto & [i, j] : std::views::zip(a, b)) {
    std::println("{}: {}", j, i);
}

C++

Posted by izadori