Jaki jest prawidłowy sposób korzystania z zakresu C ++ 11?

211

Jaki jest właściwy sposób używania opartego na zakresie C ++ 11 for?

Jakiej składni należy użyć? for (auto elem : container)lub for (auto& elem : container)lub for (const auto& elem : container)? A może jakiś inny?

Mr.C64
źródło
6
To samo dotyczy argumentów funkcji.
Maxim Egorushkin
3
W rzeczywistości ma to niewiele wspólnego z opcją opartą na zakresie. To samo można powiedzieć o każdym auto (const)(&) x = <expr>;.
Matthieu M.
2
@MatthieuM: Ma to oczywiście wiele wspólnego z zasięgiem! Rozważ początkującego, który widzi kilka składni i nie może wybrać, której formy użyć. Celem „pytań i odpowiedzi” była próba wyjaśnienia różnic między niektórymi przypadkami (i omówienia spraw, które dobrze się kompilują, ale są trochę nieefektywne z powodu bezużytecznych głębokich kopii itp.).
Mr.C64
2
@ Mr. C64: Moim zdaniem ma to więcej wspólnego z auto, ogólnie, niż z zasięgiem; bez problemu możesz używać opartego na zasięgu auto! for (int i: v) {}jest całkowicie w porządku. Oczywiście większość punktów, które podniosłeś w odpowiedzi, może mieć więcej wspólnego z typem niż z auto... ale z pytania nie jest jasne, gdzie jest punkt bólu. Osobiście walczyłbym o usunięcie autoz pytania; lub może wyraźnie powiedzieć, że niezależnie od tego, czy używasz, czy autoteż wyraźnie określasz typ, pytanie koncentruje się na wartości / referencji.
Matthieu M.
1
@ MatthieuM .: Jestem otwarty na zmianę tytułu lub edycję pytania w jakiejś formie, która może uczynić je bardziej zrozumiałymi ... Ponownie skupiłem się na omówieniu kilku opcji składni opartych na zakresie (pokazując kod, który się kompiluje, ale jest nieefektywny, kod, który się nie kompiluje itp.) i próba zaoferowania komuś wskazówek (szczególnie na poziomie początkującym) zbliżających się do pętli w zakresie C ++ 11.
Mr.C64

Odpowiedzi:

389

Zacznijmy od rozróżnienia między obserwowaniem elementów w pojemniku a modyfikowaniem ich na miejscu.

Obserwować elementy

Rozważmy prosty przykład:

vector<int> v = {1, 3, 5, 7, 9};

for (auto x : v)
    cout << x << ' ';

Powyższy kod drukuje elementy intw vector:

1 3 5 7 9

Rozważmy teraz inny przypadek, w którym elementy wektorowe nie są zwykłymi liczbami całkowitymi, ale instancjami bardziej złożonej klasy z niestandardowym konstruktorem kopii itp.

// A sample test class, with custom copy semantics.
class X
{
public:
    X() 
        : m_data(0) 
    {}

    X(int data)
        : m_data(data)
    {}

    ~X() 
    {}

    X(const X& other) 
        : m_data(other.m_data)
    { cout << "X copy ctor.\n"; }

    X& operator=(const X& other)
    {
        m_data = other.m_data;       
        cout << "X copy assign.\n";
        return *this;
    }

    int Get() const
    {
        return m_data;
    }

private:
    int m_data;
};

ostream& operator<<(ostream& os, const X& x)
{
    os << x.Get();
    return os;
}

Jeśli użyjemy powyższej for (auto x : v) {...}składni z nową klasą:

vector<X> v = {1, 3, 5, 7, 9};

cout << "\nElements:\n";
for (auto x : v)
{
    cout << x << ' ';
}

wynik jest podobny do:

[... copy constructor calls for vector<X> initialization ...]

Elements:
X copy ctor.
1 X copy ctor.
3 X copy ctor.
5 X copy ctor.
7 X copy ctor.
9

Jak można odczytać z wyjścia, wywołania konstruktora kopiowania są wykonywane podczas iteracji pętli na podstawie zakresu.
Wynika to z tego, że przechwytujemy elementy z kontenera według wartości ( auto xczęść wfor (auto x : v) ).

Jest to nieefektywny kod, np. Jeśli te elementy są instancjami std::string, można dokonać alokacji pamięci sterty, kosztownych podróży do menedżera pamięci itp. Jest to bezużyteczne, jeśli chcemy tylko obserwować elementy w kontenerze.

