Comment faire fonctionner mon type personnalisé avec «basé sur la plage pour les boucles»?

252

Comme beaucoup de gens ces jours-ci, j'ai essayé les différentes fonctionnalités qu'apporte C ++ 11. Un de mes favoris est le "basé sur la gamme pour les boucles".

Je comprends que:

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

Est équivalent à:

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

Et cela begin()revient simplement a.begin()pour des conteneurs standard.

Mais que se passe-t-il si je veux rendre mon type personnalisé "basé sur une plage pour la boucle" ?

Dois-je juste me spécialiser begin()et end()?

Si mon type personnalisé appartient à l'espace de noms xml, dois-je définir xml::begin()ou std::begin()?

En bref, quelles sont les lignes directrices pour ce faire?

ereOn
la source
C'est possible soit en définissant un membre begin/endou un ami, statique ou libre begin/end.
Faites
Quelqu'un pourrait - il s'il vous plaît poster une réponse avec l'exemple d'une plage de valeurs de flotteur qui est pas un conteneur: for( auto x : range<float>(0,TWO_PI, 0.1F) ) { ... }. Je suis curieux de savoir comment vous contournez le fait que `` opérateur! = () '' Est difficile à définir. Et qu'en est-il du déréférencement ( *__begin) dans ce cas? Je pense que ce serait une grande contribution si quelqu'un nous montrait comment cela se faisait!
BitTickler

Réponses:

183

La norme a été modifiée depuis que la question (et la plupart des réponses) ont été publiées dans la résolution de ce rapport d'anomalie .

La façon de faire fonctionner une for(:)boucle sur votre type Xest désormais l'une des deux façons suivantes:

  • Créer un membre X::begin()et X::end()renvoyer quelque chose qui agit comme un itérateur

  • Créez une fonction gratuite begin(X&)et end(X&)qui retourne quelque chose qui agit comme un itérateur, dans le même espace de nom que votre type X

Et similaire pour les constvariations. Cela fonctionnera à la fois sur les compilateurs qui implémentent les modifications de rapport de défaut et sur les compilateurs qui ne le font pas.

Les objets retournés ne doivent pas nécessairement être des itérateurs. La for(:)boucle, contrairement à la plupart des parties de la norme C ++, est spécifiée pour s'étendre à quelque chose d'équivalent à :

for( range_declaration : range_expression )

devient:

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

où les variables commençant par __sont uniquement destinées à l'exposition, et begin_expret end_exprest la magie qui appelle begin/ end

Les exigences sur la valeur de retour de début / fin sont simples: vous devez surcharger la pré- ++, vous assurer que les expressions d'initialisation sont valides, binaires !=qui peuvent être utilisés dans un contexte booléen, unaire *qui renvoie quelque chose que vous pouvez assigner-initialiserrange_declaration et exposer un public destructeur.

Le faire d'une manière qui n'est pas compatible avec un itérateur est probablement une mauvaise idée, car les futures itérations de C ++ pourraient être relativement cavalières sur la rupture de votre code si vous le faites.

Soit dit en passant, il est raisonnablement probable qu'une future révision de la norme permettra end_exprde renvoyer un type différent de celui begin_expr. Ceci est utile car il permet une évaluation "paresseuse" (comme la détection de terminaison nulle) qui est facile à optimiser pour être aussi efficace qu'une boucle C manuscrite, et d'autres avantages similaires.


¹ Notez que les for(:)boucles stockent tout temporaire dans une auto&&variable et vous le transmettent en tant que valeur l. Vous ne pouvez pas détecter si vous parcourez une valeur temporaire (ou une autre valeur); une telle surcharge ne sera pas appelée par unfor(:) boucle. Voir [stmt.ranged] 1.2-1.3 de n4527.

² Appelez la méthode begin/ end, ou la recherche ADL uniquement de la fonction gratuite begin/ end, ou magic pour la prise en charge des tableaux de style C. Notez qu'il std::beginn'est pas appelé sauf s'il range_expressionrenvoie un objet de type dans namespace stdou dépendant de celui-ci.


Dans l'expression range-for a été mise à jour

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

