Özel türümü "döngüler için aralık tabanlı" ile nasıl çalıştırabilirim?


252

Bugünlerde birçok insan gibi C ++ 11'in getirdiği farklı özellikleri deniyorum. Benim favorilerimden biri "döngüler için aralık tabanlı".

Onu anlıyorum:

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

Şuna eşittir:

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

Ve bu begin()sadece a.begin()standart kaplar için geri döner .

Ancak, özel türümü "döngü için aralık tabanlı" yapmak istiyorsam ne olur ?

Sadece uzmanlaşmak mı begin()ve end()?

Özel türüm ad alanına aitse xml, xml::begin()veya std::begin()?

Kısacası, bunu yapmanın yönergeleri nelerdir?


Bir üyeyi begin/endveya bir arkadaşı tanımlayarak , statik veya serbest olarak mümkündür begin/end. Ücretsiz isim koyduğunuz ad alanına dikkat edin: stackoverflow.com/questions/28242073/…
alfC

Herkes bir kap DEĞİLDİR bir reel değer aralığının örnekle bir cevap sonrası misiniz: for( auto x : range<float>(0,TWO_PI, 0.1F) ) { ... }. `` Operatör! = () `` Nin tanımlanması zor olduğu gerçeğini nasıl bulduğunuzu merak ediyorum. Ve *__beginbu durumda silme işlemi ( ) ne olacak ? Birinin bize nasıl gösterdi eğer büyük bir katkı olacağını düşünüyorum o yapılır!
BitTickler

Yanıtlar:


183

Soru (ve çoğu cevap) bu hata raporunun çözümünde gönderildiği için standart değiştirildi .

Bir for(:)döngüyü türünüzde çalıştırmanın Xyolu artık iki yoldan biridir:

  • Üye oluşturun X::begin()ve X::end()yineleyici gibi davranan bir şey döndürün

  • Ücretsiz bir işlev oluşturun begin(X&)ve türünüzle end(X&)aynı ad alanında yineleyici gibi davranan bir şey Xdöndürün.

Ve constvaryasyonlar için benzer . Bu, hem hata raporu değişikliklerini uygulayan derleyiciler hem de yapmayan derleyiciler üzerinde çalışır.

Döndürülen nesnelerin aslında yineleyici olması gerekmez. for(:)Döngü, C ++ standart birçok yerinde farklı olarak bir şey eşdeğer genişletmek için belirtilen :

for( range_declaration : range_expression )

dönüşür:

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

ile başlayan değişkenler nerede __sadece fuar için vardır ve begin_exprve end_exprsihirli olduğuna çağrılar begin/ end

Başlangıç ​​/ bitiş dönüş değeriyle ilgili gereksinimler basittir: Önceden aşırı yükleme yapmalı ++, başlatma ifadelerinin geçerli olduğundan, !=bir boole bağlamında kullanılabilecek ikili *, atayabileceğiniz / başlatabileceğiniz bir şeyi döndüren tekli range_declarationve herkese açık olarak göstermelisiniz yıkıcı.

Bunu yineleyici ile uyumlu olmayan bir şekilde yapmak, muhtemelen kötü bir fikirdir, çünkü C ++ 'ın gelecekteki yinelemeleri, kodunuzu kırmak konusunda nispeten daha tehlikeli olabilir.

Bir yana, standardın gelecekteki bir revizyonunun bundan end_exprfarklı bir tür döndürmesine izin vermesi makul bir olasılıktır begin_expr. Bu, elle yazılmış bir C döngüsü kadar verimli olacak şekilde optimize edilmesi kolay "tembel sonlandırma tespiti gibi" ve diğer benzer avantajlara izin verdiği için faydalıdır.


Lo for(:)Döngülerin geçici bir auto&&değişkeni sakladığını ve bunu size değer olarak ilettiğini unutmayın. Geçici (veya başka bir değer) üzerinden yineleme yapıp yapmadığınızı algılayamazsınız; böyle bir aşırı yük bir for(:)döngü tarafından çağrılmaz . Bkz. N4527'den [stmt.ranged] 1.2-1.3.

² çağrı Ya begin/ endyöntemini veya ADL okunur arama serbest fonksiyonu begin/ end, veya C tarzı dizi destek için büyü. Aynı tipte veya std::beginona bağımlı range_expressionbir nesne döndürmediği sürece çağrılmadığını unutmayın namespace std.