Dostępna jest więc lepsza składnia: przechwytywanie przez constreferencję , tj . const auto&:

vector<X> v = {1, 3, 5, 7, 9};

cout << "\nElements:\n";
for (const auto& x : v)
{ 
    cout << x << ' ';
}

Teraz dane wyjściowe to:

 [... copy constructor calls for vector<X> initialization ...]

Elements:
1 3 5 7 9

Bez żadnego fałszywego (i potencjalnie kosztownego) wywołania konstruktora kopiowania.

Tak więc, gdy obserwując elementy w pojemniku (czyli dla dostępem tylko do odczytu), następujące składnia jest w porządku dla prostych tani do imitowania typów, jak int, doubleitp .:

for (auto elem : container) 

W przeciwnym razie przechwytywanie przez constodniesienie jest lepsze w ogólnym przypadku , aby uniknąć niepotrzebnych (i potencjalnie drogich) wywołań konstruktora kopiowania:

for (const auto& elem : container) 

Modyfikacja elementów w kontenerze

Jeśli chcemy zmodyfikować elementy w kontenerze za pomocą zakresu for, powyższe for (auto elem : container)i for (const auto& elem : container) składnie są niepoprawne.

W rzeczywistości w pierwszym przypadku elemprzechowuje kopię oryginalnego elementu, więc modyfikacje dokonane w nim są po prostu tracone i nie są trwale przechowywane w kontenerze, np .:

vector<int> v = {1, 3, 5, 7, 9};
for (auto x : v)  // <-- capture by value (copy)
    x *= 10;      // <-- a local temporary copy ("x") is modified,
                  //     *not* the original vector element.

for (auto x : v)
    cout << x << ' ';

Dane wyjściowe to tylko sekwencja początkowa:

1 3 5 7 9

Zamiast tego próba użycia for (const auto& x : v)po prostu się nie kompiluje.

g ++ wyświetla komunikat o błędzie podobny do tego:

TestRangeFor.cpp:138:11: error: assignment of read-only reference 'x'
          x *= 10;
            ^

Prawidłowe podejście w tym przypadku polega na uchwyceniu przez brak constodniesienia:

vector<int> v = {1, 3, 5, 7, 9};
for (auto& x : v)
    x *= 10;

for (auto x : v)
    cout << x << ' ';

Dane wyjściowe są (zgodnie z oczekiwaniami):

10 30 50 70 90

Ta for (auto& elem : container)składnia działa również w przypadku bardziej złożonych typów, np. Biorąc pod uwagę vector<string>:

vector<string> v = {"Bob", "Jeff", "Connie"};

// Modify elements in place: use "auto &"
for (auto& x : v)
    x = "Hi " + x + "!";

// Output elements (*observing* --> use "const auto&")
for (const auto& x : v)
    cout << x << ' ';

dane wyjściowe to:

Hi Bob! Hi Jeff! Hi Connie!

Szczególny przypadek iteratorów proxy

Załóżmy, że mamy vector<bool>, i chcemy odwrócić logiczny stan logiczny jego elementów, używając powyższej składni:

vector<bool> v = {true, false, false, true};
for (auto& x : v)
    x = !x;

Powyższy kod się nie kompiluje.

g ++ wyświetla komunikat o błędzie podobny do tego:

TestRangeFor.cpp:168:20: error: invalid initialization of non-const reference of
 type 'std::_Bit_reference&' from an rvalue of type 'std::_Bit_iterator::referen
ce {aka std::_Bit_reference}'
     for (auto& x : v)
                    ^

Problemem jest to, że std::vectorszablon wyspecjalizowane dla boolz implementacji, że pakiety z boolS do miejsca Optymalizacja (każda wartość logiczna jest przechowywany w jednym kawałku, ośmiu „wartość logiczna” bitów bajtu).

Z tego powodu (ponieważ nie można zwrócić referencji do jednego bitu) vector<bool>używa tak zwanego wzorca „iteratora proxy” . „Iterator proxy” to iterator, który po dereferencji nie daje zwykłego bool &, ale zwraca (według wartości) obiekt tymczasowy , do którego można przekształcić klasę proxybool . (Zobacz także to pytanie i powiązane odpowiedzi tutaj na StackOverflow.)

Aby zmodyfikować w miejscu elementy vector<bool>, należy użyć nowego rodzaju składni (using auto&&):

for (auto&& x : v)
    x = !x;

Poniższy kod działa poprawnie:

vector<bool> v = {true, false, false, true};

