85 votes

Comment rendre une variable de boucle for constante à l'exception de l'instruction d'incrément ?

Considérez une boucle standard for :

for (int i = 0; i < 10; ++i) 
{
   // faire quelque chose avec i
}

Je veux empêcher la variable i d'être modifiée dans le corps de la boucle for.

Cependant, je ne peux pas déclarer i comme const car cela rendrait l'instruction d'incrémentation invalide. Existe-t-il un moyen de rendre i une variable const en dehors de l'instruction d'incrémentation ?

4 votes

Je crois qu'il n'y a aucun moyen de faire cela.

0 votes

Vous auriez besoin de cacher la variable du corps de la boucle, peut-être changer quelque chose comme while(i_copy = loop()) { }

0 votes

Vous pouvez faire une référence constante à celui-ci dans le corps de la boucle, par exemple const int& i_safe = i. Votre compilateur devrait éluder toute indirection.

123voto

cigien Points 11

À partir de c++20, vous pouvez utiliser ranges::views::iota comme ceci :

for (int const i : std::views::iota(0, 10))
{
   std::cout << i << " ";  // ok
   i = 42;                 // error
}

Voici une démonstration.


À partir de c++11, vous pouvez également utiliser la technique suivante, qui utilise un IIILE (immediately invoked inline lambda expression) :

int x = 0;
for (int i = 0; i < 10; ++i) [&,i] {
    std::cout << i << " ";  // ok, i is readable
    i = 42;                 // error, i is captured by non-mutable copy
    x++;                    // ok, x is captured by mutable reference
}();     // IIILE

Voici une démonstration.

Notez que [&,i] signifie que i est capturé par une copie non mutable, et tout le reste est capturé par référence mutable. Le (); à la fin de la boucle signifie simplement que le lambda est invoqué immédiatement.

0 votes

Presque appelle à une boucle spéciale puisque ce que cela offre est une alternative plus sûre à une construction très, très courante.

2 votes

@MichaelDorgan Eh bien, maintenant qu'il y a un support de bibliothèque pour cette fonctionnalité, cela ne vaudra pas la peine de l'ajouter en tant que fonctionnalité de langage principale.

1 votes

Juste, même si la plupart de mon vrai travail est encore sur C ou C++11 au maximum. J'étudie juste au cas où cela serait important pour moi à l'avenir...

46voto

Bitwize Points 555

Pour quiconque aime la réponse de std::views::iota de Cigien mais qui ne fonctionne pas en C++20 ou supérieur, il est plutôt simple de mettre en œuvre une version simplifiée et légère de std::views::iota compatible avec c++11 ou supérieur.

Tout ce dont vous avez besoin est :

  • Un type de "LegacyInputIterator" de base (quelque chose qui définit operator++ et operator*) qui enveloppe une valeur entière (par exemple, un int)
  • Une classe de type "range" qui a des méthodes begin() et end() qui renvoient les itérateurs mentionnés ci-dessus. Cela permettra de travailler dans des boucles for basées sur les plages

Une version simplifiée de ceci pourrait être :

#include 

// Il s'agit simplement d'une classe qui enveloppe un 'int' dans une abstraction d'itérateur
// Les comparaisons comparent la valeur sous-jacente, et 'operator++' incrémente simplement
// l'entier sous-jacent
class counting_iterator
{
public:
    // boilerplate d'itérateur de base
    using iterator_category = std::input_iterator_tag;
    using value_type = int;
    using reference  = int;
    using pointer    = int*;
    using difference_type = std::ptrdiff_t;

    // Constructeur / affectation
    constexpr explicit counting_iterator(int x) : m_value{x}{}
    constexpr counting_iterator(const counting_iterator&) = default;
    constexpr counting_iterator& operator=(const counting_iterator&) = default;

    // "Déférencement" (renvoie simplement la valeur sous-jacente)
    constexpr reference operator*() const { return m_value; }
    constexpr pointer operator->() const { return &m_value; }

    // Avancement de l'itérateur (incrémente simplement la valeur)
    constexpr counting_iterator& operator++() {
        m_value++;
        return (*this);
    }
    constexpr counting_iterator operator++(int) {
        const auto copy = (*this);
        ++(*this);
        return copy;
    }

    // Comparaison
    constexpr bool operator==(const counting_iterator& other) const noexcept {
        return m_value == other.m_value;
    }
    constexpr bool operator!=(const counting_iterator& other) const noexcept {
        return m_value != other.m_value;
    }
private:
    int m_value;
};

// Juste un type conteneur qui définit 'begin' et 'end' pour
// l'itération basée sur les plages. Cela détient le premier et le dernier élément
// (début et fin de la plage)
// L'itérateur de début est créé à partir de la première valeur, et l'itérateur de fin est créé à partir de la seconde valeur.
struct iota_range
{
    int first;
    int last;
    constexpr counting_iterator begin() const { return counting_iterator{first}; }
    constexpr counting_iterator end() const { return counting_iterator{last}; }
};

// Une fonction d'aide simple pour renvoyer la plage
// Cette fonction n'est pas strictement nécessaire, vous pourriez simplement construire
// directement l 'iota_range'
constexpr iota_range iota(int first, int last)
{
    return iota_range{first, last};
}

