はじめに
range based forは、終了判定条件を明示的書く必要がないのでミスが少なく簡潔にかける。しかし、インデックスはそのままでは扱えないため工夫が必要。もともとは一つの対策案の備忘録として残そうと思っただけだが、コメントで教えていたたので、いろいろなfor文(インデックスありループ処理)を列挙してみた。
今回のコードは、Visual Studio 16.6.2で動作確認を行った。
#手法
##1. インデックス値そのものを制御変数とする
配列でなくvectorを使っているものの、Cの時代から続く古典的な手法。OPENMPも使える。
見慣れているので読む人は理解しやすいが、終了判定条件の記述ミスが発生することがある(<,<=の違いなどで)。
std::vector<int> vec{1, 2, 3};
for(size_t i=0;i<vec.size();i++){
std::cout << i<< ":" << vec[i] << std::endl;
}
##2. イテレータを使って制御する
C++11より前のC++らしい手法。そもそも記述が冗長で、インデックス値をインクリメントするコードの記述が必要になる。
std::vector<int> vec{1, 2, 3};
int i=0;//イテレータをautoで受けるときは、インデックスを外に宣言する必要がある。
for(auto it=vec.begin(); it!=vec.end(); it++,i++){
std::cout << i<< ":" << *it << std::endl;
}
c++17からは、構造化束縛が使ってインデックスをfor文の()の中に宣言できる。読みにやすいかは別にして。
std::vector<int> vec{1, 2, 3};
for(auto [it,i] = std::pair{vec.begin(),0}; it!=vec.end(); it++,i++){
std::cout << i<< ":" << *it << std::endl;
}
##3. ranged based for
c++11から標準に採用された手法。終了判定条件を書く必要がなく簡潔でミスが少なく書ける。しかし、インデックスのインクリメントをfor文の{}の中に記述する必要があり、continueする場合はその前にインデックスをインクリメントすることに注意をする必要がある。
std::vector<int> vec{1, 2, 3};
int i=0;
for(const auto& v : vec){
std::cout << i<< ":" << v << std::endl;
//仮にここでcontinueするとiはインクリメントされない
i++;
}
c++20から、インデックスをfor文の()の中に宣言できる。
std::vector<int> vec{1, 2, 3};
for(int i=0;const auto& v : vec){
std::cout << i<< ":" << v << std::endl;
i++;
}
##4. ラムダ式をつかってループを関数化する
std::for_eachに直接コンテナを渡せる形の変形版を作り、ラムダ式の引数でインデックスを渡せるようにしたもの。コンテナを&&で受けているので、右辺値も参照も受け取れる。
終了判定条件を書く必要がなく簡潔でミスが少なく書け、インデックス値や値をラムダ式の引数として個別に受け取れconstや参照も明記できる。ラムダ式内でreuturn falseとするとbreak,return trueとするとcontinueと同等。ヘルパー関数を挟んでいるのでラムダ式内には必ずしもreturnがなくてよい。ただ、forループが記述された関数の直接returnは不能。
template<typename FUNC, typename... ARGS>
bool return_bool(bool* p, FUNC func, ARGS... args)
{
return func(args...);
}
template<typename FUNC, typename... ARGS>
bool return_bool(void* p, FUNC func, ARGS... args)
{
func(args...);
return true;
}
//funcの戻り値がvoidならtrueを返す
template<typename FUNC, typename... ARGS>
bool return_bool(FUNC func, ARGS... args)
{
decltype(func(args...))* p = nullptr;
return return_bool(p, func, args...);
}
template<class C, class FUNC>
void for_with_index(C&& c, FUNC func)
{
size_t i=0;
for(auto& v : c){
if(!return_bool(func,v,i)) break;
i++;
}
}
//使い方
std::vector<int> vec{1, 2, 3};
for_with_index(vec, [](const auto& v, auto i) {
std::cout << i << ":"<< v << std::endl;
});
for_with_index(vec, [](const auto& v, auto i) {
if(v==2) return true;//trueを返すとcontinue相当, falseならbreak相当
std::cout << i << ":"<< v << std::endl;
return true;//一度bool値でreturnしていたら最後にreturn trueする必要がある。
});
##5. ラムダ式を使ったループ関数とをとジェネレータ(自作クラス版)を使う
要は、std::for_eachにコンテナを2つ渡せる変形版を作る。インデックスを扱うカスタムジェネレータを作る※。ジェネレータはメモリにデータを持たず、値を逐次生成しイテレータで返す擬似コンテナのようなもの。コンテナを&&で受けているので、右辺値も参照も受け取れるようにしている。終了判定条件を書く必要がなく簡潔でミスが少なく書け、インデックス値や値をラムダ式の引数として個別に受け取れconstや参照も明記できる。ただ、forループが記述された関数からの直接returnは不能。
※c++20では、std::ranges::views::iotaがある。動作や実装は未確認だが同等の機能があるはず。
https://en.cppreference.com/w/cpp/ranges/iota_view
//return_boolは、4.を参照
template<class C, class C1, class FUNC>
void for_(C&& c, C1&& c1,FUNC func)
{
auto it1 = c1.begin();
for(auto& v : c) {
if(it1==c1.end()) break;
if(!return_bool(func,v,*it)) break;
it1++;
}
}
class Counter
{
struct iterator
{
iterator(size_t _index) : index(_index){}
size_t index;
void operator++(int) noexcept { index++; };
size_t operator*() noexcept { return index; };
bool operator==(iterator it){
return it.index==index;
}
};
template<class C, class FUNC>
friend void for_(C& c, FUNC func);
template<class C, class C1, class FUNC>
friend void for_(C& c, C1& c1, FUNC func);
size_t size_;
public:
Counter(size_t size = std::numeric_limits<size_t>::max())
: size_(size)
{
}
iterator begin() noexcept { return iterator(0); }
iterator end() noexcept { return iterator(size_); }
};
//使い方
std::vector<int> vec{1, 2, 3};
for_(vec, Counter(), [](const auto& v, auto i) {
std::cout << i << ":"<< v << std::endl;
});
for_(vec, Counter(), [](const auto& v, auto i) {
if(v==2) return true;//trueを返すとcontinue相当, falseならbreak相当
std::cout << i << ":"<< v << std::endl;
return true;//一度bool値でreturnしていたら最後にreturn trueする必要がある
});
##6. ラムダ式を使ったループ関数とをとジェネレータ(corutine版)を使う
コンテナを2つ渡せるstd::for_eachの変形版を作るのは4.と同じ。c++20であればコルーチンが使えるので、ここでは、std::experimental::generatorを使った。
終了判定条件を書く必要がなく簡潔でミスが少なく書け、インデックス値や値をラムダ式の引数として個別に受け取れconstや参照も明記できる。インデックスが不要ならfor_の二番目の引数にコンテナを渡して処理がかけるため応用が利きやすい。experimentalではあるが外部ライブラリなしにジェネレータをシンプルに書ける。しかし、corutineにオーバヘッド※がある。
※そもそもcorutineのあるべき使い方ではないので、corutineの効用を否定するものでないです。corutineに関しては以下を参考に。
https://qiita.com/tan-y/items/ae54153ec3eb42f80638
#include <experimental/generator>
std::experimental::generator<size_t> CoroutineCounter()
{
for(size_t i = 0; i < std::numeric_limits<size_t>::max(); i++) {
co_yield i;
}
co_return;
}
//使い方
std::vector<int> vec{1, 2, 3};
for_(vec, CoroutineCounter(), [](const auto& v, auto i) {
std::cout << i << ":"<< v << std::endl;
});
##7. ranged based forとジェネレータ(自作クラス版)を使う
ジェネレータのコードは、
https://cpplover.blogspot.com/2017/10/range-based-for.html
のコードを引用させていただいた。
イテレータがstd::pairを返し構造化束縛でインデックスと値を分離して受け取れるようにしたもの。
終了判定条件を書く必要がなく簡潔でミスが少なく書け、インデックスと値を個別に扱うことができるが、インデクスや値に対し個別にconstや参照を明記できない。また、with_indexの引数にmap※は使えない。
※mapのイテレータはpairを返すが、構造化束縛ではpairの入れ子は扱えないため。
template<typename Iterator>
class with_index_iterator : public Iterator
{
std::size_t i = 0;
public:
with_index_iterator(Iterator iter): Iterator(iter) {}
auto& operator++()
{
++i;
this->Iterator::operator++();
return *this;
}
auto operator*() const noexcept
{
return std::pair<std::size_t, typename std::iterator_traits<Iterator>::reference>{
i, *static_cast<Iterator const&>(*this)};
}
};
template<typename Range>
class with_index
{
Range& range;
public:
with_index(Range& range) : range(range){}
auto begin() const { return with_index_iterator{std::begin(range)}; }
auto end() const { return with_index_iterator{std::end(range)}; }
};
//使い方
std::vector<int> vec{1, 2, 3};
for ( auto[ i, v ] : with_index(vec) )
{
std::cout << i << ":"<< v << std::endl;
}
##8. ranged based forとジェネレータ(boostのクラス版)を使う
コンテナとインデックス値リストを結合したオブジェクトを返すジェネレータをつくる。boostに用意されている。終了判定条件を書く必要がなく簡潔でミスが少なく書けるが、インデックスと値を個々の変数として扱えない。
std::vector<int> vec{1, 2, 3};
for(auto&& it : vec | boost::adaptors::indexed()){
std::cout << it.index() << ":" << it.value() << std::endl;
}
##9. ranged based forとジェネレータ(corutine版)を使う
corutineを使ったジェネレータとranged based forと組み合わせる。終了判定条件を書く必要がなく簡潔でミスが少なく書ける。とくにexperimentalではあるが外部ライブラリなしにジェネレータをシンプルに書ける。しかし、インデックスと値を個々の変数として扱えるが、値に対しconstや参照などを明記できない。また、corutineにオーバヘッドがあり、構造化束縛をつかっているためmapは使えない。
template<class C>
std::experimental::generator<std::pair<size_t,typename C::value_type>> CoroutineWithIndex(C& c)
{
size_t i=0;
for(auto& v : c) {
co_yield std::pair{i,v};
i++;
}
co_return;
}
//使い方
std::vector<int> vec{1, 2, 3};
for(auto [i, v] : CoroutineWithIndex(vec)) {
std::cout << i << ":" << v << std::endl;
}
処理時間
オーバーヘッド確認のために処理時間を計測してみた。
vector<size_t> vec(8192*8192);
std::fill(vec.begin(), vec.end(), 0);
//ここから
for(size_t i = 0; i < vec.size(); i++) {
vec[i] = i;
}
//ここまでの時間を計測
各種法で同様のコードを書いて計測。5回平均の結果。corutine版の飛び抜けて遅い。他はほぼ同じと考えてよさそう。
No | 手法 | 平均時間(ms) |
---|---|---|
1 | インデックス値そのものを制御変数とする | 40.35 |
2 | イテレータを使って制御する | 39.23 |
3 | range based for | 41.01 |
4 | ラムダ式をつかってループを関数化する | 40.86 |
5 | ラムダ式を使ったループ関数とをとジェネレータ(自作クラス版)を使う | 40.48 |
6 | ラムダ式を使ったループ関数とをとジェネレータ(corutine版)を使う | 344.64 |
7 | ranged based forとジェネレータ(自作クラス版)を使う | 39.77 |
9 | ranged based forとジェネレータ(corutine版)を使う | 392.28 |
すみません。8番は省略で。 |
サマリ
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
---|---|---|---|---|---|---|---|---|---|
normal for | normal for | range based for | ラムダ式 | ラムダ式 | ラムダ式 | range based for | range based for | range based for | |
イテレータ | ジェネレータ (自作クラス版) |
ジェネレータ (corutine) |
ジェネレータ (自作クラス版) |
ジェネレータ (boostのクラス版) |
ジェネレータ (corutine) |
||||
記述がシンプル | △ | × | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
終了判定条件の明示的な記述が不要 | × | × | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
見慣れているので読む人は理解しやすい | ○ | △ | △ | △ | △ | △ | △ | △ | △ |
forの{}の中でインデックスのインクリメントが不要 | ○ | ○ | × | ○ | ○ | ○ | ○ | ○ | ○ |
オーバヘッドが少ない | ○ | ○ | ○ | ○ | ○ | × | ○ | ○ | × |
break,関数returnが可能 | ○ | ○ | ○ | △ | △ | × | ○ | ○ | ○ |
インデックスと値を個別の変数で扱え、constや参照の明記が可能。 | - | ○ | ○ | ○ | ○ | ○ | △ | × | △ |
さいごに
どれも一長一短で、ものによっては相当マニアックだが、もしsize()メンバ関数が使えるなら一周して1.が書き慣れたり読み慣れたりしいてよいのかも(笑)。自分も結局それを使うことが多い。size()が使えない、あるいは、より簡潔な記述を目指すなら、個人的には応用が利きやすそうな5.がよいかな。ループ処理のマルチスレッド化など改良の余地もあるし。