Итерация по неконстантным переменным в С++
#include <initializer_list>
struct Obj {
int i;
};
Obj a, b;
int main() {
for(Obj& obj : {a, b}) {
obj.i = 123;
}
}
Этот код не компилируется, поскольку значения из initializer_list
{a, b}
принимаются как const Obj&
и не могут быть привязаны к неконстантной ссылке obj
.
Есть ли простой способ заставить подобную конструкцию работать, то есть перебирать значения, которые находятся в разных переменных, например, здесь a
и b
.
Ответы
Ответ 1
Это не работает, потому что в {a,b}
вы делаете копию a
и b
. Одним из возможных решений было бы сделать переменную цикла указателем, взяв адреса a
и b
:
#include <initializer_list>
struct Obj {
int i;
};
Obj a, b;
int main() {
for(auto obj : {&a, &b}) {
obj->i = 123;
}
}
Посмотри в прямом эфире
Примечание: обычно лучше использовать auto
, так как это может избежать неявных неявных преобразований
Ответ 2
Причина, по которой это не работает, заключается в том, что базовые элементы std::initializer_list
скопированы из a
и b
и имеют тип const Obj
, поэтому вы, по сути, пытаетесь связать постоянное значение для изменяемой ссылки.
Можно попытаться исправить это с помощью:
for (auto obj : {a, b}) {
obj.i = 123;
}
но потом вскоре заметит, что фактические значения i
в объектах a
и b
не изменились. Причина в том, что при использовании auto
здесь тип переменной цикла obj
станет Obj
, поэтому вы просто зацикливаетесь на копиях a
и b
.
Фактически это должно быть исправлено: вы можете использовать std::ref
(определенный в заголовке <functional>
), чтобы элементы в списке инициализатора имели тип std::reference_wrapper<Obj>
. Это косвенно конвертируется в Obj&
, так что вы можете оставить это как тип переменной цикла:
#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';
}
Output:
123
123
Альтернативным способом сделать это может быть использование цикла const auto&
и std::reference_wrapper<T>::get
. Мы можем использовать постоянную ссылку здесь, потому что reference_wrapper
не изменяется, только значение, которое он переносит:
for (const auto& obj : {std::ref(a), std::ref(b)}) {
obj.get().i = 123;
}
но я думаю, что поскольку использование auto
здесь вызывает использование .get()
, это довольно громоздко, и первый способ является предпочтительным способом решения этой проблемы.
Может показаться, что сделать это проще, используя необработанные указатели в цикле, как @francesco в своем ответе, но у меня есть привычка избегать необработанных указателей, насколько это возможно, и в этом случае я просто считаю, что использование ссылок делает код яснее и чище.
Ответ 3
Если копирование a
и b
является желательным поведением, вы можете использовать временный массив, а не список инициализатора:
#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;
}
}
Это работает, даже если Obj
имеет только конструктор перемещения.
Ответ 4
Немного некрасиво, но вы можете сделать это, начиная с С++ 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));
}
Это даже сработало бы, если бы объекты не были одного типа, сделав f
набором перегрузки. Вы также можете иметь f
локальную лямбду (возможно, перегруженную). Ссылка на вдохновение.
std::tie
позволяет избежать проблемы в исходном коде, потому что он генерирует кортеж ссылок. К сожалению, std::begin
не определен для кортежей, в которых все члены имеют одинаковый тип (что было бы неплохо!), Поэтому мы не можем использовать цикл for на основе ранжирования. Но std::apply
- это следующий уровень обобщения.