基于 for 循环的无辜范围不起作用

Innocent range based for loop not working

提问人:darune 提问时间:11/6/2019 最后编辑:L. F.darune 更新时间:11/6/2019 访问量:689

问:

以下内容编译:

#include <iostream>

int main()
{
    int a{},b{},c{},d{};

    for (auto& s : {a, b, c, d}) {
        s = 1;
    }
    std::cout << a << std::endl;
    return 0;
}

在 godbolt 上试试

编译器错误是:error: assignment of read-only reference 's'

现在,在我的实际案例中,列表由类上的成员变量组成。

现在,这不起作用,因为表达式变成了实际上复制 a、b、c 和 d 的表达式 - 因此也不允许修改。initializer_list<int>

我的问题是双重的:

不允许以这种方式编写基于范围的for循环背后有什么动机吗? 例如,也许裸括号表达式可能有一个特殊情况。

修复此类循环的语法简洁方法是什么?

最好是这样的内容:

for (auto& s : something(a, b, c, d)) {
    s = 1;
}

我不认为指针间接是一个好的解决方案(即{&a,&b,&c,&d}) - 当迭代器被取消引用时,任何解决方案都应该直接提供元素引用

C++ for-loop C++17 初始值设定项列表

评论

1赞 Some programmer dude 11/6/2019
一个简单的解决方法(我自己不会真正使用)是创建一个指针列表:.{ &a, &b, &c, &d }
2赞 Jarod42 11/6/2019
initializer_list主要是对数组的看法。const
0赞 Some programmer dude 11/6/2019
我可能会做的是逐个显式初始化变量。它不会写得更多,它清晰而明确,并且它做了预期的工作。:)
3赞 Jarod42 11/6/2019
如果你不想要,你也不会想要:{ &a, &b, &c, &d }for (auto& s : std::initializer_list<std::reference_wrapper<int>>{a, b, c, d}) { s.get() = 1; }
0赞 Nicol Bolas 11/6/2019
“为什么这不能工作”的问题与“我能做些什么来使这样的事情工作”是一个非常不同的问题。

答:

4赞 mhhollomon 11/6/2019 #1

范围并不像人们想要的那么神奇。最后,必须有一个对象,编译器可以对其生成对成员函数或自由函数的调用,并且 .begin()end()

您可能最接近的是:

#include <iostream>

int main()
{
    int a{},b{},c{},d{};

    for (auto s : {&a, &b, &c, &d} ) {
        *s = 1;
    }
    std::cout << a << "\n";
    return 0;
}

评论

1赞 Jarod42 11/6/2019
您可以删除 .std::vector<int*>
0赞 darune 11/6/2019
@mhhollomon我明确表示我对指针间接解决方案不感兴趣。
1赞 L. F. 11/6/2019
它应该是 或 ,而不是 。auto sauto* sauto& s
0赞 mhhollomon 11/6/2019
@darune - 我很高兴有人给出不同的答案。目前尚不清楚现行标准是否存在这样的答案。
0赞 mhhollomon 11/6/2019
@L.F. - 同意。
4赞 101010 11/6/2019 #2

根据标准 §11.6.4 List-initialization/p5 [dcl.init.list] [Emphasis Mine]:

类型为“std::initializer_list”的对象由 初始值设定项列表,就好像实现生成并具体化一样 (7.4) 类型为“N const E 数组”的 prvalue,其中 N 是 元素。该数组的每个元素都是 使用初始值设定项的相应元素进行复制初始化 列表,并将 std::initializer_list 对象构造为引用 添加到该数组。[ 注意:选择了构造函数或转换函数 因为副本应在 初始值设定项列表。— 尾注 ] 如果需要缩小转换 要初始化任何元素,程序格式不正确。

因此,你的编译器在合理地抱怨(即,在范围的for循环中扣除到和你不能赋值到)。auto &sint const& ss

您可以通过引入容器而不是带有“std::reference_wrapper”的初始值设定项列表(例如,“std::vector”)来缓解此问题:

#include <iostream>
#include <vector>
#include <functional>

int main()
{
    int a{},b{},c{},d{};

    for (auto& s : std::vector<std::reference_wrapper<int>>{a, b, c, d}) {
        s.get()= 1;
    }
    std::cout << a << std::endl;
    return 0;
}

现场演示

评论

