Итерация по неконстантным переменным в С++

#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 - это следующий уровень обобщения.