12

The following does not compile:

#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;
}

Try it on godbolt

Compiler error is: error: assignment of read-only reference 's'

Now in my actual case the list is made of member variables on a class.

Now, this doesn't work because the expression becomes an initializer_list<int> that actually copies a,b,c, and d - hence also not allowing modification.

My question is two-fold:

Is there any motivation behind not allowing to write a range-based for loop in this way ? eg. perhaps there could be a special case for naked brace expressions.

What is a syntactical neat way of fixing this type of loop ?

Something along this line would be preferred:

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

I do not consider pointer indirection a good solution (that is {&a, &b, &c, &d}) - any solution should give the element reference directly when the iterator is de-referenced.

L. F.
  • 19,445
  • 8
  • 48
  • 82
darune
  • 10,480
  • 2
  • 24
  • 62
  • 1
    A simple workaround (that I wouldn't really use myself) is to create a list of pointers instead: `{ &a, &b, &c, &d }`. – Some programmer dude Nov 06 '19 at 12:47
  • 2
    `initializer_list` is mostly a view on `const` array. – Jarod42 Nov 06 '19 at 12:49
  • What I would probably do is to explicitly initialize the variables, one by one. It's not going to be much more to write, it's clear and explicit, and it does what's intended. :) – Some programmer dude Nov 06 '19 at 12:51
  • 3
    if you don't want `{ &a, &b, &c, &d }`, you won't want neither: `for (auto& s : std::initializer_list>{a, b, c, d}) { s.get() = 1; }` – Jarod42 Nov 06 '19 at 13:03
  • The questions "why isn't this able to work" is a very different question from "what can I do to make something like this work?" – Nicol Bolas Nov 06 '19 at 13:10
  • You should always `#include ` to use that syntax; not doing so has undefined behaviour. Usually at least one other header has coincidentally indirectly included it for you, or the compiler will error, but those shouldn't be relied upon. – underscore_d Nov 06 '19 at 13:31
  • `{&a, &b, &c, &d}` is fine. Without the ampersand, people would assume that you are passing in value and not address. – KevinZ Nov 07 '19 at 21:26

6 Answers6

4

Ranges are not as magic as people would like. In the end, there must be an object that the compiler can generate calls on to either a member function or free function begin() and end().

Closest you'll probably be able to come is:

#include <iostream>

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

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

According to the standard §11.6.4 List-initialization/p5 [dcl.init.list] [Emphasis Mine]:

An object of type 'std::initializer_list' is constructed from an initializer list as if the implementation generated and materialized (7.4) a prvalue of type “array of N const E”, where N is the number of elements in the initializer list. Each element of that array is copy-initialized with the corresponding element of the initializer list, and the std::initializer_list object is constructed to refer to that array. [ Note: A constructor or conversion function selected for the copy shall be accessible (Clause 14) in the context of the initializer list. — end note ] If a narrowing conversion is required to initialize any of the elements, the program is ill-formed.

Thus, your compiler is complaining legitimately (i.e., auto &s deducts to int const& s and you cannot assign to s in the ranged for loop).

You could alleviate this problem by introducing a container instead of an initializer list (e.g., `std::vector’) with ‘std::reference_wrapper’:

#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;
}

Live Demo

Evg
  • 25,259
  • 5
  • 41
  • 83
101010
  • 41,839
  • 11
  • 94
  • 168
  • @Jarod42 Ouups sorry, amended that. – 101010 Nov 06 '19 at 13:03
  • You solution does not fit my criteria for a nice solution - if i was happy with that I wouldn't have asked in the first place :) – darune Nov 06 '19 at 13:05
  • also your quote does not attempt to answer my question – darune Nov 06 '19 at 13:08
  • 1
    @darune - actually, the quote is the reason that your `for (auto& s : {a, b, c, d})` doesn't work. As to why the standard has that clause ..... you'd have to ask members of the standardisation committee. Like many such things, the reasoning could be anything between "We didn't consider your particular case was useful enough to bother about" through to "Too many other parts of the standard would have to change to support your case, and we postponed consideration of all that until we develop a future standard". – Peter Nov 06 '19 at 13:32
  • Can't you just use `std::array>`? – Toby Speight Nov 06 '19 at 13:34
  • Possibly: `s/vector/initializer_list/g`. – Jarod42 Nov 06 '19 at 15:52
4

Just another solution within a wrapper idea:

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...});
}

Then:

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;
}

outputs

0000
1111
Evg
  • 25,259
  • 5
  • 41
  • 83
  • 2
    This is a decent and good solution/workaround. Its an idea similiar to my own answer (I used a std::reference_wrapper and not using variadic template) – darune Nov 06 '19 at 13:54
1

To satisfy that syntax

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

you might create wrapper:

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;     
};

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302
0

Solution: use a reference wrapper

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() };
    }
};

Then used as:

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

This doesn't try to answer the first question though.

darune
  • 10,480
  • 2
  • 24
  • 62
-1

You could create wrapper class for storing reference and which will have assignment operator to update this value:

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

Live demo

rafix07
  • 20,001
  • 3
  • 20
  • 33