Niewinny zakres oparty na pętli nie działa

11

Nie można skompilować:

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

Wypróbuj na Godbolt

Błąd kompilatora to: error: assignment of read-only reference 's'

Teraz w moim przypadku lista składa się ze zmiennych składowych w klasie.

Teraz to nie działa, ponieważ wyrażenie staje się initializer_list<int>tym, które faktycznie kopiuje a, b, c i d - stąd też nie zezwala na modyfikację.

Moje pytanie jest dwojakie:

Czy kryje się za tym jakaś motywacja, by nie pisać w ten sposób pętli bazującej na zakresie? na przykład. być może mógłby istnieć specjalny przypadek nagich wyrazów nawiasów klamrowych.

Jaki jest porządny składni sposób naprawy tego typu pętli?

Preferowane byłoby coś wzdłuż tej linii:

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

Nie uważam pośrednictwa wskaźnika za dobre rozwiązanie (to znaczy {&a, &b, &c, &d}) - każde rozwiązanie powinno dawać odwołanie do elementu bezpośrednio, gdy iterator jest usunięty z odniesienia .

darune
źródło
1
Prosty obejście (że nie będę naprawdę użyć ja) jest stworzenie listy wskaźników zamiast: { &a, &b, &c, &d }.
Jakiś programista koleś
2
initializer_listjest głównie widokiem consttablicy.
Jarod42,
To, co prawdopodobnie bym zrobił, to jawna inicjalizacja zmiennych, jedna po drugiej. Nie będzie więcej pisać, jest jasne i wyraźne, i robi to, co jest zamierzone. :)
Jakiś programista koleś
3
jeśli nie chcesz { &a, &b, &c, &d }, nie będziesz chciał:for (auto& s : std::initializer_list<std::reference_wrapper<int>>{a, b, c, d}) { s.get() = 1; }
Jarod42,
Pytanie „dlaczego to nie jest w stanie działać” jest zupełnie innym pytaniem niż „co mogę zrobić, aby coś takiego działało?”
Nicol Bolas,

Odpowiedzi:

4

Zasięg nie jest tak magiczny, jak ludzie by chcieli. Na koniec musi istnieć obiekt, który kompilator może generować wywołania funkcji członkowskiej lub funkcji swobodnej begin()i end().

Najbliżej prawdopodobnie będziesz w stanie przyjść to:

#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
źródło
1
Możesz upuścić std::vector<int*>.
Jarod42,
@ mhhollomon Wyraźnie oświadczyłem, że nie jestem zainteresowany rozwiązaniem pośrednim dla wskaźnika.
darune
1
Powinno być auto slub auto* snie auto& s.
LF,
@darune - Będę szczęśliwy, że ktoś udzieli innej odpowiedzi. Nie jest jasne, czy taka odpowiedź istnieje w obecnym standardzie.
mhhollomon
@LF - uzgodnione.
mhhollomon
4

Kolejne rozwiązanie w ramach idei opakowania:

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

Następnie:

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

wyjścia

0000
1111
Evg
źródło
2
To jest dobre i dobre rozwiązanie / obejście. Jest to pomysł podobny do mojej własnej odpowiedzi (użyłem std :: reference_wrapper i nie korzystam z szablonu variadic)
darune
4

Zgodnie ze standardowym §11.6.4 Inicjalizacja listy / p5 [dcl.init.list] [ Emphasis Mine ]:

Obiekt typu „std :: initializer_list” jest tworzony z listy inicjalizującej, tak jakby implementacja wygenerowała i zmaterializowała (7.4) wartość typu „tablica N stałej E” , gdzie N jest liczbą elementów na liście inicjalizatora. Każdy element tej tablicy jest inicjowany przez kopiowanie odpowiednim elementem listy inicjalizującej, a obiekt std :: initializer_list jest konstruowany tak, aby odwoływał się do tej tablicy. [Uwaga: Konstruktor lub funkcja konwersji wybrana dla kopii powinna być dostępna (klauzula 14) w kontekście listy inicjalizatora. - uwaga końcowa] Jeśli wymagane jest zawężenie konwersji w celu zainicjowania któregokolwiek z elementów, program jest źle sformułowany.

Zatem twój kompilator narzeka zgodnie z prawem (tzn. auto &sOdejmuje int const& si nie możesz przypisywać do sw pętli dystansowej dla).

Możesz rozwiązać ten problem, wprowadzając kontener zamiast listy inicjalizującej (np. `Std :: vector ') z opcją' 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;
}

Demo na żywo

101010
źródło
@ Jarod42 Ouups przepraszam, poprawiłem to.
101010,
Twoje rozwiązanie nie spełnia moich kryteriów dla fajnego rozwiązania - gdybym był z tego zadowolony, nie
zapytałbym
również twój cytat nie próbuje odpowiedzieć na moje pytanie
darune
1
@darune - w rzeczywistości cytat jest powodem, dla którego for (auto& s : {a, b, c, d})nie działa. Co do tego, dlaczego norma ma tę klauzulę ..... trzeba by zapytać członków komitetu normalizacyjnego. Podobnie jak wiele takich rzeczy, rozumowanie może być cokolwiek pomiędzy: „Nie uważaliśmy, że Twój konkretny przypadek był wystarczająco użyteczny, aby zawracać sobie głowę” aż do „Zbyt wiele innych części standardu musiałoby się zmienić, aby poprzeć twoją sprawę, i odłożyliśmy rozpatrzenie tego wszystkiego, dopóki nie opracujemy przyszłego standardu ”.
Peter,
Nie możesz po prostu użyć std::array<std::reference_wrapper>>?
Toby Speight
1

Aby spełnić tę składnię

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

możesz utworzyć opakowanie:

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

Próbny

Jarod42
źródło
1
Czym to się różni std::reference_wrapper?
Toby Speight,
1
@TobySpeight: std::reference_wrapperwymagałoby s.get() = 1;.
Jarod42,
0

Rozwiązanie: użyj opakowania referencyjnego

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

Następnie używany jako:

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

Nie próbuje to jednak odpowiedzieć na pierwsze pytanie.

darune
źródło
-1

Możesz utworzyć klasę opakowania do przechowywania referencji, która będzie miała operatora przypisania do aktualizacji tej wartości:

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

Demo na żywo

rafix07
źródło