31 votes

Quelle est la façon idiomatique d'itérer un conteneur tout en incrémentant un indice entier ?

Supposons que vous souhaitiez connaître l'indice numérique d'un élément lors de l'itération d'un conteneur qui ne propose pas d'itérateurs à accès aléatoire. Par exemple :

std::list<std::string> items;
int i = 0;
for (auto & item : items) item += std::to_string(i++);

Existe-t-il une façon plus idiomatique ou plus agréable de faire cela ? Je suppose que ce modèle se présente dans diverses situations. Je n'aime pas que l'indice entier soit disponible en dehors de la boucle. Le fait de mettre entre parenthèses la boucle et la définition de l'index dans un bloc local me semble également moche.

Bien sûr, lorsque le conteneur offre des itérateurs à accès aléatoire, on peut tirer parti de la différence entre les itérateurs, mais on ne peut alors pas utiliser le range-for :

std::vector<std::string> items;
for (auto it = items.begin(); it != items.end(); ++it)
  *it += std::to_string(it - items.begin());

Bien que je ne montre qu'un exemple C++11, je cherche également des conseils pour C++0x et C++98.

30voto

TemplateRex Points 26447

Ma préférence personnelle : gardez simplement l'indice supplémentaire. C'est clair comme de l'eau de roche et si jamais vous avez un problème if() à l'intérieur de la boucle, vous pouvez aussi facilement sauter le comptage :

std::list<std::string> items;
{
    int i = 0;
    for (auto & item : items) {
        if (some_condition(item)) {
            item += std::to_string(i); // mark item as the i-th match
            ++i;
        }
    }
}

Assurez-vous juste de garder le i compteur proche de la boucle, en utilisant des { } pour créer une portée imbriquée. De plus, le post-incrément est à la limite du flou.

Alternatives : J'aimerais avoir une base à distance index_for construction du langage qui fournirait un compteur automatique i mais hélas, ce n'est pas le cas actuellement.

Toutefois, si vous tenez absolument, positivement, définitivement à un bel emballage, il est en fait instructif de regarder la sémantique de votre boucle, qui est celle d'une std::transform avec une paire de std::list et un boost::counting_iterator .

std::transform(
    begin(items), end(items), 
    boost::counting_iterator<int>(0), 
    begin(items), 
    [](auto const& elem, auto i) {
    return elem + std::to_string(i);    
});

Cette surcharge à 4 pattes de std::transform est parfois appelé zip_with C'est la raison pour laquelle certains commentaires préconisent l'utilisation d'une boost::zip_iterator con el list y counting_iterator .

Vous pouvez créer des enveloppes basées sur les intervalles pour être beaucoup plus concis :

template<class Range, class T, class BinaryOp>
void self_transform(Range& rng, T value, BinaryOp op)
{
    auto i = value;
    for (auto& elem : rng) {
        elem = op(elem, i);        
        ++i;
    }
}

qui peut être appelé de manière plus compacte comme :

self_transform(items, 0, [](auto const& elem, auto i) {
    return elem + std::to_string(i);    
});

Exemple en direct .

10voto

jrok Points 30472

Certains compilateurs proposent déjà des expressions avec des captures lambda qui seront dans la norme C++1y. Vous pourriez donc faire ceci :

#include <string>
#include <list>
#include <iostream>

int main()
{
    std::list<std::string> items {"a","b","c","d","e"};

    //                 interesting part here, initializes member i with 0, 
    //          ˇˇˇˇˇˇ type being deduced from initializer expression            
    auto func = [i = 0](auto& s) mutable { s+= std::to_string(i++); };
    for (auto& item : items) func(item);

    for (const auto& item : items) std::cout << item << ' ';
}

Sortie : a0 b1 c2 d3 e4

EDIT : Pour mémoire, je pense qu'avoir une petite variable d'index en dehors de la portée de la boucle est la meilleure solution (voir les autres réponses). Mais pour le plaisir, j'ai écrit un adaptateur d'itérateur (avec l'aide de Adaptateur d'itérateur Boost ) que vous pouvez utiliser pour lier une fonction membre index à tout itérateur :