İçinde for-for ifadesi güncellendi

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

__beginve __endayrıştırılmış türleri ile .

Bu, son yineleyicinin start ile aynı tipte olmamasına izin verir. Bitiş yineleyici türünüz, yalnızca başlangıç !=yineleyici türüyle desteklenen bir "sentinel" olabilir .

Bunun neden faydalı olduğuna dair pratik bir örnek, son yinelemenizin a ile " char*işaret edip etmediğini kontrol etmek" i okuyabilmesidir . Bu, bir C ++ aralığı-for ifadesinin, boş sonlandırılmış bir arabellek üzerinden yineleme yaparken en uygun kodu üretmesini sağlar .'0'==char*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;
  }
};

tam C ++ 17 desteği olmayan bir derleyicide canlı örnek ; fordöngü elle genişletildi.


Menzil tabanlı farklı bir arama mekanizması kullanıyorsa, bu aralık tabanlı normal kodda mevcut olandan farklı bir çift beginve endişlev alır . Belki daha sonra farklı davranmak için çok uzman olabilirler (mümkün olan en üst düzeye çıkarmak için son argümanı göz ardı ederek daha hızlı olabilirler.) Ama bunu nasıl yapacağımdan emin olmak için ad alanlarıyla yeterince iyi değilim.
Aaron McDaid

@AaronMcÇok pratik değil. Kolayca şaşırtıcı sonuçlar elde edersiniz, çünkü başlangıç ​​/ bitiş çağrısının bazı yolları başlangıç ​​/ bitiş için aralığa dayalı olur ve diğerleri olmaz. (İstemci tarafından) zararsız değişiklikler davranış değişiklikleri alır.
Yakk - Adam Nevraumont

1
İhtiyacınız yok begin(X&&). Geçici, havada auto&&dayalı bir aralıkta havada askıya alınır ve beginher zaman bir değer ( __range) ile çağrılır .
TC

2
Bu cevap, kişinin kopyalayıp uygulayabileceği bir şablon örneğinden gerçekten fayda sağlayacaktır.
Tomáš Zato - Monica'yı geri yükle

Yineleyici türü (*, ++,! =) Özellikleri üzerinde vurgu yapmak istiyorum. Yineleyici türü teknik özellikleri daha cesur yapmak için bu yanıtı yeniden söylemenizi istemeliyim.
Kırmızı Dalga

62

Cevabımı yazıyorum çünkü bazı insanlar STL içermeyen basit gerçek hayattan daha mutlu olabilirler.

Herhangi bir nedenle kendi düz sadece veri dizisi uygulaması var ve döngü için aralığı kullanmak istedim. İşte benim çözümüm:

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

Sonra kullanım örneği:

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

2
Örnek, begin () ve end () yöntemlerine sahiptir ve ayrıca herhangi bir özel kap türü için kolayca ayarlanabilen temel (anlaşılması kolay) örnek yineleyici sınıfına sahiptir. Std :: array <> ve olası herhangi bir alternatif uygulamanın karşılaştırılması farklı bir sorudur ve bence döngü için aralık tabanlı ile hiçbir ilgisi yoktur.
csjpeter

Bu çok özlü ve pratik bir cevap! Tam da aradığım şey buydu! Teşekkürler!
Zac Taylor

1
İçin const dönüş niteleyicisini kaldırmak const DataType& operator*()ve kullanıcının const auto&veya kullanmayı seçmesine izin vermek daha uygun olur auto&mu? Yine de teşekkürler, büyük cevap;)
Rick

53

Standardın ilgili kısmı 6.5.4 / 1'dir:

_RangeT bir sınıf tipiyse, niteliksiz kimlikler başlar ve biter _RangeT sınıfı kapsamında sanki sınıf üyesi erişim araması (3.4.5) ile aranır ve eğer (veya her ikisi de) en az bir bildirim bulursa, başlar - ifade ve bitiş ifade sırasıyla __range.begin()ve __range.end();

- Aksi başlamak-expr ve son expr olan begin(__range)ve end(__range)sırasıyla, bağımsız ve bağımlı arama (3.4.2) ile aranır nerede başlar ve biter. Bu isim araması için ad alanı std, ilişkili bir ad alanıdır.

Böylece, aşağıdakilerden herhangi birini yapabilirsiniz:

  • tanım beginve endüye fonksiyonları
  • ADL tarafından bulunacak tanımlayın beginve endserbest bırakın (basitleştirilmiş sürüm: bunları sınıfla aynı ad alanına koyun)
  • uzmanlaş std::beginvestd::end