// Invert boolean status
for (auto&& x : v)  // <-- note use of "auto&&" for proxy iterators
    x = !x;

// Print new element values
cout << boolalpha;        
for (const auto& x : v)
    cout << x << ' ';

i wyniki:

false true true false

Zauważ, że for (auto&& elem : container)składnia działa również w innych przypadkach zwykłych iteratorów (niebędących proxy) (np. Dla a vector<int>lub avector<string> ).

(Na marginesie, wspomniana wyżej „obserwująca” składnia for (const auto& elem : container)działa dobrze również w przypadku iteratora proxy).

Podsumowanie

Powyższą dyskusję można streścić w następujących wytycznych:

  1. Aby obserwować elementy, użyj następującej składni:

    for (const auto& elem : container)    // capture by const reference
    • Jeśli obiekty są tanie do skopiowania (takie jak ints, doubles itp.), Można użyć nieco uproszczonej formy:

      for (auto elem : container)    // capture by value
  2. Aby zmodyfikować elementy na miejscu, użyj:

    for (auto& elem : container)    // capture by (non-const) reference
    • Jeśli kontener używa „iteratorów proxy” (jak std::vector<bool>), użyj:

      for (auto&& elem : container)    // capture by &&

Oczywiście, jeśli istnieje potrzeba wykonania lokalnej kopii elementu w ciele pętli, dobrym rozwiązaniem jest przechwycenie za pomocą value ( for (auto elem : container)).


Dodatkowe uwagi na temat kodu ogólnego

W ogólnym kodzie , ponieważ nie możemy zakładać, że typ ogólny Tjest tani do kopiowania, w trybie obserwacji zawsze można bezpiecznie używać for (const auto& elem : container).
(Nie uruchomi to potencjalnie kosztownych, bezużytecznych kopii, będzie działać dobrze również w przypadku typów tanich do kopiowania, takich jak int, a także w przypadku kontenerów korzystających z iteratorów proxy itp std::vector<bool>.)

Co więcej, w trybie modyfikacji , jeśli chcemy, aby kod ogólny działał również w przypadku iteratorów proxy, najlepszą opcją jest for (auto&& elem : container).
(Będzie to działać dobrze również w przypadku kontenerów używających zwykłych iteratorów innych niż proxy, takich jak std::vector<int>lubstd::vector<string> .)

Zatem w kodzie ogólnym można podać następujące wytyczne:

  1. Do obserwacji elementów użyj:

    for (const auto& elem : container)
  2. Aby zmodyfikować elementy na miejscu, użyj:

    for (auto&& elem : container)
