Como fazer o meu tipo personalizado para trabalhar com “loops baseados em intervalo”?

Como muitas pessoas hoje em dia, tenho tentado os diferentes resources que o C + 11 traz. Um dos meus favoritos é o “loop baseado em intervalo for loops”.

Eu entendi aquilo:

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

É equivalente a:

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

E isso begin() simplesmente retorna a.begin() para contêineres padrão.

Mas e se eu quiser fazer o meu tipo personalizado “baseado em intervalo for loop” -aware ?

Devo apenas especializar begin() e end() ?

Se meu tipo personalizado pertencer ao namespace xml , devo definir xml::begin() ou std::begin() ?

Em suma, quais são as diretrizes para fazer isso?

A norma foi alterada desde que a pergunta (e a maioria das respostas) foi publicada na resolução deste relatório de defeitos .

A maneira de fazer um loop for(:) funciona no seu tipo X é agora uma das duas maneiras:

  • Criar membros X::begin() e X::end() que retornam algo que age como um iterador

  • Crie uma function livre begin(X&) e end(X&) que retorne algo que atue como um iterador, no mesmo namespace do seu tipo X

E semelhante para variações const . Isso funcionará tanto em compiladores que implementam as mudanças no relatório de defeitos, quanto nos compiladores que não o fazem.

Os objects retornados não precisam ser realmente iteradores. O loop for(:) , diferentemente da maioria das partes do padrão C ++, é especificado para expandir para algo equivalente a :

 for( range_declaration : range_expression ) 

torna-se:

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

onde as variables ​​que começam com __ são apenas para exposição, e begin_expr e end_expr é a mágica que chama begin / end .

Os requisitos sobre o valor de retorno inicial / final são simples: Você deve sobrecarregar pre- ++ , garantir que as expressões de boot sejam válidas, binário != Que pode ser usado em um contexto booleano, unário * que retorna algo que você pode atribuir-initialize range_declaration com e expor um destruidor público.

Fazê-lo de uma forma que não é compatível com um iterador é provavelmente uma má ideia, já que futuras iterações do C ++ podem ser relativamente arrogantes em quebrar seu código se você o fizer.

Como um aparte, é razoavelmente provável que uma revisão futura do padrão permitirá que o end_expr retorne um tipo diferente do begin_expr . Isso é útil na medida em que permite uma avaliação de “final lento” (como detectar a terminação nula) que é fácil de otimizar para ser tão eficiente quanto um loop C escrito à mão e outras vantagens semelhantes.


¹ Observe que for(:) loops for(:) armazena qualquer temporário em uma variável auto&& e passa para você como um lvalue. Você não pode detectar se você está iterando sobre um temporário (ou outro rvalue); essa sobrecarga não será chamada por um loop for(:) . Veja [stmt.ranged] 1.2-1.3 de n4527.

² Chame o método begin / end , ou a pesquisa somente ADL do begin / end function livre, ou a mágica para o suporte ao array no estilo C. Note que std::begin não é chamado a menos que range_expression retorne um object do tipo em namespace std ou dependente dele.


Em c ++ 17 a expressão range-for foi atualizada

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

com os tipos __begin e __end foram dissociados.

Isso permite que o iterador final não seja do mesmo tipo que o begin. Seu tipo de iterador final pode ser um “sentinela” que suporta apenas != Com o tipo de iniciador.

Um exemplo prático de por que isso é útil é que seu iterador final pode ler “cheque seu char* para ver se ele aponta para '0' ” quando == com um char* . Isso permite que uma expressão de intervalo C ++ para gerar código ideal ao iterar em um buffer char* terminado com nulo.

 struct null_sentinal_t { template{},int> =0 > friend bool operator==(Rhs const& ptr, null_sentinal_t) { return !*ptr; } template{},int> =0 > friend bool operator!=(Rhs const& ptr, null_sentinal_t) { return !(ptr==null_sentinal_t{}); } template{},int> =0 > friend bool operator==(null_sentinal_t, Lhs const& ptr) { return !*ptr; } template{},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; } }; 

exemplo vivo em um compilador sem suporte total ao C ++ 17; for loop expandido manualmente.

A parte relevante da norma é 6.5.4 / 1:

se _RangeT é um tipo de class, os ids não-iniciados começam e terminam no escopo da class _RangeT como se fosse por uma consulta de access de membro de class (3.4.5), e se um deles (ou ambos) encontrar pelo menos uma declaração, comece – expr e end-expr são __range.begin() e __range.end() , respectivamente;

– caso contrário, begin-expr e end-expr são begin(__range) e end(__range) , respectivamente, onde begin e end são procurados com lookup dependente de argumento (3.4.2). Para fins de pesquisa de nome, namespace std é um namespace associado.

Então, você pode fazer o seguinte:

  • definir funções de membro begin e end
  • defina funções livres de begin e end que serão encontradas pelo ADL (versão simplificada: coloque-as no mesmo namespace da class)
  • especializar std::begin e std::end

std::begin chama a function de membro begin() qualquer maneira, então se você implementar apenas uma das opções acima, os resultados devem ser os mesmos, não importa qual você escolher. Esses são os mesmos resultados for loops forçados baseados em ranged, e também o mesmo resultado para um mero código mortal que não tem suas próprias regras de resolução de nomes mágicos, portanto, apenas using std::begin; seguido por uma chamada não qualificada para begin(a) .

Se você implementar as funções de membro e as funções ADL, então, os loops baseados em intervalo devem chamar as funções de membro, enquanto meros mortais chamarão as funções de ADL. É melhor que eles façam a mesma coisa nesse caso!

Se a coisa que você está escrevendo implementar a interface do contêiner, ela terá as funções de membro begin() e end() , o que deve ser suficiente. Se for um intervalo que não é um contêiner (o que seria uma boa ideia se for imutável ou se você não souber o tamanho na frente), você está livre para escolher.

Das opções que você coloca, observe que você não deve sobrecarregar std::begin() . Você tem permissão para especializar modelos padrão para um tipo definido pelo usuário, mas, além disso, adicionar definições ao namespace std é um comportamento indefinido. Mas, de qualquer forma, a especialização de funções padrão é uma má escolha, porque a falta de especialização de function parcial significa que você só pode fazer isso para uma única class, não para um modelo de class.

Devo apenas especializar begin () e end ()?

Tanto quanto sei, isso é suficiente. Você também precisa garantir que o incremento do ponteiro seja obtido do começo ao fim.

Próximo exemplo (está faltando versão const de começar e fim) compila e funciona bem.

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

Aqui está outro exemplo com begin / end como funções. Eles precisam estar no mesmo namespace da class, devido à ADL:

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

Escrevo minha resposta porque algumas pessoas podem estar mais felizes com exemplos simples da vida real, sem inclusões de STL.

Eu tenho a minha própria implementação de array de dados simples apenas por algum motivo, e eu queria usar o intervalo baseado em loop. Aqui está a minha solução:

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

Então o exemplo de uso:

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

Caso você queira retornar a iteração de uma class diretamente com seu membro std::vector ou std::map , aqui está o código para isso:

 #include  using std::cout; using std::endl; #include  using std::string; #include  using std::vector; #include  using std::map; ///////////////////////////////////////////////////// /// classs ///////////////////////////////////////////////////// class VectorValues { private: vector v = vector(10); public: vector::iterator begin(){ return v.begin(); } vector::iterator end(){ return v.end(); } vector::const_iterator begin() const { return v.begin(); } vector::const_iterator end() const { return v.end(); } }; class MapValues { private: map v; public: map::iterator begin(){ return v.begin(); } map::iterator end(){ return v.end(); } map::const_iterator begin() const { return v.begin(); } map::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; } 

Aqui, estou compartilhando o exemplo mais simples de criar um tipo personalizado, que funcionará com ” loop baseado em intervalo “:

 #include using namespace std; template 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 numberList; numberList.addData(20.25); numberList.addData(50.12); for(auto val: numberList){ cout<  

Espero que seja útil para alguns desenvolvedores novatos como eu: p 🙂
Obrigado.

A resposta de Chris Redford também funciona para contêineres do Qt (é claro). Aqui está uma adaptação (note que eu retorno um constBegin() , respectivamente constEnd() dos methods const_iterator):

 class MyCustomClass{ QList data_; public: // ctors,dtor, methods here... QList::iterator begin() { return data_.begin(); } QList::iterator end() { return data_.end(); } QList::const_iterator begin() const{ return data_.constBegin(); } QList::const_iterator end() const{ return data_.constEnd(); } };