std::beginbegin()üye işlevini yine de çağırır , bu nedenle yukarıdakilerden yalnızca birini uygularsanız, hangisini seçerseniz seçin sonuçların aynı olması gerekir. Bu, döngüler için aralık tabanlı için aynı sonuçlar ve aynı zamanda kendi büyülü ad çözümleme kurallarına sahip olmayan sadece ölümlü kod için de aynı sonuç sadece using std::begin;niteliksiz bir çağrı gelir begin(a).

Üye işlevlerini ve ADL işlevlerini uygularsanız, döngüler için aralık tabanlı üye işlevlerini çağırırken, yalnızca ölümlüler ADL işlevlerini çağırır. Bu durumda aynı şeyi yaptıklarından emin ol!

Yazdığınız şey kapsayıcı arabirimini uygularsa, zaten ve sahip olması gereken işlevler begin()ve end()üye işlevleri yeterli olacaktır. Bir kap olmayan bir aralıksa (değişmezse veya ön taraftaki boyutu bilmiyorsanız iyi bir fikir olurdu), seçim yapmakta özgürsünüz.

Yerleştirdiğiniz seçeneklerden aşırı yüklenmemeniz gerektiğini unutmayın std::begin(). Kullanıcı tanımlı bir tür için standart şablonları özelleştirmenize izin verilir, ancak bunun dışında ad alanı std'sine tanım eklemek tanımsız bir davranıştır. Ancak, kısmi işlev uzmanlığının olmaması, sınıf şablonu için değil, yalnızca tek bir sınıf için yapabileceğiniz anlamına gelirse, standart işlevlerin uzmanlaştırılması kötü bir seçimdir.


Yineleyicinin çok fazla karşıladığı belirli gereksinimler yok mu? yani bir ForwardIterator veya bu satırlar boyunca bir şey olabilir.
Pubby

2
@Pubby: 6.5.4'e baktığımda, InputIterator'ın yeterli olduğunu düşünüyorum. Ama aslında ben iade tipi sanmıyorum sahiptir için aralık tabanlı için hiç bir yineleyici olması. İfade, standartta eşdeğeriyle tanımlanır, bu nedenle yalnızca standarttaki kodda kullanılan ifadeleri uygulamak yeterlidir: işleçler !=, önek ++ve tekli *. Muhtemelen bu akılsızca uygulamak begin()ve end()üye işlevleri veya üye olmayan ADL fonksiyonları bir yineleyici dışındaki dönüş şey olduğunu, ama bunun yasal olduğunu düşünüyorum. std::beginYineleyici olmayan bir dönüş için uzmanlaşmış UB olduğunu düşünüyorum.
Steve Jessop

Std :: begin dosyasını aşırı yüklememeniz gerektiğinden emin misiniz? Soruyorum çünkü standart kütüphane bunu birkaç durumda yapıyor.
ThreeBit

@ThreeBit: evet, eminim. Standart kütüphane uygulamaları için kurallar, programların kurallarından farklıdır.
Steve Jessop


34

Sadece begin () ve end () konusunda uzmanlaşmalı mıyım?

Bildiğim kadarıyla bu yeterli. Ayrıca işaretçiyi artırmanın başından sonuna kadar gideceğinden emin olmalısınız.

Sonraki örnek (başlangıç ​​ve bitiş const sürümü eksik) derler ve iyi çalışıyor.

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

İşlev olarak start / end ile başka bir örnek. Onlar zorunda çünkü ADL'nin, sınıfla aynı ad be:

#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 Sınıfın tanımlandığı ad alanında aynı. İkinci örneğe bakın
BЈовић

2
Tebrikler de :) İkinci örnek için Argüman Bağımlı Arama (ADL) veya Koenig Arama terimlerini belirtmeye değer olabilir ( ücretsiz işlevin neden üzerinde çalıştığı sınıfla aynı ad alanında olması gerektiğini açıklamak için).
Matthieu M.

1
@ereOn: aslında, bilmiyorsun. ADL, bağımsız değişkenlerin ait olduğu ad alanlarını otomatik olarak dahil etmek için kapsamları aramaya genişletmekle ilgilidir. Ne yazık ki isim arama bölümünü atlayan aşırı yük çözünürlüğü hakkında iyi bir ACCU makalesi var. Ad arama adaylar işlevini toplamayı içerir, geçerli kapsama + bağımsız değişkenlerin kapsamlarına bakarak başlarsınız. Eşleşen bir ad bulunmazsa, geçerli kapsamın üst kapsamına gider ve global kapsama ulaşıncaya kadar yeniden arama yaparsınız.
Matthieu M.Kasım

