¿Cómo hacer una variable de ciclo for const con la excepción de la declaración de incremento?

82

Considere un estándar para bucle:

for (int i = 0; i < 10; ++i) 
{
   // do something with i
}

Quiero evitar que la variable ise modifique en el cuerpo del forbucle.

Sin embargo, no puedo declararlo iya constque esto invalida la declaración de incremento. ¿Hay alguna forma de hacer iuna constvariable fuera de la declaración de incremento?

jhourback
fuente
4
Creo que no hay forma de hacer esto
Itay
27
Suena como una solución en busca de un problema.
Pete Becker
14
Convierta el cuerpo de su bucle for en una función con un const int iargumento. La mutabilidad del índice solo se expone donde es necesario y puede usar la inlinepalabra clave para que no tenga ningún efecto en la salida compilada.
Monty Thibault
4
¿Qué (o más bien, quién) podría cambiar el valor del índice además de ... usted? ¿Desconfías de ti mismo? ¿Quizás un compañero de trabajo? Estoy de acuerdo con @PeteBecker.
Nivel Z4
4
@ Z4-tier Sí, por supuesto que desconfío de mí mismo. Sé que cometo errores. Todo buen programador lo sabe. Es por eso que tenemos cosas como constpara empezar.
Konrad Rudolph

Respuestas:

119

Desde c ++ 20, puede usar rangos :: vistas :: iota así:

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

Aquí tienes una demostración .


Desde c ++ 11, también puede usar la siguiente técnica, que usa un IIILE (expresión lambda en línea inmediatamente invocada):

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

Aquí tienes una demostración .

Tenga en cuenta que eso [&,i]significa que ise captura mediante una copia no mutable, y todo lo demás se captura mediante una referencia mutable. El ();al final del ciclo simplemente significa que el lambda se invoca inmediatamente.

cigien
fuente
Casi requiere una construcción especial de bucle for, ya que lo que ofrece es una alternativa más segura a una construcción muy, muy común.
Michael Dorgan
2
@MichaelDorgan Bueno, ahora que hay soporte de biblioteca para esta característica, no valdrá la pena agregarla como una característica principal del lenguaje.
cigien
1
Justo, aunque casi todo mi trabajo real sigue siendo C o C ++ 11 como máximo. Estudio por si acaso me importa en el futuro ...
Michael Dorgan
9
El truco de C ++ 11 que agregó con la lambda es bueno, pero no sería práctico en la mayoría de los lugares de trabajo en los que he estado. El análisis estático se quejaría de la &captura generalizada , lo que obligaría a capturar cada referencia explícitamente, lo que hace que esto sea bastante incómodo. También sospecho que esto podría conducir a errores fáciles en los que un autor se olvida (), haciendo que el código nunca se invoque. Esto es lo suficientemente pequeño como para perderse en la revisión del código.
Human-Compiler
1
@cigien Las herramientas de análisis estático como SonarQube y cppcheck marcan capturas generales como [&]porque entran en conflicto con los estándares de codificación como AUTOSAR (Regla A5-1-2), HIC ++ y creo que también MISRA (no estoy seguro). No es que no sea correcto; es que las organizaciones prohíben este tipo de código para cumplir con los estándares. En cuanto a (), la versión más reciente de gcc no marca esto incluso con -Wextra. Sigo pensando que el enfoque es ordenado; simplemente no funciona para muchas organizaciones.
Human-Compiler
44

Para cualquiera a quien le guste la std::views::iotarespuesta de Cigien pero no esté trabajando en C ++ 20 o superior, es bastante sencillo implementar una versión simplificada y liviana de std::views::iotacompatible o superior.

Todo lo que requiere es:

  • Un tipo básico " LegacyInputIterator " (algo que define operator++y operator*) que envuelve un valor integral (por ejemplo, un int)
  • Alguna clase similar a "rango" que tiene begin()y end()que devuelve los iteradores anteriores. Esto le permitirá trabajar en forbucles basados ​​en rangos.

Una versión simplificada de esto podría ser:

#include <iterator>

// This is just a class that wraps an 'int' in an iterator abstraction
// Comparisons compare the underlying value, and 'operator++' just
// increments the underlying int
class counting_iterator
{
public:
    // basic iterator boilerplate
    using iterator_category = std::input_iterator_tag;
    using value_type = int;
    using reference  = int;
    using pointer    = int*;
    using difference_type = std::ptrdiff_t;

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

    // "Dereference" (just returns the underlying value)
    constexpr reference operator*() const { return m_value; }
    constexpr pointer operator->() const { return &m_value; }

    // Advancing iterator (just increments the value)
    constexpr counting_iterator& operator++() {
        m_value++;
        return (*this);
    }
    constexpr counting_iterator operator++(int) {
        const auto copy = (*this);
        ++(*this);
        return copy;
    }

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

// Just a holder type that defines 'begin' and 'end' for
// range-based iteration. This holds the first and last element
// (start and end of the range)
// The begin iterator is made from the first value, and the
// end iterator is made from the second value.
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}; }
};

