Iterar sobre variables no constantes en C++
initializer-list (4)
La razón por la que eso no funciona es que los elementos subyacentes de
std::initializer_list
se copian de
a
y
b
, y son de tipo
const Obj
, por lo que esencialmente está tratando de vincular un valor constante a una referencia mutable.
Uno podría intentar solucionar esto usando:
for (auto obj : {a, b}) {
obj.i = 123;
}
pero pronto notaría que los valores reales de
i
en los objetos
b
no cambiaron.
La razón es que cuando se usa
auto
aquí, el tipo de la variable de bucle
obj
se convertirá en
Obj
, por lo que simplemente está haciendo un bucle sobre las copias de
b
.
La forma real de solucionar esto es que puede usar
std::ref
(definido en el encabezado
<functional>
), para que los elementos en la lista de inicializadores sean de tipo
std::reference_wrapper<Obj>
.
Eso es implícitamente convertible a
Obj&
, por lo que puede mantenerlo como el tipo de la variable de bucle:
#include <functional>
#include <initializer_list>
#include <iostream>
struct Obj {
int i;
};
Obj a, b;
int main()
{
for (Obj& obj : {std::ref(a), std::ref(b)}) {
obj.i = 123;
}
std::cout << a.i << ''/n'';
std::cout << b.i << ''/n'';
}
Salida:
123 123
Una forma alternativa de hacer lo anterior sería hacer que el bucle use
const auto&
y
std::reference_wrapper<T>::get
.
Podemos usar una referencia constante aquí, porque
reference_wrapper
no se altera, solo el valor que envuelve sí:
for (const auto& obj : {std::ref(a), std::ref(b)}) {
obj.get().i = 123;
}
pero creo que de esta manera usar
auto
y
.get()
es bastante engorroso y probablemente no sea preferible en la mayoría de los casos.
Puede parecer más simple hacer esto usando punteros sin procesar en el bucle como @francesco hizo en su respuesta, pero tengo la costumbre de evitar los punteros sin procesar tanto como sea posible, y en este caso solo creo que usar referencias hace El código más claro y más limpio.
#include <initializer_list>
struct Obj {
int i;
};
Obj a, b;
int main() {
for(Obj& obj : {a, b}) {
obj.i = 123;
}
}
Este código no se compila porque los valores de
initializer_list
{a, b}
se toman como
const Obj&
, y no se pueden vincular a la referencia no const
obj
.
¿Hay una manera simple de hacer que una construcción similar funcione, es decir, iterar sobre valores que están en diferentes variables, como
a
y
b
aquí?
No funciona porque en
{a,b}
estás haciendo una copia de
a
y
b
.
Una posible solución sería hacer que la variable de bucle sea un puntero, tomando las direcciones de
a
y
b
:
#include <initializer_list>
struct Obj {
int i;
};
Obj a, b;
int main() {
for(auto obj : {&a, &b}) {
obj->i = 123;
}
}
Míralo en live
Nota: genéricamente es mejor usar
auto
, ya que podría
evitar conversiones implícitas silenciosas
Si copiar
b
es el comportamiento deseado, puede usar una matriz temporal en lugar de una lista de inicializador:
#include <initializer_list>
struct Obj {
int i;
} a, b;
int main() {
typedef Obj obj_arr[];
for(auto &obj : obj_arr{a, b}) {
obj.i = 123;
}
}
Un poco feo pero puedes hacer esto, ya que C ++ 17:
#define APPLY_TUPLE(func, t) std::apply( [](auto&&... e) { ( func(e), ...); }, (t))
static void f(Obj& x)
{
x.i = 123;
}
int main()
{
APPLY_TUPLE(f, std::tie(a, b));
}
Incluso funcionaría si los objetos no son todos del mismo tipo, haciendo
f
un conjunto de sobrecarga.
También puede hacer que
f
sea una lambda local (posiblemente sobrecargada).
Enlace a la inspiración
.
El
std::tie
evita el problema en el código original porque genera una tupla de referencias.
Desafortunadamente,
std::begin
no está definido para tuplas donde todos los miembros son del mismo tipo (¡eso sería bueno!), Por lo que no podemos usar el bucle for basado en rangos.
Pero
std::apply
es la siguiente capa de generalización.