avec les types de __beginet __endont été découplés.

Cela permet à l'itérateur final de ne pas être du même type que begin. Votre type d'itérateur de fin peut être une "sentinelle" qui ne prend !=en charge qu'avec le type d'itérateur de début.

Un exemple pratique de la raison pour laquelle cela est utile est que votre itérateur final peut lire "vérifiez votre char*pour voir s'il pointe vers '0'" lorsqu'il est ==avec un char*. Cela permet à une expression d'intervalle C ++ de générer du code optimal lors de l'itération sur un char*tampon terminé par null .

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

exemple en direct dans un compilateur sans prise en charge complète de C ++ 17; forboucle développée manuellement.

Yakk - Adam Nevraumont
la source
Si pour une plage basée sur utilise un mécanisme de recherche différent, il est peut-être possible d'organiser cette plage pour obtenir une paire de fonctions beginet des endfonctions différentes de celles disponibles dans le code normal. Peut-être qu'ils pourraient alors être très spécialisés pour se comporter différemment (c'est-à-dire plus rapidement en ignorant l'argument de fin pour obtenir les optimisations maximales possibles.) Mais je ne suis pas assez bon avec les espaces de noms pour être sûr de savoir comment faire.
Aaron McDaid
@AaronMcDaid n'est pas très pratique. Vous vous retrouveriez facilement avec des résultats surprenants, car certains moyens d'appeler début / fin se retrouveraient avec la plage basée sur début / fin, et d'autres non. Des changements innocents (du côté client) entraîneraient des changements de comportement.
Yakk - Adam Nevraumont
1
Tu n'as pas besoin begin(X&&). Le temporaire est suspendu dans les airs par auto&&une plage basée sur, et beginest toujours appelé avec un lvalue ( __range).
TC
2
Cette réponse bénéficierait vraiment d'un exemple de modèle que l'on peut copier et implémenter.
Tomáš Zato - Réintègre Monica
Je préfère mettre l'accent sur les propriétés du type itérateur (*, ++,! =). Je devrais vous demander de reformuler cette réponse pour rendre les spécifications du type d'itérateur plus audacieuses.
Red.Wave
62

J'écris ma réponse parce que certaines personnes pourraient être plus satisfaites d'un simple exemple de la vie réelle sans STL inclus.

J'ai ma propre implémentation de tableau de données uniquement pour une raison quelconque, et je voulais utiliser la plage basée sur la boucle. Voici ma solution:

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

Ensuite, l'exemple d'utilisation:

PodArray<char> array;
// fill up array in some way
for(auto& c : array)
  printf("char: %c\n", c);
csjpeter
la source
2
L'exemple a les méthodes begin () et end (), ainsi qu'un exemple de classe d'itérateur de base (facile à comprendre) qui peut facilement être ajusté pour tout type de conteneur personnalisé. Comparer std :: array <> et toute autre implémentation possible est une question différente, et à mon avis n'a rien à voir avec la boucle for basée sur la plage.
csjpeter
C'est une réponse très concise et pratique! C'était exactement ce que je cherchais! Merci!
Zac Taylor
1
Serait-il plus approprié de supprimer le const qualificatif de retour pour const DataType& operator*(), et de laisser l'utilisateur choisir d'utiliser const auto&ou auto&? Merci quand même, bonne réponse;)
Rick
53

La partie pertinente de la norme est 6.5.4 / 1:

si _RangeT est un type de classe, les identifiants non définis commencent et finissent dans la portée de la classe _RangeT comme si par la recherche d'accès des membres de la classe (3.4.5), et si l'un (ou les deux) trouve au moins une déclaration, commencez - expr et expr-fin sont __range.begin()et __range.end(), respectivement;

- sinon, begin-expr et end-expr sont begin(__range)et end(__range), respectivement, où début et fin sont recherchés avec une recherche dépendante de l'argument (3.4.2). Aux fins de cette recherche de nom, l'espace de noms std est un espace de noms associé.

Vous pouvez donc effectuer l'une des opérations suivantes:

  • définir beginet endmembres fonctions
  • définir beginet endlibérer des fonctions qui seront trouvées par ADL (version simplifiée: les mettre dans le même espace de noms que la classe)
  • se spécialiser std::beginetstd::end