// A simple helper function to return the range
// This function isn't strictly necessary, you could just construct
// the 'iota_range' directly
constexpr iota_range iota(int first, int last)
{
    return iota_range{first, last};
}

He definido lo anterior con constexprdónde es compatible, pero para versiones anteriores de C ++ como C ++ 11/14, es posible que deba eliminarlo constexprdonde no es legal en esas versiones para hacerlo.

La plantilla anterior permite que el siguiente código funcione en versiones anteriores a C ++ 20:

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

Que generará el mismo ensamblado que la std::views::iotasolución C ++ 20 y la forsolución clásica -loop cuando se optimice.

Esto funciona con cualquier compilador compatible con C ++ 11 (por ejemplo, compiladores como gcc-4.9.4) y aún produce un ensamblaje casi idéntico a un forequivalente básico de bucle.

Nota: La iotafunción auxiliar es solo para la paridad de características con la std::views::iotasolución C ++ 20 ; pero de manera realista, también podría construir directamente un en iota_range{...}lugar de llamar iota(...). El primero solo presenta una ruta de actualización fácil si un usuario desea cambiar a C ++ 20 en el futuro.

Compilador humano
fuente
3
Requiere un poco de repetición, pero en realidad no es tan complicado en términos de lo que está haciendo. En realidad, es solo un patrón de iterador básico, pero envuelve un inty luego crea una clase de "rango" para devolver el comienzo / fin
Human-Compiler
1
No es muy importante, pero también agregué una solución c ++ 11 que nadie más publicó, por lo que es posible que desee reformular ligeramente la primera línea de su respuesta :)
cigien
No estoy seguro de quién votó negativamente, pero agradecería algunos comentarios si cree que mi respuesta no es satisfactoria para que pueda mejorarla. Votar en contra es una excelente manera de mostrar que sientes que una respuesta no aborda adecuadamente la pregunta, pero en este caso no existen críticas o fallas obvias en la respuesta que puedan mejorar.
Human-Compiler
@ Human-Compiler También obtuve un DV al mismo tiempo, y tampoco comentaron por qué :( Supongo que a alguien no le gustan las abstracciones de rango. No me preocuparía por eso.
cigien
1
"asamblea" es un sustantivo de masas como "equipaje" o "agua". La redacción normal sería "se compilará en el mismo ensamblado que C ++ 20 ...". La salida asm del compilador para una sola función no es un ensamblado singular, es "ensamblador" (una secuencia de instrucciones en lenguaje ensamblador).
Peter Cordes
29

La versión KISS ...

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

    // use i here
}

Si su caso de uso es solo para evitar la modificación accidental del índice de bucle, esto debería hacer que el error sea obvio. (Si quieres evitar modificaciones intencionales , bueno, buena suerte ...)

Artelius
fuente
11
Creo que enseñas la lección equivocada al usar identificadores mágicos que comienzan con _. Y un poco de explicación (por ejemplo, alcance) sería útil. De lo contrario, sí, muy bien KISSy.
Yunnosch
14
Llamar a la variable "oculta" i_sería más compatible.
Yirkha
9
No estoy seguro de cómo esto responde a la pregunta. La variable de bucle es la _ique todavía se puede modificar en el bucle.
cigien
4
@cigien: En mi opinión, esta solución parcial vale la pena ir sin C ++ 20 std::views::iotade una manera totalmente a prueba de balas. El texto de la respuesta explica sus limitaciones y cómo intenta responder la pregunta. Un montón de C ++ 11 demasiado complicado hace que la cura sea peor que la enfermedad en términos de fácil lectura y mantenimiento, en mi opinión. Esto sigue siendo muy fácil de leer para todos los que conocen C ++, y parece razonable como un idioma. (Pero debe evitar los nombres de subrayado inicial.)
Peter Cordes
5
@Yunnosch solamente _Uppercasey los double__underscoreidentificadores están reservados. _lowercaselos identificadores solo están reservados en el ámbito global.
Roman Odaisky
13

Si no tiene acceso a , cambio de imagen típico con una función

#include <vector>
#include <numeric> // std::iota

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

ahora puedes

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

( Ver una demostración )


Actualización : inspirado en el comentario de @ Human-Compiler , me preguntaba si las respuestas dadas tienen alguna diferencia en el caso del rendimiento. Resulta que, a excepción de este enfoque, todos los demás enfoques sorprendentemente tienen el mismo rendimiento (para el rango [0, 10)). El std::vectorenfoque es el peor.

ingrese la descripción de la imagen aquí

( Consulte Quick-Bench en línea )