0赞 101010 11/6/2019
@Jarod42 对不起,修改了。
0赞 darune 11/6/2019
您的解决方案不符合我对良好解决方案的标准 - 如果我对此感到满意,我一开始就不会问:)
0赞 darune 11/6/2019
此外,您的报价不会试图回答我的问题
1赞 Peter 11/6/2019
@darune - 实际上,报价是你不起作用的原因。至于为什么标准有那个条款......你必须问问标准化委员会的成员。像许多这样的事情一样,推理可以是介于“我们认为您的特定案例不够有用以打扰”到“标准的太多其他部分必须更改以支持您的案例,我们推迟了对所有这些的考虑,直到我们制定未来的标准”。for (auto& s : {a, b, c, d})
0赞 Toby Speight 11/6/2019
你不能只用吗?std::array<std::reference_wrapper>>
-1赞 rafix07 11/6/2019 #3

您可以创建用于存储引用的包装类,该类将具有赋值运算符来更新此值:

template<class T>
struct Wrapper {
    T& ref;

    Wrapper(T& ref)
    : ref(ref){}

    template<class U>
    void operator=(U u) {
        ref = u;
    }
};

template<class...T>
auto sth(T&...t) {
    return std::array< Wrapper<std::common_type_t<T...> > ,sizeof...(t) >{Wrapper(t)...};
};

int main(){
    int a{},b{},c{},d{};

    for (auto s : sth(a,b,c,d)) {
        s = 1;
    }
    std::cout << a << std::endl; // 1

现场演示

1赞 Jarod42 11/6/2019 #4

满足该语法

for (auto& s : something{a, b, c, d}) {
    s = 1;
}

您可以创建包装器:

template <typename T>
struct MyRefWrapper
{
public:
    MyRefWrapper(T& p)  : p(&p) {}

    T& operator =(const T& value) const { return *p = value; }

    operator T& () const { return *p; }
private:
    T* p;     
};

演示

评论

1赞 Toby Speight 11/6/2019
这与 ?std::reference_wrapper
1赞 Jarod42 11/6/2019
@TobySpeight:需要.std::reference_wrappers.get() = 1;
4赞 Evg 11/6/2019 #5

只是包装器创意中的另一个解决方案:

template<typename T, std::size_t size>
class Ref_array {
    using Array = std::array<T*, size>;

    class Iterator {
    public:
        explicit Iterator(typename Array::iterator it) : it_(it) {}

        void operator++() { ++it_; }
        bool operator!=(const Iterator& other) const { return it_ != other.it_; }
        decltype(auto) operator*() const { return **it_; }

    private:
        typename Array::iterator it_;
    };

public:
    explicit Ref_array(Array args) : args_(args) {}

    auto begin() { return Iterator(args_.begin()); }
    auto end() { return Iterator(args_.end()); }

private:
    Array args_;
};

template<typename T, typename... Ts>
auto something(T& first, Ts&... rest) {
    static_assert((std::is_same_v<T, Ts> && ...));
    return Ref_array<T, 1 + sizeof...(Ts)>({&first, &rest...});
}

然后:

int main() {
    int a{}, b{}, c{}, d{};

    for (auto& s : something(a, b, c, d)) {
        std::cout << s;
        s = 1;
    }

    std::cout  << std::endl;
    for (auto& s : something(a, b, c, d))
        std::cout << s;
}

输出

0000
1111

评论

2赞 darune 11/6/2019
这是一个体面且良好的解决方案/解决方法。它的想法类似于我自己的答案(我使用了 std::reference_wrapper而不是使用可变参数模板)
0赞 darune 11/6/2019 #6

解决方案:使用引用包装器

template <class It>
struct range_view_iterator : public It{//TODO: don't inherit It
    auto& operator*() {
        return (*this)->get();
    }
};

template<class It>
range_view_iterator(It) -> range_view_iterator<It>;


template<class T>
struct range_view {
    std::vector<std::reference_wrapper<T> > refs_;
    range_view(std::initializer_list<std::reference_wrapper<T> > refs) : refs_{refs} {
    }

    auto begin() {
        return range_view_iterator{ refs_.begin() };
    }

    auto end() {
        return range_view_iterator{ refs_.end() };
    }
};

然后用作:

for (auto& e : range_view<int>{a, b, c, d}) {
    e = 1;
}

不过,这并没有试图回答第一个问题。