#include <boost/iterator/iterator_adaptor.hpp>
#include <list>
#include <string>
#include <iostream>
#include <algorithm>

// boiler-plate

template<typename Iterator>
class indexed_iterator
: public boost::iterator_adaptor<indexed_iterator<Iterator>, Iterator>
{
public:
    indexed_iterator(Iterator it, int index_value = 0)
    : indexed_iterator::iterator_adaptor_(it)
    , i_(index_value)
    { }

private:
    int i_;

    friend class boost::iterator_core_access;
    void increment(){ ++i_; ++this->base_reference(); }

    /* TODO: decrement, etc. */

public:
    int index() const { return i_; }
};

template<typename Iterator>
indexed_iterator<Iterator> make_indexed_iterator(Iterator it, int index = 0)
{
    return indexed_iterator<Iterator>(it, index);
}

// usuable code

int main()
{
    std::list<std::string> items(10);

    auto first = make_indexed_iterator(items.begin());
    auto last  = make_indexed_iterator(items.end());
    while (first != last) {
        std::cout << first.index() << ' ';
        ++first;
    }
}

Sortie : 0 1 2 3 4 5 6 7 8 9

6voto

John Calsbeek Points 19381

Je finirais probablement avec quelque chose comme ça :

std::list<std::string> items = ...;

{
    int index = 0;
    auto it = items.begin();
    for (; it != items.end(); ++index, ++it)
    {
        *it += std::to_string(index);
    }
}

J'ai vu beaucoup plus d'utilisations de boucles for avec deux variables de boucle que d'utilisations d'itérateurs zippés ou de variables comptées de capture lambda. "Idiomatique" est un terme subjectif, mais je qualifierais cela d'idiomatique.

Le fait d'avoir une variable supplémentaire explicite permet évident que nous ne faisons que compter vers le haut. Ceci est important si vous décidez de faire quelque chose de non trivial dans la boucle. Par exemple, vous pouvez insérer ou supprimer un élément dans la liste et ajuster l'index en conséquence. Si vous utilisiez un adaptateur d'itérateur, il ne serait peut-être même pas évident que l'index qu'il fournit ne soit pas réellement l'index de l'élément dans le conteneur.


Alternativement, vous pourriez écrire une variante de std::for_each :

template <typename InputIt, typename BinaryFn>
BinaryFn for_each_index(InputIt first, InputIt last, BinaryFn fn)
{
    for (int i = 0; first != last; ++i, ++first)
    {
        fn(i, *first);
    }
    return fn;
}

qui au moins n'est pas obscurci. Alors tu peux faire ça :

std::list<std::string> items = ...;
for_each_index(items.begin(), items.end(), [](int i, std::string& s)
{
    s += std::to_string(i);
});

5voto

Paul Points 4552

En utilisant Gamme Boost. vous pouvez le faire :

std::list<std::string> items;
for (const auto & t : boost::combine(items, boost::irange(0, items.size()))) 
{
    std::string& item = boost::get<0>(t);
    int i = boost::get<1>(t);
    item += std::to_string(i);
}

2voto

hildensia Points 560

Il existe une petite bibliothèque appelée pythonique qui vous donne le enumerate() que vous connaissez peut-être de Python, en C++. Elle crée une liste de paires avec index et valeur. Vous pouvez ensuite itérer sur cette liste. Elle vous permet de faire ce qui suit (d'après leur documentation) :

#include <vector>
#include <iostream>
#include "pythonic/enumerate.h"
using namespace pythonic;

// ...

typedef std::vector<int> vec;

for (auto v : enumerate(vec {0, -1337, 42}))
{
    std::cout << v.first << " " << v.second << std::endl;
}

// ...

Ce qui vous donne comme résultat

$ ./enumerate
0 0
1 -1337
2 42
$

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