JeJo
fuente
4
Aunque esto funciona para pre-c ++ 20, esto tiene una gran cantidad de sobrecarga ya que requiere usar vector. Si el rango es muy grande, esto podría ser malo.
Human-Compiler
@ Human-Compiler: A std::vectores bastante terrible en una escala relativa si el rango también es pequeño, y podría ser muy malo si se suponía que era un pequeño bucle interno que se ejecutaba muchas veces. Algunos compiladores (como clang con libc ++, pero no libstdc ++) pueden optimizar nuevo / eliminar de una asignación que no escapa a la función, pero de lo contrario, esta podría ser fácilmente la diferencia entre un pequeño bucle completamente desenrollado frente a una llamada a new+ delete, y tal vez almacenarlo en esa memoria.
Peter Cordes
En mi opinión, el beneficio menor de const isimplemente no vale la pena la sobrecarga en la mayoría de los casos, sin C ++ 20 formas que lo hacen barato. Especialmente con rangos de variables en tiempo de ejecución que hacen que sea menos probable que el compilador optimice todo.
Peter Cordes
13

¿No podría simplemente mover parte o todo el contenido de su bucle for en una función que acepte i como una constante?

Es menos óptimo que algunas soluciones propuestas, pero si es posible, esto es bastante simple de hacer.

Editar: Solo un ejemplo, ya que tiendo a ser poco claro.

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

void looper ( const int v )
{
    // do your thing here
}
Al rl
fuente
10

Y aquí hay una versión de C ++ 11:

for (int const i : {0,1,2,3,4,5,6,7,8,9,10})
{
    std::cout << i << " ";
    // i = 42; // error
}

Aquí está la demostración en vivo

Vlad Feinstein
fuente
6
Esto no se escala si el número máximo se decide mediante un valor de tiempo de ejecución.
Human-Compiler
12
@ Human-Compiler Simplemente amplíe la lista al valor deseado y vuelva a compilar todo su programa dinámicamente;)
Monty Thibault
5
No mencionaste cuál es el caso de {..}. Necesita incluir algo para activar esta función. Por ejemplo, su código se romperá si no agrega los encabezados adecuados: godbolt.org/z/esbhra . ¡Remitir <iostream>para otros encabezados es una mala idea!
JeJo
6
#include <cstdio>
  
#define protect(var) \
  auto &var ## _ref = var; \
  const auto &var = var ## _ref

int main()
{
  for (int i = 0; i < 10; ++i) 
  {
    {
      protect(i);
      // do something with i
      //
      printf("%d\n", i);
      i = 42; // error!! remove this and it compiles.
    }
  }
}

Nota: necesitamos anidar el alcance debido a una asombrosa estupidez en el lenguaje: la variable declarada en el for(...)encabezado se considera que está en el mismo nivel de anidación que las variables declaradas en la {...}declaración compuesta. Esto significa que, por ejemplo:

for (int i = ...)
{
  int i = 42; // error: i redeclared in same scope
}

¿Qué? ¿No acabamos de abrir un tirante rizado? Además, es inconsistente:

void fun(int i)
{
  int i = 42; // OK
}
Kaz
fuente
1
Esta es fácilmente la mejor respuesta. Aprovechar el 'sombreado de variables' de C ++ para hacer que el identificador se resuelva en una variable const ref que hace referencia a la variable de paso original, es una solución elegante. O al menos, el más elegante disponible.
Max Barraclough
4

Un enfoque simple aún no mencionado aquí que funciona en cualquier versión de C ++ es crear un contenedor funcional alrededor de un rango, similar a lo que std::for_eachocurre con los iteradores. El usuario es responsable de pasar un argumento funcional como una devolución de llamada que se invocará en cada iteración.

Por ejemplo:

// A struct that holds the start and end value of the range
struct numeric_range
{
    int start;
    int end;

    // A simple function that wraps the 'for loop' and calls the function back
    template <typename Fn>
    void for_each(const Fn& fn) const {
        for (auto i = start; i < end; ++i) {
            const auto& const_i = i;
            fn(const_i);
        }
    }
};

Dónde sería el uso:

numeric_range{0, 10}.for_each([](const auto& i){
   std::cout << i << " ";  // ok
   //i = 100;              // error
});

Cualquier cosa anterior a C ++ 11 se atascaría al pasar un puntero de función con nombre fuerte en for_each(similar a std::for_each), pero aún funciona.

Aquí hay una demostración


Aunque esto puede no ser idiomático para forbucles en C ++ , este enfoque es bastante común en otros lenguajes. Los envoltorios funcionales son realmente elegantes por su capacidad de composición en declaraciones complejas y pueden ser muy ergonómicos para su uso.

Este código también es fácil de escribir, comprender y mantener.

Compilador humano
fuente
Una limitación a tener en cuenta con este enfoque es que algunas organizaciones prohíben las capturas predeterminadas en lambdas (p. Ej., [&]O [=]) para cumplir con ciertos estándares de seguridad, lo que puede inflar la lambda y cada miembro debe capturarse manualmente. No todas las organizaciones hacen esto, por lo que solo lo menciono como un comentario y no en la respuesta.
Human-Compiler
0
template<class T = int, class F>
void while_less(T n, F f, T start = 0){
    for(; start < n; ++start)
        f(start);
}

int main()
{
    int s = 0;
    
    while_less(10, [&](auto i){
        s += i;
    });
    
    assert(s == 45);
}

tal vez llamarlo for_i

Sin gastos generales https://godbolt.org/z/e7asGj

Hrisip
fuente