J'ai défini le code ci-dessus avec constexpr là où c'est supporté, mais pour les versions antérieures de C++ comme C++11/14, vous devrez peut-être supprimer constexpr là où ce n'est pas autorisé dans ces versions.

Le code ci-dessus permet d'exécuter le code suivant en pré-C++20 :

for (int const i : iota(0, 10))
{
   std::cout << i << " ";  // ok
   i = 42;                 // erreur
}

Cela générera le même assemblage que la solution C++20 std::views::iota et la solution classique avec une boucle for lorsqu'elle est optimisée.

Ceci fonctionne avec n'importe quel compilateur conforme à C++11 (par exemple, des compilateurs comme gcc-4.9.4) et produit toujours un assemblage pratiquement identique à une solution de boucle for de base.

Remarque : La fonction d'aide iota est juste pour une parité de fonctionnalités avec la solution C++20 std::views::iota; mais en réalité, vous pourriez aussi construire directement un iota_range{...} au lieu d'appeler iota(...). Le premier présente simplement un chemin de mise à niveau facile si un utilisateur souhaite passer à C++20 à l'avenir.

3 votes

Il nécessite un peu de code qui se répète, mais ce n'est pas vraiment compliqué en termes de ce qu'il fait. En fait, c'est simplement un modèle itératif de base, mais qui enveloppe un int, puis crée une classe "range" pour renvoyer le début / fin

1 votes

Ce n'est pas super important, mais j'ai également ajouté une solution en c++11 que personne d'autre n'a postée, donc vous voudrez peut-être reformuler légèrement la première ligne de votre réponse :)

0 votes

Je ne suis pas sûr qui a voté négativement, mais j'apprécierais des commentaires si vous pensez que ma réponse n'est pas satisfaisante afin que je puisse m'améliorer. Voter négativement est une bonne façon de montrer que vous pensez qu'une réponse ne répond pas adéquatement à la question, mais dans ce cas, il n'y a pas de critiques existantes ou de défauts évidents dans la réponse que je pourrais améliorer.

28voto

Artelius Points 25772

La version KISS...

for (int _i = 0; _i < 10; ++_i) {
    const int i = _i;

    // utiliser i ici
}

Si votre cas d'utilisation consiste simplement à empêcher la modification accidentelle de l'index de boucle, alors cela devrait rendre un tel bogue évident. (Si vous souhaitez empêcher toute modification intentionnelle, eh bien, bonne chance...)

11 votes

Je pense que vous enseignez la mauvaise leçon en utilisant des identifiants magiques qui commencent par _. Et un peu d'explication (par exemple, la portée) serait utile. Sinon, oui, c'est joliment KISSy.

14 votes

Appeler la variable "cachée" i_ serait plus conforme.

10 votes

Je ne suis pas sûr que cela réponde à la question. La variable de boucle est _i qui est toujours modifiable dans la boucle.

13voto

Al rl Points 314

Ne pourriez-vous pas simplement déplacer une partie ou la totalité du contenu de votre boucle for dans une fonction qui accepte i comme constante?

Ce n'est pas aussi optimal que certaines des solutions proposées, mais si c'est possible, c'est assez simple à faire.

Édition : Juste un exemple car j'ai tendance à être peu clair.

for (int i = 0; i < 10; ++i) 
{
   looper( i );
}

void looper ( const int v )
{
    // faites votre traitement ici
}

12voto

JeJo Points 12135

Si vous n'avez pas accès à c++20, un relooking typique en utilisant une fonction

#include 
#include  // std::iota

std::vector makeRange(const int start, const int end) noexcept
{
   std::vector vecRange(end - start);
   std::iota(vecRange.begin(), vecRange.end(), start);
   return vecRange;
}

maintenant vous pourriez

for (const int i : makeRange(0, 10))
{
   std::cout << i << " ";  // ok
   //i = 100;              // error
}

(Voir une démo)


Mise à jour: Inspiré par le commentaire de @Human-Compiler, je me demandais si les réponses données avaient des différences en termes de performances. Il s'avère que, excepté pour cette approche, toutes les autres approches ont étonnamment la même performance (pour l'intervalle [0, 10)). L'approche avec std::vector est la pire.

Description de l'image ici

(Voir Quick-Bench en ligne)

5 votes

Bien que cela fonctionne pour pre-c++20, cela représente un assez grand surcoût car cela nécessite l'utilisation de vector. Si la plage est très grande, cela pourrait être mauvais.

1 votes

@Human-Compiler: Un std::vector est assez terrible sur une échelle relative si la plage est petite, et pourrait être très mauvais si cela était censé être une petite boucle interne qui tournerait de nombreuses fois. Certains compilateurs (comme clang avec libc++, mais pas libstdc++) peuvent optimiser la nouvelle suppression d'une allocation qui n'échappe pas à la fonction, sinon cela pourrait facilement être la différence entre une petite boucle entièrement déroulée vs. un appel à nouveau + supprimer, et peut-être même stocker réellement dans cette mémoire.

0 votes

IMO, le léger avantage de const i n'en vaut tout simplement pas la peine dans la plupart des cas, sans les moyens de C++20 qui le rendent bon marché. Surtout avec des plages de variables de temps d'exécution qui rendent moins probable que le compilateur optimise tout.

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