如何在C++中迭代非const变量?

41
#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 绑定。

是否有一种简单的方式使类似的构造工作正常,即迭代不同变量中的值,比如这里的 ab


@ruohola,这并不是特别关于使用initializer_list(就像在这个错误的代码中一样),而是如果在C++中有任何其他简单的方法可以迭代分别存储在不同变量中的对象。 - tmlen
也许完全遵循现代C++惯例并不是很有用,例如避免使用原始指针和使用std::reference_wrapper,因为这只会使代码更加复杂;毕竟C++本身也远非完美。 - tmlen
好的,我明白了 :) 只是想说因为所有受欢迎的答案都用 std::initalizer_list 解决了问题,而且这也被证明是解决这个问题最干净的方法,所以你的问题意图可能会偏向于 initializer_list,这样搜索这种解决方案的人就可以更容易地找到答案。 - ruohola
4个回答

51

它无法工作是因为在{a,b}中,您正在复制ab。一个可能的解决方案是使循环变量成为指针,取ab的地址:

#include <initializer_list>

struct Obj {
    int i;
};

Obj a, b;

int main() {
    for(auto obj : {&a, &b}) {
        obj->i = 123;   
    }
}

查看实时演示

注:通常最好使用auto,因为它可以避免隐式转换


这里使用 for(auto& obj: {&a,&b}) 可以吗?还是会进行转换? - Demolishun
1
使用auto& obj : {&a, &b},变量obj是一个指向指针的const引用。只有在想要修改指针本身时才有必要使用指向引用的指针,参见这里。在这种情况下,你将无法这样做,因为obj是对临时指针&a&bconst引用。尽管如此,在实践中,你不会注意到编译后代码的任何区别,请查看这里 - francesco

45

那样不起作用的原因是 std::initializer_list 的底层元素是从 ab 复制而来,且类型为 const Obj,因此您实际上是在尝试将常量值绑定到可变引用上。

可以尝试使用以下方式进行修复:

for (auto obj : {a, b}) {
    obj.i = 123;
}

但是很快你会注意到,对象 ab 中的 i 的实际值并没有改变。原因是在这里使用 auto 时,循环变量 obj 的类型将变为 Obj,因此您只是在循环中复制了 ab

实际上应该修复的方法是,您可以使用 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';
}

输出:

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在他的答案中所做的那样,但我有一个尽可能避免使用原始指针的习惯,并且在这种情况下,我认为使用引用可以使代码更清晰、更干净。


4
虽然这个解决方案看起来可能略微复杂,但我认为它比被接受的方案大大优越,因为被接受的方案不仅改变了指针的语义(如果期望对象,这是“奇怪”的),而且由于使用了更加普遍更好的 auto,没有使用引用符号却实际上复制了指针,这对用户来说并不明显。 auto 关键字真是一个肮脏的叛徒。复制指针确实“大多是无害的”,但这只是巧合。始终使用实际引用(具有引用/对象语义)就更简洁。 - Damon

5

如果想要复制ab,则可以使用临时数组而不是初始化列表:

#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 只有移动构造函数,这也能正常工作。


1
如果复制是所需的行为,您不需要一个临时数组,只需放弃引用:for(auto obj : {a, b}) { obj.i = 123; } - zett42
是的,我现在添加了一条说明,解释为什么我仍然认为它很相关。 - Jacob Manaker

-3

虽然有点丑,但自从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));
}

即使对象的类型不完全相同,通过将f设置为重载集合,它也可以正常工作。您还可以将f设置为(可能是重载的)本地lambda函数。灵感链接

std::tie避免了原始代码中的问题,因为它生成了一个引用的元组。不幸的是,对于所有成员类型相同的元组,std::begin没有定义(那样会很好!),所以我们不能使用基于范围的for循环。但是std::apply是更高层次的泛化。


你可以使用std make array。 - sudo rm -rf slash

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接