Mr.C64
źródło
7
Brak porad dotyczących ogólnych kontekstów? :(
R. Martinho Fernandes
11
Dlaczego nie zawsze używać auto&&? Czy istnieje const auto&&?
Martin Ba
1
Chyba brakuje Ci przypadku, w którym potrzebujesz kopii wewnątrz pętli?
juanchopanza
6
„Jeśli użyje pojemnik«iteratory proxy»” - a ty wiesz, że używa „iteratory proxy” (co może nie być w przypadku kodu ogólne). Myślę więc, że najlepsze jest rzeczywiście auto&&, ponieważ obejmuje ono auto&równie dobrze.
Christian Rau,
5
Dziękuję, to było naprawdę świetne „wprowadzenie do kursu awaryjnego” do składni i kilka wskazówek dotyczących zakresu dla programisty C #. +1.
AndrewJacksonZA
17

Nie ma prawidłowego sposobu użycia for (auto elem : container), lub for (auto& elem : container)lub for (const auto& elem : container). Po prostu wyrażasz, co chcesz.

Pozwól mi rozwinąć tę kwestię. Przejdźmy się.

for (auto elem : container) ...

Ten jest cukrem syntaktycznym dla:

for(auto it = container.begin(); it != container.end(); ++it) {

    // Observe that this is a copy by value.
    auto elem = *it;

}

Możesz użyć tego, jeśli twój kontener zawiera elementy, których kopiowanie jest tanie.

for (auto& elem : container) ...

Ten jest cukrem syntaktycznym dla:

for(auto it = container.begin(); it != container.end(); ++it) {

    // Now you're directly modifying the elements
    // because elem is an lvalue reference
    auto& elem = *it;

}

Użyj tego, jeśli chcesz na przykład pisać bezpośrednio do elementów w kontenerze.

for (const auto& elem : container) ...

Ten jest cukrem syntaktycznym dla:

for(auto it = container.begin(); it != container.end(); ++it) {

    // You just want to read stuff, no modification
    const auto& elem = *it;

}

Jak mówi komentarz, tylko do czytania. I o to chodzi, wszystko jest „prawidłowe”, gdy jest właściwie używane.


źródło
2
Zamierzałem dać pewne wskazówki, z przykładowymi kodami kompilującymi się (ale nieefektywnymi) lub nieudanymi w kompilacji i wyjaśniającymi dlaczego, i próbować zaproponować pewne rozwiązania.
Mr.C64
2
@ Mr.C64 Och, przepraszam - właśnie zauważyłem, że jest to jedno z pytań typu FAQ. Jestem nowy na tej stronie. Przeprosiny! Twoja odpowiedź jest świetna, głosowałem za nią - ale chciałem też przedstawić bardziej zwięzłą wersję dla tych, którzy chcą istoty . Mam nadzieję, że nie przeszkadzam.
1
@ Mr. C64, na czym polega problem z odpowiedzią OP na pytanie? To tylko kolejna, ważna odpowiedź.
mfontanini,
1
@mfontanini: Nie ma absolutnie żadnego problemu, jeśli ktoś opublikuje odpowiedź, nawet lepszą niż moja. Ostatecznym celem jest wniesienie wysokiej jakości wkładu do społeczności (szczególnie dla początkujących, którzy mogą czuć się zagubieni przed różnymi składniami i różnymi opcjami oferowanymi przez C ++).
Mr.C64
4

Prawidłowe środki są zawsze

for(auto&& elem : container)

Zagwarantuje to zachowanie całej semantyki.

Szczeniak
źródło
6
Ale co jeśli kontener zwróci tylko modyfikowalne odwołania i chcę wyjaśnić, że nie chcę ich modyfikować w pętli? Czy nie powinienem wtedy użyć, auto const &aby wyjaśnić moją intencję?
RedX
@RedX: Co to jest „modyfikowalne odniesienie”?
Wyścigi lekkości na orbicie
2
@RedX: Referencje nigdy constnie są i nigdy nie można ich modyfikować . W każdym razie moja odpowiedź na to pytanie brzmiałaby „ tak” .
Wyścigi lekkości na orbicie
4
Chociaż może to zadziałać, uważam, że jest to kiepska rada w porównaniu z bardziej dopracowanym i przemyślanym podejściem podanym przez doskonałą i kompleksową odpowiedź pana C64 podaną powyżej. C ++ nie jest redukowane do najmniej powszechnego mianownika.
Jack Aidley,
6
Ta propozycja ewolucji języka zgadza się z tą „słabą” odpowiedzią: open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3853.htm
Luc Hermitte
1

Podczas gdy początkową motywacją dla pętli zasięgu może być łatwość iteracji po elementach kontenera, składnia jest na tyle ogólna, że ​​jest użyteczna nawet dla obiektów, które nie są czysto kontenerami.

Wymaganiem składniowym dla pętli for jest to, że range_expressionobsługuje begin()i end()jako obie funkcje - albo jako funkcje składowe typu, którego ocenia, albo jako funkcje nie będące członkami, które przyjmują instancję typu.

Jako wymyślony przykład można wygenerować zakres liczb i iterować po nim, używając następującej klasy.

struct Range
{
   struct Iterator
   {
      Iterator(int v, int s) : val(v), step(s) {}

      int operator*() const
      {
         return val;
      }

      Iterator& operator++()
      {
         val += step;
         return *this;
      }

      bool operator!=(Iterator const& rhs) const
      {
         return (this->val < rhs.val);
      }

      int val;
      int step;
   };

   Range(int l, int h, int s=1) : low(l), high(h), step(s) {}

   Iterator begin() const
   {
      return Iterator(low, step);
   }

   Iterator end() const
   {
      return Iterator(high, 1);
   }

   int low, high, step;
}; 

Dzięki następującej mainfunkcji

#include <iostream>

int main()
{
   Range r1(1, 10);
   for ( auto item : r1 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;

   Range r2(1, 20, 2);
   for ( auto item : r2 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;

   Range r3(1, 20, 3);
   for ( auto item : r3 )
   {
      std::cout << item << " ";
   }
   std::cout << std::endl;
}

otrzymamy następujące dane wyjściowe.

1 2 3 4 5 6 7 8 9 
1 3 5 7 9 11 13 15 17 19 
1 4 7 10 13 16 19 
R Sahu
źródło