カスタムタイプを「範囲ベースのforループ」で機能させるにはどうすればよいですか?


252

最近の多くの人のように、私はC ++ 11がもたらすさまざまな機能を試しています。私のお気に入りの1つは、「範囲ベースのforループ」です。

という事は承知しています:

for(Type& v : a) { ... }

以下と同等です。

for(auto iv = begin(a); iv != end(a); ++iv)
{
  Type& v = *iv;
  ...
}

そして、それはbegin()単にa.begin()標準的なコンテナに戻ります。

しかし、カスタムタイプを「範囲ベースのforループ」に対応せるにはどうすればよいでしょうか。

私はちょうど特化すべきであるbegin()end()

カスタムタイプが名前空間xmlに属している場合、定義する必要がありますxml::begin()std::begin()

要するに、それを行うためのガイドラインは何ですか?


これはbegin/end、静的または無料のメンバーまたは友達を定義することによって可能begin/endです。:ちょうどあなたがフリー機能置かれた名前空間に注意してくださいstackoverflow.com/questions/28242073/...
alfC

誰もがコンテナではありませんfloat値の範囲の例で答えを投稿してくださいでした:for( auto x : range<float>(0,TWO_PI, 0.1F) ) { ... }。`´operator!=()` `を定義するのが難しいという事実にどのように対処するか、私は興味があります。そして*__begin、この場合の逆参照()はどうですか?それがどのよう行われるか誰かが見せてくれれば、それは大きな貢献になると思います!
BitTickler 2018年

回答:


183

質問(およびほとんどの回答)がこの欠陥レポートの解決策に掲載されて以来、標準は変更されています。

for(:)型でループを機能させるX方法は、次の2つの方法のいずれかになります。

  • メンバーX::begin()を作成しX::end()、イテレーターのように機能するものを返す

  • タイプと同じ名前空間で、フリー関数begin(X&)を作成しend(X&)、イテレータのように機能するものを返しますX。¹

constバリエーションについても同様です。これは、欠陥レポートの変更を実装するコンパイラと実装しないコンパイラの両方で機能します。

返されるオブジェクトは、実際にイテレータである必要はありません。for(:)ループは、C ++標準の大部分とは異なり、されに何か相当に拡大して指定します

for( range_declaration : range_expression )

になる:

{
  auto && __range = range_expression ;
  for (auto __begin = begin_expr,
            __end = end_expr;
            __begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

ここで始まる変数__は説明のためだけでありbegin_expr、/. ² end_exprを呼び出す魔法ですbeginend

begin / endの戻り値の要件は単純です。pre-をオーバーロードし++、初期化式が有効で!=あること、ブールコンテキストで使用できるバイナリ、*割り当て初期化できるものを返す単項range_declaration、およびパブリックを公開する必要があります。デストラクタ。

イテレータと互換性のない方法でこれを行うことは、おそらく悪い考えです。C++の将来の反復は、コードを壊すことに関して比較的無頓着になる可能性があるためです。

余談ですが、標準の将来の改訂では、とend_exprは異なるタイプを返すことが許可される可能性がかなりありbegin_exprます。これは、手書きのCループと同じくらい効率的に最適化するのが簡単な「遅延終了」評価(ヌル終了の検出など)を許可するという点で役立ちます。


loop for(:)ループは一時auto&&変数を変数に格納し、それを左辺値として渡すことに注意してください。一時(または他の右辺値)を反復処理しているかどうかは検出できません。このようなオーバーロードはfor(:)ループによって呼び出されません。n4527の[stmt.ranged] 1.2-1.3を参照してください。

²呼び出しのどちらかbegin/のend方法を、またはADL専用のルックアップフリー機能のbegin/ endまたは Cスタイルの配列をサポートするための魔法。タイプが同じまたは依存しているオブジェクトを返さstd::beginない限り、は呼び出されないことに注意してください。range_expressionnamespace std


range-for式が更新されました

{
  auto && __range = range_expression ;
  auto __begin = begin_expr;
  auto __end = end_expr;
  for (;__begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

__beginおよびのタイプで__end分離されています。

これにより、終了反復子がbeginと同じ型にならないようにすることができます。終了反復子タイプは!=、開始反復子タイプでのみサポートされる「センチネル」にすることができます。

これは便利である理由の実用的な例は、反復子は、「あなたをチェック読むことができるあなたの最後ということでchar*、それが指すかどうかを確認するために'0'とき」==char*。これにより、C ++のrange-for式は、nullで終了するchar*バッファーを反復処理するときに最適なコードを生成できます。

struct null_sentinal_t {
  template<class Rhs,
    std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
  >
  friend bool operator==(Rhs const& ptr, null_sentinal_t) {
    return !*ptr;
  }
  template<class Rhs,
    std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
  >
  friend bool operator!=(Rhs const& ptr, null_sentinal_t) {
    return !(ptr==null_sentinal_t{});
  }
  template<class Lhs,
    std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
  >
  friend bool operator==(null_sentinal_t, Lhs const& ptr) {
    return !*ptr;
  }
  template<class Lhs,
    std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
  >
  friend bool operator!=(null_sentinal_t, Lhs const& ptr) {
    return !(null_sentinal_t{}==ptr);
  }
  friend bool operator==(null_sentinal_t, null_sentinal_t) {
    return true;
  }
  friend bool operator!=(null_sentinal_t, null_sentinal_t) {
    return false;
  }
};

C ++ 17を完全にサポートしていないコンパイラでのライブの例forループは手動で拡張されました。


for range-based forが別のルックアップメカニズムを使用する場合、そのrange-based forを調整して、通常のコードで使用できるものとは異なるペアのbeginend関数を取得することができます。おそらく、それらは異なる動作をするように非常に特化できます(つまり、最大化の最適化を可能にするために、end引数を無視することでより高速になります)。
Aaron McDaid

@AaronMcDaidはあまり実用的ではありません。begin / endを呼び出すいくつかの手段は、範囲ベースのbegin / endで終わるものもあれば、そうでないものもあるので、簡単に意外な結果になるでしょう。無害な変更(クライアント側から)は、動作の変更を取得します。
Yakk-Adam Nevraumont 2015

1
あなたは必要ありませんbegin(X&&)。一時変数auto&&は、範囲ベースのfor によって空中で中断され、begin常に左辺値(__range)で呼び出されます。
TC

2
この回答は、コピーして実装できるテンプレートの例から本当にメリットがあります。
トマーシュZato -復活モニカ

イテレータtype(*、++、!=)のプロパティに重点を置きます。イテレータタイプの仕様を太字にするために、この返信を言い換えるようにお願いします。
Red.Wave

62

一部の人々は、STLインクルードを含まない単純な実際の例に満足している可能性があるため、私の回答を書いています。

なんらかの理由で、独自のプレーンのみのデータ配列の実装があり、範囲ベースのforループを使用したいと考えていました。これが私の解決策です:

 template <typename DataType>
 class PodArray {
 public:
   class iterator {
   public:
     iterator(DataType * ptr): ptr(ptr){}
     iterator operator++() { ++ptr; return *this; }
     bool operator!=(const iterator & other) const { return ptr != other.ptr; }
     const DataType& operator*() const { return *ptr; }
   private:
     DataType* ptr;
   };
 private:
   unsigned len;
   DataType *val;
 public:
   iterator begin() const { return iterator(val); }
   iterator end() const { return iterator(val + len); }

   // rest of the container definition not related to the question ...
 };

次に、使用例:

PodArray<char> array;
// fill up array in some way
for(auto& c : array)
  printf("char: %c\n", c);

2
この例にはbegin()メソッドとend()メソッドがあり、カスタムコンテナタイプに合わせて簡単に調整できる基本的な(わかりやすい)サンプルイテレータクラスもあります。std :: array <>と可能な代替実装の比較は別の問題であり、私の意見では、範囲ベースのforループとは何の関係もありません。
csjpeter

これは非常に簡潔で実用的な答えです。それはまさに私が探していたものでした!ありがとう!
ザックテイラー

1
const return修飾子を削除してconst DataType& operator*()、ユーザーがを使用するconst auto&auto&どうかを選択する方が適切でしょうか?とにかくありがとう、素晴らしい答え;)
リック

53

標準の関連部分は6.5.4 / 1です。

_RangeTがクラス型の場合、クラスメンバーアクセス検索(3.4.5)の場合と同様に、クラス_RangeTのスコープで修飾されていないIDの開始と終了が検索され、いずれか(または両方)が少なくとも1つの宣言を見つけた場合、開始- exprは、エンドexprがある__range.begin()__range.end()、それぞれ、

-そうでない場合は、開始-exprは、エンドexprがあるbegin(__range)end(__range)、それぞれ、引数依存ルックアップ(3.4.2)を用いてルックアップされる開始と終了。この名前の検索のために、名前空間stdは関連付けられた名前空間です。

したがって、次のいずれかを実行できます。

  • 定義beginおよびendメンバー関数
  • ADLによって検出される関数を定義beginしてend解放します(簡易バージョン:クラスと同じ名前空間に配置します)
  • 専門にstd::beginしてstd::end

std::beginbegin()とにかくメンバー関数を呼び出すので、上記の1つだけを実装した場合、どちらを選択しても結果は同じになります。これは、範囲ベースのforループの結果と同じです。また、独自の魔法の名前解決ルールを持たない単なるコードの場合も同じ結果になるためusing std::begin;、への修飾されていない呼び出しが続きbegin(a)ます。

ただし、メンバー関数 ADL関数を実装する場合、範囲ベースのforループはメンバー関数を呼び出す必要がありますが、一般の人はADL関数を呼び出します。その場合、彼らが同じことを確実に行うようにしてください!

作成しているものがコンテナーインターフェイスを実装している場合は、既にコンテナー関数begin()end()メンバー関数があり、これで十分です。それがコンテナーではない範囲である場合(それが不変であるか、事前にサイズがわからない場合は、これは良い考えです)、自由に選択できます。

レイアウトするオプションのうち、オーバーロードしてはいけないことに注意してくださいstd::begin()。ユーザー定義型の標準テンプレートを特殊化することは許可されていますが、それ以外に、名前空間stdに定義を追加することは未定義の動作です。しかし、とにかく、部分的な関数の特殊化がないために、クラステンプレートではなく単一のクラスに対してのみそれを実行できることを意味する場合に限り、標準関数を特殊化することは適切ではありません。


イテレータがはるかに満たす特定の要件はありませんか?つまり、ForwardIteratorまたはそれらに沿ったものになります。
Pubby

2
@Pubby:6.5.4を見ると、InputIteratorで十分だと思います。しかし、実際に私が返される型は考えていない持っているために範囲ベースのためのすべてのイテレータであることを。ステートメントはそれと同等のものによって標準で定義されているので、標準のコードで使用されている式(operator !=、prefix ++、unary)のみを実装するだけで十分*です。それはおそらくだ愚か実装するbegin()と、end()メンバ関数や非会員のADL機能イテレータ以外の戻り何でもすることを、私はそれが法的だと思います。std::begin非イテレーターを返すことに特化しているのはUBだと思います。
Steve Jessop

std :: beginをオーバーロードしてはいけませんか?標準ライブラリがいくつかの場合それ自体でそうするので私は尋ねます。
ThreeBit

@ThreeBit:はい、きっと。標準ライブラリ実装の規則は、プログラムの規則とは異なります。
スティーブジェソップ2013

3
これは、open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1442に合わせて更新する必要があります。
TC

34

begin()とend()だけを特化する必要がありますか?

私の知る限り、それで十分です。また、ポインタのインクリメントが最初から最後まで確実に行われるようにする必要があります。

次の例(beginとendのconstバージョンがない)はコンパイルして正常に動作します。

#include <iostream>
#include <algorithm>

int i=0;

struct A
{
    A()
    {
        std::generate(&v[0], &v[10], [&i](){  return ++i;} );
    }
    int * begin()
    {
        return &v[0];
    }
    int * end()
    {
        return &v[10];
    }

    int v[10];
};

int main()
{
    A a;
    for( auto it : a )
    {
        std::cout << it << std::endl;
    }
}

これは、関数としてbegin / endを使用する別の例です。彼らはする必要があるためADLの、クラスと同じ名前空間にあります:

#include <iostream>
#include <algorithm>


namespace foo{
int i=0;

struct A
{
    A()
    {
        std::generate(&v[0], &v[10], [&i](){  return ++i;} );
    }

    int v[10];
};

int *begin( A &v )
{
    return &v.v[0];
}
int *end( A &v )
{
    return &v.v[10];
}
} // namespace foo

int main()
{
    foo::A a;
    for( auto it : a )
    {
        std::cout << it << std::endl;
    }
}

1
@ereOnクラスが定義されているのと同じ名前空間内。第二の例を参照してください
BЈовић

2
おめでとうございます:) 2番目の例のArgument Dependent Lookup(ADL)またはKoenig Lookupという用語に言及する価値があるかもしれません(フリー関数が操作対象のクラスと同じ名前空間にある必要がある理由を説明するため)。
Matthieu M.11年

1
@ereOn:実際にはそうではありません。ADLは、スコープをルックアップに拡張して、引数が属する名前空間を自動的に含めることを目的としています。オーバーロードの解決に関する優れたACCU記事があり、残念ながら名前の検索部分はスキップされます。名前の検索には、候補関数の収集が含まれます。まず、現在のスコープと引数のスコープを調べます。一致する名前が見つからない場合は、現在のスコープの親スコープに移動し、グローバルスコープに到達するまで再度検索します。
Matthieu M.11年

1
@BЈовић申し訳ありませんが、end()関数のどの理由で危険なポインタを返しますか?私はそれが機能することを知っていますが、私はこれの論理を理解したいと思います。配列の最後はv [9]ですが、なぜv [10]を返すのですか?
gedamial 2016年

1
@gedamial同意する。そうだと思うreturn v + 10&v[10]配列の直後のメモリ位置を逆参照します。
ミリー・スミス

16

std::vectorまたはstd::mapメンバーでクラスの反復を直接バックしたい場合は、そのためのコードを次に示します。

#include <iostream>
using std::cout;
using std::endl;
#include <string>
using std::string;
#include <vector>
using std::vector;
#include <map>
using std::map;


/////////////////////////////////////////////////////
/// classes
/////////////////////////////////////////////////////

class VectorValues {
private:
    vector<int> v = vector<int>(10);

public:
    vector<int>::iterator begin(){
        return v.begin();
    }
    vector<int>::iterator end(){
        return v.end();
    }
    vector<int>::const_iterator begin() const {
        return v.begin();
    }
    vector<int>::const_iterator end() const {
        return v.end();
    }
};

class MapValues {
private:
    map<string,int> v;

public:
    map<string,int>::iterator begin(){
        return v.begin();
    }
    map<string,int>::iterator end(){
        return v.end();
    }
    map<string,int>::const_iterator begin() const {
        return v.begin();
    }
    map<string,int>::const_iterator end() const {
        return v.end();
    }

    const int& operator[](string key) const {
        return v.at(key);
    }
    int& operator[](string key) {
        return v[key];
    } 
};


/////////////////////////////////////////////////////
/// main
/////////////////////////////////////////////////////

int main() {
    // VectorValues
    VectorValues items;
    int i = 0;
    for(int& item : items) {
        item = i;
        i++;
    }
    for(int& item : items)
        cout << item << " ";
    cout << endl << endl;

    // MapValues
    MapValues m;
    m["a"] = 1;
    m["b"] = 2;
    m["c"] = 3;
    for(auto pair: m)
        cout << pair.first << " " << pair.second << endl;
}

2
ことを言及することの価値const_iteratorにもアクセスすることができるauto介して(C ++ 11)互換方法cbegincend等が、
underscore_d

2

ここでは、「範囲ベースのforループ」で機能するカスタムタイプを作成する最も簡単な例を紹介します

#include<iostream>
using namespace std;

template<typename T, int sizeOfArray>
class MyCustomType
{
private:
    T *data;
    int indx;
public:
    MyCustomType(){
        data = new T[sizeOfArray];
        indx = -1;
    }
    ~MyCustomType(){
        delete []data;
    }
    void addData(T newVal){
        data[++indx] = newVal;
    }

    //write definition for begin() and end()
    //these two method will be used for "ranged based loop idiom"
    T* begin(){
        return &data[0];
    }
    T* end(){
        return  &data[sizeOfArray];
    }
};
int main()
{
    MyCustomType<double, 2> numberList;
    numberList.addData(20.25);
    numberList.addData(50.12);
    for(auto val: numberList){
        cout<<val<<endl;
    }
    return 0;
}

私のような初心者開発者に役立つことを願っています:p :)
ありがとうございます。


endメソッドで無効なメモリの逆参照を回避するために、1つの追加要素を割り当てませんか?
AndersK

@Andersほとんどすべての終了反復子は、それらの包含構造の終了後にポイントするためです。end()それが唯一の「アドレスの」は、このメモリ位置を取るため、機能自体は明らかに、不適切なメモリ位置を逆参照しません。追加の要素を追加することは、より多くのメモリが必要になることを意味しyour_iterator::end()、その値を逆参照するような方法で使用しても、同じ方法で構築されているため、他のイテレータでは機能しません。
Qqwy

@Qqwy彼のendメソッドは逆参照- return &data[sizeofarray]私見それはアドレスデータ+ sizeofarrayを返すだけですが、私が知っていること
AndersK

@Anders正解です。私を鋭くしてくれてありがとう:-) はい、data + sizeofarrayこれを書く正しい方法でしょう。
Qqwy

1

クリス・レッドフォードの答えは、Qtコンテナーでも機能します(もちろん)。これが適応です(私constBegin()はそれぞれconstEnd()const_iteratorメソッドからを返すことに注意してください):

class MyCustomClass{
    QList<MyCustomDatatype> data_;
public:    
    // ctors,dtor, methods here...

    QList<MyCustomDatatype>::iterator begin() { return data_.begin(); }
    QList<MyCustomDatatype>::iterator end() { return data_.end(); }
    QList<MyCustomDatatype>::const_iterator begin() const{ return data_.constBegin(); }
    QList<MyCustomDatatype>::const_iterator end() const{ return data_.constEnd(); }
};

0

@Steve Jessopの回答の一部を詳しく説明したいのですが、最初はわかりませんでした。それが役に立てば幸い。

std::beginbegin()とにかくメンバー関数を呼び出すので、上記の1つだけを実装した場合、どちらを選択しても結果は同じになります。これは、範囲ベースのforループの結果と同じです。また、独自の魔法の名前解決ルールを持たない単なるコードの場合も同じ結果になるためusing std::begin;、への修飾されていない呼び出しが 続きbegin(a)ます。

ただし、メンバー関数 ADL関数を実装する場合、範囲ベースのforループはメンバー関数を呼び出す必要がありますが、単なる一般の人はADL関数を呼び出します。その場合、彼らが同じことを確実に行うようにしてください!


https://en.cppreference.com/w/cpp/language/range-for

  • もし...
  • 場合range_expressionクラス型の式であるCという名前のメンバの両方有するbeginと名付け部材end次いで、(種類に関係なく、またはそのような部材のアクセシビリティのを)begin_exprである __range.begin()とend_exprです__range.end()
  • それ以外の場合は、begin_expris begin(__range)およびend_exprisですend(__range)。これらは、引数に依存するルックアップによって検出されます(非ADLルックアップは実行されません)。

範囲ベースのforループの場合、メンバー関数が最初に選択されます。

しかし

using std::begin;
begin(instance);

ADL関数が最初に選択されます。


例:

#include <iostream>
#include <string>
using std::cout;
using std::endl;

namespace Foo{
    struct A{
        //member function version
        int* begin(){
            cout << "111";
            int* p = new int(3);  //leak I know, for simplicity
            return p;
        }
        int *end(){
            cout << "111";
            int* p = new int(4);
            return p;
        }
    };

    //ADL version

    int* begin(A a){
        cout << "222";
        int* p = new int(5);
        return p;
    }

    int* end(A a){
        cout << "222";
        int* p = new int(6);
        return p;
    }

}

int main(int argc, char *args[]){
//    Uncomment only one of two code sections below for each trial

//    Foo::A a;
//    using std::begin;
//    begin(a);  //ADL version are selected. If comment out ADL version, then member functions are called.


//      Foo::A a;
//      for(auto s: a){  //member functions are selected. If comment out member functions, then ADL are called.
//      }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.