Python の for 文は C++ と比べてかなり充実しています。C++ でも同じような表記ができると便利だろうということで、C++17 の範囲 for と構造化束縛を組み合わせて、enumerate と zip を用いたループ処理を簡潔に実現するクラスを作成したので、公開します。
前回の記事 https://qiita.com/kkoba775/items/1849eaecf47789be4921 では関数の返り値など、一時オブジェクトを渡すことができませんでしたが、これを可能にしました。
(1) zip を実現するクラス
#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 を実現するクラス
#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)) の代わりになります。
#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 から利用できるようにしました。