86 votes

Comment trouver l'index de l'objet courant dans une boucle for basée sur l'intervalle ?

Supposons que j'ai le code suivant :

vector<int> list;
for(auto& elem:list) {
    int i = elem;
}

Puis-je trouver la position de elem dans le vecteur sans maintenir un itérateur séparé ?

17 votes

Ce n'est pas à cela que sert la gamme pour (heh, est-ce un jeu de mots ?)

2 votes

Ceci n'est pas possible dans les conteneurs STL, à moins d'utiliser std::find ou toute autre fonction excessive. On ne peut pas conclure des itérateurs à partir d'éléments contenus. Pourquoi ne pas maintenir un itérateur ?

2 votes

Pour deux raisons. La première est que tout ce que je veux faire (dans ce cas) est de voir si je suis au dernier élément ou pas :) et la seconde est que le compilateur doit en maintenir une, pourquoi ne puis-je pas y accéder ? "this" est une variable dont la portée est maintenue par le compilateur, pourquoi pas ici ? Ou alors, proposez une syntaxe alternative (mais toujours pratique) qui, comme le fait javascript, définit une variable qui change au fur et à mesure que l'on avance dans la boucle. for(auto& index:liste)

66voto

Matthieu M. Points 101624

Oui, vous pouvez, il faut juste un peu de massage ;)

L'astuce consiste à utiliser la composition : au lieu d'itérer sur le conteneur directement, vous le "zappez" avec un index en cours de route.

Code de la fermeture éclair spécialisée :

template <typename T>
struct iterator_extractor { typedef typename T::iterator type; };

template <typename T>
struct iterator_extractor<T const> { typedef typename T::const_iterator type; };

template <typename T>
class Indexer {
public:
    class iterator {
        typedef typename iterator_extractor<T>::type inner_iterator;

        typedef typename std::iterator_traits<inner_iterator>::reference inner_reference;
    public:
        typedef std::pair<size_t, inner_reference> reference;

        iterator(inner_iterator it): _pos(0), _it(it) {}

        reference operator*() const { return reference(_pos, *_it); }

        iterator& operator++() { ++_pos; ++_it; return *this; }
        iterator operator++(int) { iterator tmp(*this); ++*this; return tmp; }

        bool operator==(iterator const& it) const { return _it == it._it; }
        bool operator!=(iterator const& it) const { return !(*this == it); }

    private:
        size_t _pos;
        inner_iterator _it;
    };

    Indexer(T& t): _container(t) {}

    iterator begin() const { return iterator(_container.begin()); }
    iterator end() const { return iterator(_container.end()); }

private:
    T& _container;
}; // class Indexer

template <typename T>
Indexer<T> index(T& t) { return Indexer<T>(t); }

Et l'utiliser :

#include <iostream>
#include <iterator>
#include <limits>
#include <vector>

// Zipper code here

int main() {
    std::vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9};

    for (auto p: index(v)) {
        std::cout << p.first << ": " << p.second << "\n";
    }
}

Vous pouvez le voir sur idéone bien qu'il manque le support de la boucle for-range et que ce soit moins joli.

EDITAR:

Je viens de me rappeler que je devrais vérifier Boost.Range plus souvent. Malheureusement pas zip mais j'ai trouvé une perle : boost::adaptors::indexed . Cependant, il faut accéder à l'itérateur pour extraire l'index. Dommage :x

Sinon, avec le counting_range et un générique zip Je suis sûr qu'il serait possible de faire quelque chose d'intéressant...

Dans un monde idéal, j'imagine :

int main() {
    std::vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9};

    for (auto tuple: zip(iota(0), v)) {
        std::cout << tuple.at<0>() << ": " << tuple.at<1>() << "\n";
    }
}

Avec zip créer automatiquement une vue comme une gamme de tuples de références et de iota(0) en créant simplement une "fausse" gamme qui commence à partir de 0 et compte simplement vers l'infini (ou bien, le maximum de son type...).

3 votes

Et si counting_range (ou boost::counting_iterator ) + boost::zip_iterator ?

0 votes

@ildjarn : Oui, Boost.Iterators a les blocs de construction (il semble), cependant il n'y a pas de gamme correspondante, ce qui est ennuyeux.

0 votes

@Xeo Votre version fonctionne bien pour les lvalues (en effet, comme vous l'avez dit, aucune copie n'a lieu). Pour rvalues il y a un problème, cependant. Je ne l'ai pas encore repéré, mais je vais continuer à l'examiner demain. En gros, lorsque j'utilise index comme ceci for (auto x : index(std::vector<int>{2, 4, 6})) { ... } Je reçois cette erreur : error: no matching function for call to ‘Indexer<std::vector<int, std::allocator<int> > >::iterator::iterator(std::vector<int, std::allocator<int> >::const_iterator)’ . J'ai utilisé g++-4.7.

31voto

Jrok a raison : les boucles d'arrêt basées sur la distance ne sont pas conçues dans ce but.

Cependant, dans votre cas, il est possible de le calculer en utilisant l'arithmétique des pointeurs puisque vector stocke ses éléments de manière contiguë (*)

