web-dev-qa-db-fra.com

Comment faire en sorte que mon type personnalisé fonctionne avec des "boucles for basées sur une plage"?

Comme beaucoup de gens, j'ai essayé les différentes fonctionnalités de C + 11. L'un de mes favoris est la "plage 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 ce begin() renvoie simplement a.begin() pour les conteneurs standard.

Mais que se passe-t-il si je veux rendre mon type personnalisé "au sens de la plage"?

Devrais-je simplement 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 directives pour le faire?

226
ereOn

La norme a été modifiée depuis que la question (et la plupart des réponses) ont été postées dans la résolution du présent rapport de défaut .

La façon de faire fonctionner une boucle for(:) sur votre type X est maintenant l'une des deux manières suivantes:

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

  • Créez une fonction libre begin(X&) et end(X&), qui renvoie quelque chose qui se comporte comme un itérateur, dans le même espace de nom que votre type X. ¹

Et similaire pour const variations. Cela fonctionnera à la fois sur les compilateurs qui implémentent les modifications du rapport de défauts et sur ceux qui ne le font pas.

Les objets retournés ne doivent pas nécessairement être des itérateurs. La boucle for(:), contrairement à la plupart des parties de la norme C++, est spécifiée pour être étendue à 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 __ ne sont que pour une exposition, et begin_expr et end_expr est la magie qui appelle begin/end. ²

La configuration requise pour la valeur de début/fin est simple: vous devez surcharger pre -++, vous assurer que les expressions d'initialisation sont correctes, binaire != pouvant être utilisé dans un contexte booléen, unary * qui retourne quelque chose que vous pouvez assigner-initialiser range_declaration avec, et exposer un destructeur public.

Faire cela d'une manière qui ne soit pas compatible avec un itérateur est probablement une mauvaise idée, car les futures itérations de C++ pourraient être relativement cavalières quant à la modification de votre code si vous le faites.

En passant, il est raisonnablement probable qu'une future révision de la norme permettra à end_expr de renvoyer un type différent de begin_expr. Ceci est utile dans la mesure où il permet une évaluation "lazy-end" (comme la détection de la terminaison nulle) facile à optimiser pour être aussi efficace qu'une boucle C écrite à la main et d'autres avantages similaires.


¹ Notez que les boucles for(:) stockent toutes les valeurs temporaires dans une variable auto&& et vous les transmettent en tant que lvalue. Vous ne pouvez pas détecter si vous effectuez une itération sur une valeur temporaire (ou autre); une telle surcharge ne sera pas appelée par une boucle for(:). Voir [rangées] 1.2-1.3 de n4527.

² Appelez la méthode begin/end ou recherchez dans la fonction libre uniquement ADL begin/end, ou magique pour la prise en charge des tableaux de style C. Notez que std::begin n'est appelé que si range_expression renvoie un objet de type dans namespace std ou dépend de celui-ci.


Dans c ++ 17 la plage d'expression 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 __begin et __end ont été découplés.

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

Un exemple pratique de l’utilité de cette opération est que votre itérateur final peut lire "vérifiez votre char* pour voir s’il pointe vers '0'" lorsque == avec un char*. Cela permet à une expression d'intervalle C++ de générer un code optimal lors d'une itération sur un tampon char* à terminaison nulle.

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; for boucle développée manuellement.

154

La partie pertinente de la norme est 6.5.4/1:

si _RangeT est un type de classe, les débuts et fins d'id-qualifiée sont recherchés dans l'étendue de la classe _RangeT comme si la recherche d'accès du membre de la classe (3.4.5) et si l'une (ou les deux) trouve au moins une déclaration, commence - expr et end-expr sont respectivement __range.begin() et __range.end();

- sinon, begin-expr et end-expr sont begin(__range) et end(__range), respectivement, où begin et end sont recherchées avec une recherche dépendante de l'argument (3.4.2). Pour les besoins de cette recherche de nom, namespace std est un namespace associé.

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

  • définir les fonctions membres begin et end
  • define begin et end fonctions libres qui seront trouvées par ADL (version simplifiée: mettez-les dans le même espace de nom que la classe)
  • spécialiser std::begin et std::end

std::begin appelle quand même la fonction membre begin(). Ainsi, si vous ne mettez en œuvre que l'un des éléments ci-dessus, les résultats doivent être identiques, quel que soit votre choix. Il en va de même pour les boucles for à distance, et également pour le code mortel qui n'a pas ses propres règles de résolution de noms magiques, donc 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, alors que les mortels appellent les fonctions ADL. Le mieux est de s’assurer qu’ils font la même chose dans ce cas!

Si l'élément que vous écrivez implémente l'interface conteneur, il aura déjà les fonctions membres begin() et end(), ce qui devrait suffire. 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 des modèles standard pour un type défini par l'utilisateur, mais en dehors de cela, l'ajout de définitions à namespace std est un comportement indéfini. Quoi qu'il en soit, la spécialisation des fonctions standard est un mauvais choix, ne serait-ce que parce que l'absence 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.

52
Steve Jessop

J'écris ma réponse parce que certaines personnes pourraient être plus heureuses avec des exemples simples de la vie réelle sans STL.

Pour une raison quelconque, j’ai ma propre implémentation de tableau de données simple et clair et je voulais utiliser la plage basée sur la boucle for. 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 ...
 };

Puis l'exemple d'utilisation:

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

Devrais-je me spécialiser avec begin () et end ()?

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

L'exemple suivant (il manque la version const de begin and end) compile et fonctionne bien.

#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 begin/end en tant que fonctions. Ils doivent être dans le même espace de noms que la classe, à cause de 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;
    }
}
32
BЈовић

Si vous voulez sauvegarder directement l'itération d'une classe avec son membre std::vector ou std::map, voici le code correspondant:

#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;
}
13
Chris Redford

La réponse de Chris Redford fonctionne également pour les conteneurs Qt (bien sûr). Voici une adaptation (notez que je retourne une 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(); }
};
1
user2366975

Ici, je partage le plus simple exemple 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 cela sera utile pour certains développeurs novices comme moi: p :)
Merci.

1
RajibTheKing