std::beginappelle de begin()toute façon la fonction membre, donc si vous implémentez uniquement l'une des options ci-dessus, les résultats devraient être les mêmes, peu importe celui que vous choisissez. Ce sont les mêmes résultats pour les boucles à distance, et aussi le même résultat pour le simple code mortel qui n'a pas ses propres règles de résolution de noms magiques, juste using std::begin;suivi d'un appel sans réserve à begin(a).

Si vous implémentez les fonctions membres et les fonctions ADL, les boucles for basées sur la plage doivent appeler les fonctions membres, tandis que les simples mortels appellent les fonctions ADL. Assurez-vous qu'ils font la même chose dans ce cas!

Si la chose que vous écrivez implémente l'interface de conteneur, il aura begin()et end()fonctions membres déjà, ce qui devrait être suffisant. S'il s'agit d'une gamme qui n'est pas un conteneur (ce qui serait une bonne idée si elle est immuable ou si vous ne connaissez pas la taille à l'avance), vous êtes libre de choisir.

Parmi les options que vous disposez, notez que vous ne devez pas surcharger std::begin(). Vous êtes autorisé à spécialiser les modèles standard pour un type défini par l'utilisateur, mais à part cela, l'ajout de définitions à l'espace de noms std est un comportement non défini. Mais de toute façon, la spécialisation des fonctions standard est un mauvais choix, ne serait-ce que parce que le manque de spécialisation partielle des fonctions signifie que vous ne pouvez le faire que pour une seule classe, pas pour un modèle de classe.

Steve Jessop
la source
N'y a-t-il pas certaines exigences auxquelles l'itérateur satisfait beaucoup? c'est-à-dire être un ForwardIterator ou quelque chose du genre.
Pubby
2
@Pubby: En regardant 6.5.4, je pense que InputIterator est suffisant. Mais en fait, je ne pense pas que le type renvoyé doit être un itérateur du tout pour une plage basée sur. L'instruction est définie dans la norme par ce à quoi elle équivaut, il suffit donc d'implémenter uniquement les expressions utilisées dans le code de la norme: opérateurs !=, préfixe ++et unaire *. Il est probablement sage de mettre en œuvre begin()et les end()fonctions membres ou des fonctions non membres ADL que tout retour autre qu'un iterator, mais je pense que c'est légal. std::beginJe pense que la spécialisation pour renvoyer un non-itérateur est UB.
Steve Jessop
Êtes-vous sûr de ne pas surcharger std :: begin? Je demande parce que la bibliothèque standard le fait dans quelques cas elle-même.
ThreeBit
@ThreeBit: oui, j'en suis sûr. Les règles pour les implémentations de bibliothèque standard sont différentes des règles pour les programmes.
Steve Jessop
3
Cela doit être mis à jour pour open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1442 .
TC
34

Dois-je juste spécialiser begin () et end ()?

Pour autant que je sache, cela suffit. Vous devez également vous assurer que l'incrémentation du pointeur se produira du début à la fin.

L'exemple suivant (il manque la version const de début et de fin) se compile et fonctionne correctement.

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

Voici un autre exemple avec des fonctions de début / fin. Ils doivent être dans le même espace de noms que la classe, à cause d'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Јовић
la source
1
@ereOn Dans le même espace de noms où la classe est définie. Voir le 2ème exemple
BЈовић
2
Félicitations également :) Il vaut peut-être la peine de mentionner les termes Argument Dependent Lookup (ADL) ou Koenig Lookup pour le deuxième exemple (pour expliquer pourquoi la fonction free devrait se trouver dans le même espace de noms que la classe sur laquelle elle fonctionne).
Matthieu M.
1
@ereOn: en fait, vous ne le faites pas. ADL consiste à étendre les étendues à rechercher pour inclure automatiquement les espaces de noms auxquels les arguments appartiennent. Il y a un bon article ACCU sur la résolution de surcharge, qui ignore malheureusement la partie de recherche de nom. La recherche de nom implique la collecte de la fonction des candidats, vous commencez par regarder dans la portée actuelle + la portée des arguments. Si aucun nom ne correspond, vous passez à l'étendue parent de l'étendue actuelle et effectuez une nouvelle recherche ... jusqu'à atteindre l'étendue globale.
Matthieu M.
1
@ BЈовић désolé, mais pour quelle raison dans la fonction end () renvoyez-vous un pointeur dangereux? Je sais que cela fonctionne, mais je veux comprendre la logique de cela. La fin du tableau est v [9], pourquoi voudriez-vous jamais retourner v [10]?
gedamial
1
@gedamial je suis d'accord. Je pense que ça devrait l'être return v + 10. &v[10]déréférence l'emplacement de mémoire juste après le tableau.
Millie Smith
16