1
@ BЈовић üzgünüm, ama hangi nedenle end () fonksiyonunda tehlikeli bir işaretçi döndürürsünüz? İşe yaradığını biliyorum, ama bunun mantığını anlamak istiyorum. Dizinin sonu v [9] 'dur, neden v [10]' a dönersin?
gedamial

1
@ gedamial katılıyorum. Bence öyle olmalı return v + 10. &v[10]dizinin hemen yanındaki bellek konumunu dereferences.
Millie Smith

16

Bir sınıfın yinelemesini doğrudan std::vectorveya std::mapüyesi ile yedeklemek istiyorsanız , bunun kodu aşağıdadır:

#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
Söz It yetmeyecek const_iteratorda erişilebilir autoyoluyla (C ++ 11) uyumlu bir şekilde cbegin, cendvb
underscore_d

2

Burada, " döngü tabanlı for döngüsü " ile çalışacak özel tür oluşturma en basit örneğini paylaşıyorum :

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

Umarım benim gibi bazı acemi geliştiriciler için yararlı olacaktır: p :)
Teşekkür ederim.


neden son yönteminizde geçersiz belleğin kayıttan çıkarılmasını önlemek için fazladan bir öğe ayırmıyorsunuz?
AndersK

@Anders Çünkü neredeyse tüm yineleyiciler , içerdikleri yapıların bitiminden sonra işaret ederler. end()Yalnızca bu bellek konumu '-adresini' sürdüğü işlevi kendisi açıkçası, hatalı bellek konumu inceleyebilirsiniz değil. Fazladan bir öğe eklemek, daha fazla belleğe ihtiyaç duyacağınız anlamına gelir ve your_iterator::end()bu değerin, aynı şekilde oluşturuldukları için herhangi bir yineleyici ile aynı şekilde çalışmaz.
Qqwy

@Qqwy onun son yöntemi reddetti - return &data[sizeofarray]IMHO sadece adres verilerini + sizeofarray döndürmelidir ama ne biliyorum,
AndersK

@Anders Doğru. Beni keskin tuttuğun için teşekkürler :-). Evet, data + sizeofarraybunu yazmanın doğru yolu olurdu.
Qqwy

1

Chris Redford'un yanıtı Qt konteynırları için de çalışıyor (elbette). İşte bir uyarlama (uyarı const_iterator yöntemlerinden constBegin()sırasıyla bir a dön constEnd()):

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'un cevabının, ilk başta anlamadığım bazı kısımlarını ayrıntılandırmak istiyorum. Umarım yardımcı olur.

std::beginbegin()üye işlevini yine de çağırır , bu nedenle yukarıdakilerden yalnızca birini uygularsanız, hangisini seçerseniz seçin sonuçların aynı olması gerekir. Bu, döngüler için aralık tabanlı için aynı sonuçlar ve aynı zamanda kendi büyülü ad çözümleme kurallarına sahip olmayan sadece ölümlü kod için de aynı sonuç sadece using std::begin;niteliksiz bir çağrı gelir begin(a).

Üye işlevlerini ve ADL işlevlerini uygularsanız, döngüler için aralık tabanlı üye işlevlerini çağırırken, yalnızca ölümlüler ADL işlevlerini çağırır. Bu durumda aynı şeyi yaptıklarından emin ol!


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

  • Eğer ...
  • Eğer range_expressionbir sınıf türü bir ifadesidir Cadında bir üye hem sahiptir beginve adlandırılmış bir üyesini enddaha sonra, (hangi türde olursa olsun ya da bu tür üyesinin erişilebilirlik) begin_exprolduğu __range.begin() ve end_exprolduğu __range.end();
  • Aksi takdirde, begin_exprbir begin(__range)ve end_exprbir end(__range)(gerçekleştirilmez olmayan ADL arama) bağımsız ve bağımlı arama ile bulunan, hangi.

Döngü tabanlı aralık için, önce üye işlevleri seçilir.

Ama için

using std::begin;
begin(instance);

Önce ADL işlevleri seçilir.


Misal:

#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.
//      }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.