Bagaimana cara membuat jenis khusus saya untuk bekerja dengan "range-based for loop"?

252

Seperti banyak orang hari ini saya telah mencoba berbagai fitur yang dibawa oleh C ++ 11. Salah satu favorit saya adalah "range-based for loop".

Aku mengerti itu:

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

Setara dengan:

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

Dan itu begin()hanya mengembalikan a.begin()kontainer standar.

Tetapi bagaimana jika saya ingin membuat tipe kustom saya "berbasis-rentang untuk loop" -adar ?

Haruskah saya hanya mengkhususkan begin()dan end()?

Jika tipe khusus saya milik namespace xml, apakah saya harus mendefinisikan xml::begin()atau std::begin()?

Singkatnya, apa pedoman untuk melakukan itu?

sebelum
sumber
Mungkin saja dengan mendefinisikan anggota begin/endatau teman, statis atau gratis begin/end. Hanya berhati-hatilah di namespace mana Anda meletakkan fungsi gratis: stackoverflow.com/questions/28242073/…
alfC
Mungkin orang tolong posting jawaban dengan contoh berbagai nilai mengambang yang TIDAK wadah: for( auto x : range<float>(0,TWO_PI, 0.1F) ) { ... }. Saya ingin tahu bagaimana Anda mengatasi kenyataan bahwa `´operator! = ()` `Sulit untuk didefinisikan. Dan bagaimana dengan dereferencing ( *__begin) dalam kasus ini? Saya pikir itu akan menjadi kontribusi yang bagus jika seseorang menunjukkan kepada kita bagaimana hal itu dilakukan!
BitTickler

Jawaban:

183

Standar telah diubah sejak pertanyaan (dan sebagian besar jawaban) diposting dalam resolusi laporan cacat ini .

Cara membuat for(:)loop bekerja pada tipe Anda Xsekarang adalah salah satu dari dua cara:

  • Buat anggota X::begin()dan X::end()kembalikan sesuatu yang bertindak seperti iterator

  • Buat fungsi gratis begin(X&)dan end(X&)yang mengembalikan sesuatu yang bertindak seperti iterator, di namespace yang sama dengan tipe Anda X

Dan serupa untuk constvariasi. Ini akan bekerja pada kompiler yang mengimplementasikan perubahan laporan cacat, dan kompiler yang tidak.

Objek yang dikembalikan tidak harus benar-benar menjadi iterator. The for(:)Loop, tidak seperti sebagian besar C ++ standar, yang ditentukan untuk memperluas untuk sesuatu yang setara dengan :

for( range_declaration : range_expression )

menjadi:

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

di mana variabel yang dimulai dengan __hanya untuk eksposisi, dan begin_exprdan end_expradalah sihir yang memanggil begin/ end

Persyaratan pada nilai pengembalian awal / akhir sederhana: Anda harus kelebihan pre- ++, memastikan ekspresi inisialisasi valid, biner !=yang dapat digunakan dalam konteks boolean, unary *yang mengembalikan sesuatu yang dapat Anda tetapkan-inisialisasi range_declarationdengan, dan mengekspos publik destruktor.

Melakukannya dengan cara yang tidak kompatibel dengan iterator mungkin merupakan ide yang buruk, karena iterasi C ++ di masa depan mungkin relatif lebih berani tentang memecahkan kode Anda jika Anda melakukannya.

Sebagai tambahan, ada kemungkinan wajar bahwa revisi standar di masa depan akan memungkinkan end_expruntuk mengembalikan jenis yang berbeda dari begin_expr. Ini berguna karena memungkinkan evaluasi "lazy-end" (seperti mendeteksi null-termination) yang mudah dioptimalkan agar seefisien loop C tulisan tangan, dan keuntungan serupa lainnya.


¹ Perhatikan bahwa for(:)loop menyimpan sementara apa pun dalam auto&&variabel, dan memberikannya kepada Anda sebagai nilai. Anda tidak dapat mendeteksi jika Anda mengulanginya sementara (atau nilai lainnya); kelebihan seperti itu tidak akan dipanggil dengan for(:)loop. Lihat [stmt.ranged] 1.2-1.3 dari n4527.

² Baik panggil metode begin/ end, atau pencarian fungsi bebas ADL saja begin/ end, atau sulap untuk dukungan larik gaya-C. Catatan yang std::begintidak dipanggil kecuali range_expressionmengembalikan objek bertipe namespace stdatau bergantung pada yang sama.


Di rentang-untuk ekspresi telah diperbarui

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

dengan jenis __begindan __endtelah dipisahkan.

Ini memungkinkan iterator akhir untuk tidak menjadi tipe yang sama seperti mulai. Jenis iterator akhir Anda bisa berupa "sentinel" yang hanya mendukung !=dengan tipe iterator begin.

Contoh praktis mengapa ini berguna adalah bahwa iterator akhir Anda dapat membaca "periksa Anda char*untuk melihat apakah itu menunjuk '0'" ketika ==dengan a char*. Ini memungkinkan rentang C ++ untuk ekspresi menghasilkan kode yang optimal ketika iterasi lebih dari char*buffer null-dihentikan .

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;
  }
};

contoh langsung dalam kompiler tanpa dukungan penuh C ++ 17; forloop diperluas secara manual.

Yakk - Adam Nevraumont
sumber
Jika rentang berbasis untuk menggunakan mekanisme pencarian yang berbeda, maka mungkin itu mungkin untuk mengatur bahwa rentang berbasis untuk mendapatkan pasangan begindan endfungsi yang berbeda dari yang tersedia dalam kode normal. Mungkin mereka kemudian bisa menjadi sangat khusus untuk berperilaku berbeda (yaitu lebih cepat dengan mengabaikan argumen akhir untuk mendapatkan optimalisasi maksimal yang mungkin). Tapi saya tidak cukup baik dengan ruang nama untuk memastikan bagaimana melakukan ini.
Aaron McDaid
@AaronMcDaid tidak terlalu praktis. Anda akan dengan mudah berakhir dengan hasil yang mengejutkan, karena beberapa cara memanggil begin / end akan berakhir dengan berbasis rentang untuk memulai / akhir, dan yang lainnya tidak. Perubahan yang tidak berbahaya (dari sisi klien) akan mendapatkan perubahan perilaku.
Yakk - Adam Nevraumont
1
Anda tidak perlu begin(X&&). Sementara ditangguhkan di udara oleh auto&&dalam rentang berbasis, dan beginselalu disebut dengan nilai ( __range).
TC
2
Jawaban ini akan sangat bermanfaat dari contoh templat yang dapat disalin dan diterapkan oleh seseorang.
Tomáš Zato - Reinstate Monica
Saya lebih suka menekankan sifat-sifat tipe iterator (*, ++,! =). Saya harus meminta Anda untuk mengulangi balasan ini untuk membuat spesifikasi jenis iterator lebih tebal.
Merah. Gelombang
62

Saya menulis jawaban saya karena beberapa orang mungkin lebih senang dengan contoh kehidupan nyata yang sederhana tanpa menyertakan STL.

Saya punya implementasi array data hanya polos saya untuk beberapa alasan, dan saya ingin menggunakan rentang berdasarkan untuk loop. Ini solusinya:

 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 ...
 };

Maka contoh penggunaannya:

PodArray<char> array;
// fill up array in some way
for(auto& c : array)
  printf("char: %c\n", c);
csjpeter
sumber
2
Contoh memiliki metode begin () dan end (), dan juga memiliki kelas iterator contoh dasar (mudah dimengerti) yang dapat dengan mudah disesuaikan untuk semua jenis wadah kustom. Membandingkan std :: array <> dan setiap implementasi alternatif yang mungkin adalah pertanyaan yang berbeda, dan menurut saya tidak ada hubungannya dengan range-based untuk loop.
csjpeter
Ini adalah jawaban yang sangat ringkas dan praktis! Persis seperti yang saya cari! Terima kasih!
Zac Taylor
1
Apakah akan lebih tepat untuk menghapus const kualifikasi pengembalian const DataType& operator*(), dan membiarkan pengguna memilih untuk menggunakan const auto&atau auto&? Terima kasih, jawaban yang bagus;)
Rick
53

Bagian yang relevan dari standar adalah 6.5.4 / 1:

jika _RangeT adalah tipe kelas, id yang tidak memenuhi syarat dimulai dan berakhir dicari dalam lingkup kelas _RangeT seolah-olah berdasarkan pencarian akses anggota kelas (3.4.5), dan jika salah satu (atau keduanya) menemukan setidaknya satu deklarasi, mulai - expr dan end-expr adalah __range.begin()dan __range.end(), masing-masing;

- jika tidak, begin-expr dan end-expr adalah begin(__range)dan end(__range), masing-masing, di mana begin dan end dicari dengan lookup yang bergantung pada argumen (3.4.2) Untuk keperluan pencarian nama ini, namespace std adalah namespace terkait.

Jadi, Anda dapat melakukan salah satu dari yang berikut:

  • tentukan begindan endfungsi anggota
  • mendefinisikan begindan endmembebaskan fungsi-fungsi yang akan ditemukan oleh ADL (versi yang disederhanakan: letakkan di namespace yang sama dengan kelas)
  • spesialisasi std::begindanstd::end

std::begintetap memanggil begin()fungsi anggota, jadi jika Anda hanya menerapkan salah satu di atas, maka hasilnya akan sama tidak peduli yang mana yang Anda pilih. Itu adalah hasil yang sama untuk rentang berbasis untuk loop, dan juga hasil yang sama untuk kode fana belaka yang tidak memiliki aturan resolusi nama magis sendiri sehingga hanya using std::begin;diikuti oleh panggilan yang tidak memenuhi syarat untukbegin(a) .

Jika Anda menerapkan fungsi anggota dan fungsi ADL, maka rentang berbasis untuk loop harus memanggil fungsi anggota, sedangkan manusia biasa akan memanggil fungsi ADL. Pastikan mereka melakukan hal yang sama dalam kasus itu!

Jika hal yang Anda tulis mengimplementasikan antarmuka wadah, maka itu akan memiliki begin()dan end()fungsi anggota sudah, yang seharusnya sudah cukup. Jika rentang yang bukan wadah (yang akan menjadi ide bagus jika tidak berubah atau jika Anda tidak tahu ukurannya di depan), Anda bebas untuk memilih.

Dari opsi yang Anda layangkan, perhatikan bahwa Anda tidak boleh kelebihan beban std::begin(). Anda diizinkan untuk mengkhususkan templat standar untuk jenis yang ditentukan pengguna, tetapi selain itu, menambahkan definisi ke namespace std adalah perilaku yang tidak ditentukan. Tapi bagaimanapun, mengkhususkan fungsi standar adalah pilihan yang buruk jika hanya karena kurangnya spesialisasi fungsi parsial berarti Anda hanya dapat melakukannya untuk satu kelas, bukan untuk templat kelas.

Steve Jessop
sumber
Apakah tidak ada persyaratan tertentu yang harus dipenuhi oleh iterator? yaitu menjadi ForwardIterator atau sesuatu seperti itu.
Pubby
2
@Ubby: Melihat 6.5.4, saya pikir InputIterator sudah cukup. Tapi sebenarnya saya tidak berpikir tipe yang dikembalikan harus menjadi iterator sama sekali untuk rentang berbasis. Pernyataan ini didefinisikan dalam standar dengan apa yang setara dengan itu, sehingga cukup hanya menerapkan ekspresi yang digunakan dalam kode dalam standar: operator !=, awalan ++dan unary *. Mungkin tidak bijaksana untuk menerapkan begin()dan end()fungsi anggota atau fungsi ADL non-anggota yang mengembalikan apa pun selain iterator, tapi saya pikir itu legal. Spesialisasi std::beginuntuk mengembalikan non-iterator adalah UB, saya pikir.
Steve Jessop
Anda yakin tidak boleh kelebihan std :: begin? Saya bertanya karena perpustakaan standar melakukannya dalam beberapa kasus itu sendiri.
ThreeBit
@ThreeBit: ya, saya yakin. Aturan untuk implementasi perpustakaan standar berbeda dari aturan untuk program.
Steve Jessop
3
Ini perlu diperbarui untuk open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1442 .
TC
34

Haruskah saya mengkhususkan spesialisasi begin () dan end ()?

Sejauh yang saya tahu, itu sudah cukup. Anda juga harus memastikan bahwa penunjuk yang bertambah akan didapat dari awal hingga akhir.

Contoh berikutnya (tidak ada versi const awal dan akhir) mengkompilasi dan berfungsi dengan baik.

#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;
    }
}

Berikut adalah contoh lain dengan fungsi begin / end sebagai fungsi. Mereka harus berada di namespace yang sama dengan kelas, karena 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;
    }
}
BЈовић
sumber
1
@ eOn Di namespace yang sama di mana kelas didefinisikan. Lihat contoh 2
BЈовић
2
Selamat juga :) Mungkin perlu menyebutkan istilah Argument Dependent Lookup (ADL) atau Koenig Lookup untuk contoh kedua (untuk menjelaskan mengapa fungsi bebas harus berada dalam namespace yang sama dengan kelas tempat operasinya).
Matthieu M.
1
@ereOn: sebenarnya, kamu tidak. ADL adalah tentang memperluas cakupan untuk mencari agar secara otomatis memasukkan ruang nama yang menjadi tempat argumen. Ada artikel ACCU yang bagus tentang resolusi kelebihan, yang sayangnya melewatkan bagian pencarian nama. Pencarian nama melibatkan fungsi mengumpulkan kandidat, Anda mulai dengan melihat dalam ruang lingkup saat ini + cakupan argumen. Jika tidak ada nama yang cocok dengan itu, Anda naik ke lingkup induk dari lingkup saat ini dan mencari lagi ... sampai Anda mencapai lingkup global.
Matthieu M.
1
@ BЈовић maaf, tapi untuk alasan apa fungsi end () Anda mengembalikan pointer berbahaya? Saya tahu ini berhasil, tetapi saya ingin memahami logika ini. Akhir dari array adalah v [9], mengapa Anda pernah mengembalikan v [10]?
gedamial
1
@gedamial saya setuju. Saya pikir seharusnya begitu return v + 10. &v[10]referensi lokasi memori hanya melewati array.
Millie Smith
16

Jika Anda ingin mendukung iterasi kelas secara langsung dengan anggotanya std::vectoratau std::mapanggota, berikut adalah kode untuk itu:

#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;
}
Chris Redford
sumber
2
Itu layak disebut yang const_iteratorjuga dapat diakses dalam auto(C ++ 11) -yang kompatibel dengan cara melalui cbegin, cend, dll
underscore_d
2

Di sini, saya membagikan contoh paling sederhana untuk membuat tipe kustom, yang akan bekerja dengan " range-based for loop ":

#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;
}

Semoga bermanfaat bagi beberapa pengembang pemula seperti saya: p :)
Terima kasih.

RajibTheKing
sumber
mengapa tidak mengalokasikan satu elemen tambahan untuk menghindari dereferencing memori yang tidak valid dalam metode akhir Anda?
AndersK
@ Anders Karena hampir semua ujung-iterator menunjuk ke setelah akhir struktur yang mengandung mereka. The end()fungsi itu sendiri jelas tidak dereference lokasi memori yang tidak tepat, karena hanya mengambil 'alamat-dari' lokasi memori ini. Menambahkan elemen tambahan berarti Anda akan membutuhkan lebih banyak memori, dan menggunakan your_iterator::end()dengan cara apa pun yang akan mengurangi nilai itu tidak akan bekerja dengan iterator lain karena mereka dibangun dengan cara yang sama.
Qqwy
@Qqwy metode akhirnya de-refences - return &data[sizeofarray]IMHO seharusnya mengembalikan data alamat + sizeofarray tapi apa yang saya tahu,
AndersK
@Anders Anda benar. Terima kasih telah membuat saya tajam :-). Ya, data + sizeofarrayakan menjadi cara yang tepat untuk menulis ini.
Qqwy
1

Jawaban Chris Redford juga bekerja untuk kontainer Qt (tentu saja). Berikut ini adalah adaptasi (pemberitahuan saya mengembalikan constBegin(), masing-masing constEnd()dari metode 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(); }
};
pengguna2366975
sumber
0

Saya ingin menguraikan beberapa bagian dari jawaban @Steve Jessop, yang awalnya saya tidak mengerti. Semoga ini bisa membantu.

std::begintetap memanggil begin()fungsi anggota, jadi jika Anda hanya menerapkan salah satu di atas, maka hasilnya akan sama tidak peduli yang mana yang Anda pilih. Itu adalah hasil yang sama untuk rentang berbasis untuk loop, dan juga hasil yang sama untuk kode fana belaka yang tidak memiliki aturan resolusi nama magis sendiri sehingga hanya using std::begin;diikuti oleh panggilan yang tidak memenuhi syarat untuk begin(a).

Jika Anda menerapkan fungsi anggota dan fungsi ADL , maka rentang berbasis untuk loop harus memanggil fungsi anggota, sedangkan manusia biasa akan memanggil fungsi ADL. Pastikan mereka melakukan hal yang sama dalam kasus itu!


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

  • Jika ...
  • Jika range_expressionadalah ekspresi dari tipe kelas Cyang memiliki anggota bernama begindan anggota bernama end(terlepas dari jenis atau aksesibilitas anggota tersebut), maka begin_expradalah __range.begin() dan end_expradalah __range.end();
  • Kalau tidak, begin_exprini begin(__range)dan end_expritu end(__range), yang ditemukan melalui pencarian argumen-dependen (pencarian non-ADL tidak dilakukan).

Untuk rentang berbasis untuk loop, fungsi anggota dipilih terlebih dahulu.

Tapi untuk

using std::begin;
begin(instance);

Fungsi ADL dipilih terlebih dahulu.


Contoh:

#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.
//      }
}
Rick
sumber