vector<int> list;
for(auto& elem:list) { 
    int i = elem;
    int pos = &elem-&list[0]; // pos contains the position in the vector 

    // also a &-operator overload proof alternative (thanks to ildjarn) :
    // int pos = addressof(elem)-addressof(list[0]); 

}

Mais c'est clairement une mauvaise pratique car elle obscurcit le code et le rend plus fragile (il se casse facilement si quelqu'un change le type de conteneur, surcharge la fonction & ou remplacer 'auto&' par 'auto'. Bonne chance pour déboguer cela !)

NOTE : La contiguïté est garantie pour les vecteurs dans la norme C++03, et pour les tableaux et les chaînes dans la norme C++11.

6 votes

Oui, c'est indiqué dans la norme. La contiguïté est garantie pour vector en C++03, et array y string en C++11.

1 votes

" il se brise facilement si quelqu'un ... surcharge le & opérateur " C'est ce que std::addressof est pour. :-]

0 votes

Vous avez raison. Donc la version prouvée par &-overload serait : int pos = addressof(elem)- addressof(list[0]) ; .... Le wrapper d'itérateur de Matthieu M. est bien meilleur :)

20voto

Nicol Bolas Points 133791

Non, vous ne pouvez pas (du moins, pas sans effort). Si vous avez besoin de la position d'un élément, vous ne devriez pas utiliser la méthode range-based for. N'oubliez pas qu'il s'agit d'un outil pratique pour le cas le plus courant : exécuter du code pour chaque élément. Dans les circonstances moins courantes où vous avez besoin de la position de l'élément, vous devez utiliser l'outil régulier moins pratique for boucle.

11voto

Michael Points 193

Si vous disposez d'un compilateur prenant en charge C++14, vous pouvez le faire dans un style fonctionnel :

#include <iostream>
#include <string>
#include <vector>
#include <functional>

template<typename T>
void for_enum(T& container, std::function<void(int, typename T::value_type&)> op)
{
    int idx = 0;
    for(auto& value : container)
        op(idx++, value);
}

int main()
{
    std::vector<std::string> sv {"hi", "there"};
    for_enum(sv, [](auto i, auto v) {
        std::cout << i << " " << v << std::endl;
    });
}

Fonctionne avec clang 3.4 et gcc 4.9 (pas avec 4.8) ; pour les deux, il est nécessaire de configurer -std=c++1y . La raison pour laquelle vous avez besoin de c++14 est à cause de la auto dans la fonction lambda.

4 votes

std::function utilise l'effacement de type qui est coûteux. Pourquoi ne pas utiliser template<typename T, typename Callable> void for_enum(T& container, Callable op) pour ne pas avoir à payer pour l'effacement des caractères ?

2voto

alfC Points 881

J'ai lu dans vos commentaires qu'une des raisons pour lesquelles vous voulez connaître l'indice est de savoir si l'élément est le premier/dernier de la séquence. Si c'est le cas, vous pouvez faire

for(auto& elem:list) {
//  loop code ...
    if(&elem == &*std::begin(list)){ ... special code for first element ... }
    if(&elem == &*std::prev(std::end(list))){ ... special code for last element ... }
//  if(&elem == &*std::rbegin(list)){... (C++14 only) special code for last element ...}
//  loop code ... 
}

EDITAR: Par exemple, ceci imprime un conteneur en sautant un séparateur dans le dernier élément. Fonctionne pour la plupart des conteneurs que je peux imaginer (y compris les tableaux), (démonstration en ligne http://coliru.stacked-crooked.com/a/9bdce059abd87f91 ):

#include <iostream>
#include <vector>
#include <list>
#include <set>
using namespace std;

template<class Container>
void print(Container const& c){
  for(auto& x:c){
    std::cout << x; 
    if(&x != &*std::prev(std::end(c))) std::cout << ", "; // special code for last element
  }
  std::cout << std::endl;
}

int main() {
  std::vector<double> v{1.,2.,3.};
  print(v); // prints 1,2,3
  std::list<double> l{1.,2.,3.};
  print(l); // prints 1,2,3
  std::initializer_list<double> i{1.,2.,3.};
  print(i); // prints 1,2,3
  std::set<double> s{1.,2.,3.};
  print(s); // print 1,2,3
  double a[3] = {1.,2.,3.}; // works for C-arrays as well
  print(a); // print 1,2,3
}

0 votes

Veuillez noter (avant tout downvoting injustifié) que l'auteur de la question pose cette question dans le contexte de la détection du dernier élément dans une boucle for-ranged pour un conteneur. Pour cela, je ne vois pas pourquoi la comparaison de &elem y &*std::prev(std::end(list)) ne fonctionnera pas ou ne sera pas pratique. Je suis d'accord avec l'autre réponse qu'un for basé sur un itérateur est plus approprié pour cela, mais quand même.

0 votes

Il semble plus facile de déclarer int i=c.size(); avant la boucle et le test if(--i==0) .

0 votes

@MarcGlisse, le int i n'était qu'un exemple. Je vais le supprimer pour éviter toute confusion. Même si vous utilisez size avant la boucle, vous aurez besoin d'un compteur.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X