Dans le cas où vous souhaitez sauvegarder l'itération d'une classe directement avec son membre std::vectorou std::map, voici le code pour cela:

#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
la source
2
Il convient de mentionner que const_iteratorpeut également être consulté dans un auto(C ++ 11) manière via compatible cbegin, cendetc.
underscore_d
2

Ici, je partage l'exemple le plus simple de création de type personnalisé, qui fonctionnera avec " boucle basée sur une plage ":

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

J'espère que ce sera utile pour un développeur novice comme moi: p :)
Merci.

RajibTheKing
la source
pourquoi ne pas allouer un élément supplémentaire pour éviter de déréférencer la mémoire invalide dans votre méthode de fin?
AndersK
@Anders Parce que presque tous les itérateurs finaux pointent après la fin de leur structure conteneur . leend() toute évidence, la fonction elle-même ne déréférence pas un emplacement de mémoire incorrect, car elle ne prend que «l'adresse» de cet emplacement de mémoire. L'ajout d'un élément supplémentaire signifierait que vous auriez besoin de plus de mémoire et que l'utilisation your_iterator::end()d'une manière qui déréférencerait cette valeur ne fonctionnerait de toute façon pas avec d'autres itérateurs car ils sont construits de la même manière.
Qqwy
@Qqwy sa méthode de fin dé-référence - return &data[sizeofarray] il devrait simplement renvoyer les données d'adresse + sizeofarray mais que sais-je,
AndersK
@Anders Vous avez raison. Merci de m'avoir aidé :-). Oui, ce data + sizeofarrayserait la bonne façon d'écrire ceci.
Qqwy
1

La réponse de Chris Redford fonctionne également pour les conteneurs Qt (bien sûr). Voici une adaptation (remarquez que je retourne a constBegin(), respectivement à constEnd()partir des méthodes 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(); }
};
user2366975
la source
0

Je voudrais élaborer certaines parties de la réponse de @Steve Jessop, pour lesquelles au début je ne comprenais pas. J'espère que ça aide.

std::beginappelle de begin()toute façon la fonction membre, donc si vous implémentez uniquement l'une des options ci-dessus, les résultats devraient être les mêmes, peu importe celui que vous choisissez. Ce sont les mêmes résultats pour les boucles à distance, et aussi le même résultat pour le simple code mortel qui n'a pas ses propres règles de résolution de noms magiques, juste using std::begin;suivi d'un appel non qualifié àbegin(a) .

Si vous implémentez les fonctions membres et les fonctions ADL , les boucles for basées sur la plage doivent appeler les fonctions membres, tandis que les simples mortels appellent les fonctions ADL. Assurez-vous qu'ils font la même chose dans ce cas!


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

  • Si ...
  • Si range_expressionest une expression d'un type de classe Cqui a à la fois un membre nommé beginet un membre nommé end(quel que soit le type ou l'accessibilité de ce membre), alors begin_exprest __range.begin() et end_exprest __range.end();
  • Sinon, begin_expris begin(__range)et end_expris end(__range), qui sont trouvés via la recherche dépendante de l'argument (la recherche non ADL n'est pas effectuée).

Pour une boucle basée sur une plage, les fonctions membres sont sélectionnées en premier.

Mais pour

using std::begin;
begin(instance);

Les fonctions ADL sont sélectionnées en premier.


Exemple:

#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.
//      }
}
Meule
la source