Jaki jest prawidłowy sposób użycia bazującego na zakresach C++11?

Jaki jest prawidłowy sposób użycia C++11 ' s range-based for?

Jakiej składni należy używać? for (auto elem : container), albo for (auto& elem : container) lub for (const auto& elem : container)? Albo jakieś inne?

Author: Mr.C64, 2013-04-10

4 answers

Zacznijmy rozróżniać obserwując elementy w kontenerze vs. modyfikując je na miejscu.

Obserwowanie pierwiastków

Rozważmy prosty przykład:

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

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

Powyższy kod drukuje elementy (int s) w vector:

1 3 5 7 9

Rozważmy teraz inny przypadek, w którym elementy wektorowe nie są tylko prostymi liczbami całkowitymi, ale instancje bardziej złożonej klasy, z niestandardowym konstruktorem kopiującym, itd.

// 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 składni for (auto x : v) {...} z tą nową klasą:

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

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

Wyjście to coś w stylu:

[... 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

Ponieważ można go odczytać z wyjścia, Konstruktor kopiujący wywołania są wykonywane podczas iteracji pętli opartych na zakresach.
To dlatego, że przechwytujemy elementy z kontenera przez wartość (część auto x w for (auto x : v)).

Jest to nieefektywny kod, np. jeśli elementy te są instancje std::string, alokacja pamięci sterty może być wykonana, z kosztownymi wycieczkami 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: capture by const reference , czyli const auto&:

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

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

Teraz wyjście to:

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

Elements:
1 3 5 7 9

Bez fałszywego (i potencjalnie kosztownego) wywołania konstruktora kopiującego.

Więc kiedy obserwujemy elementy w kontenerze (np. dostęp tylko do odczytu), następująca składnia jest dobra dla prostych typów tanich do kopiowania, takich jak int, double, itd.:

for (auto elem : container) 

Else, przechwytywanie przez const odniesienie jest lepsze w przypadku ogólnym , aby uniknąć niepotrzebnych (i potencjalnie kosztownych) wywołań konstruktora kopiującego:

for (const auto& elem : container) 

Modyfikowanie elementów w pojemniku

Jeśli chcemy zmodyfikować elementy w kontenerze za pomocą zakresu for, powyżej for (auto elem : container) i for (const auto& elem : container) składnia jest błędna.

W rzeczywistości, w pierwszym przypadku, elem przechowuje kopię oryginału element, więc dokonane w nim modyfikacje są po prostu utracone i nie przechowywane uporczywie w pojemniku, 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 << ' ';

Wyjście jest tylko sekwencją początkową:

1 3 5 7 9

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

G++ wyświetla komunikat o błędzie coś takiego:

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

The correct podejście w tym przypadku jest przechwytywanie przez nie - const odniesienie:

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

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

Wyjście jest (zgodnie z oczekiwaniami):

10 30 50 70 90

Ta składnia for (auto& elem : container) działa również dla bardziej złożonych typów, np. rozważając 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 << ' ';

Wyjście 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 z jego elementów, przy użyciu powyższej składni:

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

Powyższe kod nie jest kompilowany.

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)
                    ^

Problem polega na tym, że std::vector szablon jest dla bool, Z implementacja, która pakuje bool s w celu optymalizacji przestrzeni (każda wartość logiczna jest przechowywany w jednym bitie, osiem "boolean" bitów w bajcie).

Z tego powodu (ponieważ nie jest możliwe zwrócenie odniesienia do pojedynczego bitu), vector<bool> używa tak zwanego "iteratora proxy" wzór. Iterator proxy (ang. proxy iterator) to iterator, który po zderferowaniu nie daje zwykłe bool &, ale zamiast tego zwraca (według wartości) obiekt tymczasowy ]}, który jest Klasa proxy bool. (Zobacz również to pytanie i związane z nim odpowiedzi tutaj na StackOverflow.)

Aby zmodyfikować w miejsce elementów vector<bool>, nowy rodzaj składni (za pomocą auto&&) należy użyć:

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

Następujący kod działa fine:

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 wyjścia:

false true true false

Zauważ, że for (auto&& elem : container) składnia działa również w innych przypadkach iteratory zwykłe (inne niż proxy) (np. dla a vector<int> lub a vector<string>).

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

Podsumowanie

Powyższa dyskusja może być podsumowana w następujących wytycznych:

  1. Dla obserwując elementy, używaj następującej składni:

    for (const auto& elem : container)    // capture by const reference
    
    • Jeśli obiekty są Tanie do skopiowania (jak int s, double S, itp.), możliwe jest użycie nieco uproszczonej formy:

      for (auto elem : container)    // capture by value
      
  2. Do modyfikowania elementów 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 tworzy lokalną kopię elementu wewnątrz ciała pętli, przechwytując przez Wartość (for (auto elem : container)) to dobry wybór.


Dodatkowe uwagi o kodzie rodzajowym

W kod generyczny , ponieważ nie możemy przyjąć założeń co do tego, że typ generyczny T jest tani w kopiowaniu, w trybie obserwując jest bezpieczny w użyciu for (const auto& elem : container).
(Nie spowoduje to potencjalnie drogich bezużytecznych kopii, będzie działać dobrze również w przypadku tanich do kopiowania typów, takich jak int, a także dla kontenerów używających iteratorów proxy, takich jak std::vector<bool>.)

Ponadto, w trybie modyfikowanie , jeśli chcemy, aby kod generyczny działał również w przypadku iteratorów proxy, najlepszym rozwiązaniem jest for (auto&& elem : container).
(Będzie to działać również w przypadku kontenerów używających zwykłych iteratorów innych niż proxy, takich jak std::vector<int> lub std::vector<string>.)

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

  1. Dla obserwując elementy, zastosowanie:

    for (const auto& elem : container)
    
  2. Do modyfikowania elementów na miejscu, użyj:

    for (auto&& elem : container)
    
 414
Author: Mr.C64,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-12-22 10:02:45

Nie mapoprawnego sposobu użycia for (auto elem : container), for (auto& elem : container) lub for (const auto& elem : container). Po prostu wyrażasz to, czego chcesz.

Pozwól, że ci to wyjaśnię. Przejdźmy się.
for (auto elem : container) ...

Ten jest cukrem składniowym 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óre są tanie w kopiowaniu.

for (auto& elem : container) ...

Ten jest cukrem składniowym 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, gdy chcesz pisać bezpośrednio do elementów w kontenerze, dla przykład.

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

Ten jest cukrem składniowym 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 to wszystko, wszystko jest" poprawne", gdy jest właściwie używane.

 17
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-10 13:34:03

Poprawnym środkiem jest zawsze

for(auto&& elem : container)

To zagwarantuje zachowanie całej semantyki.

 4
Author: Puppy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-10 19:35:24

Podczas gdy początkową motywacją pętli range-for mogła być łatwość iteracji nad elementami kontenera, składnia jest na tyle ogólna, że może być użyteczna nawet dla obiektów, które nie są wyłącznie kontenerami.

Wymogiem składniowym dla pętli for Jest to, że range_expression obsługuje begin() i end() jako funkcje-jako funkcje członkowskie typu, do którego jest ewaluowana, lub jako funkcje nie-członkowskie, które przyjmują instancję tego typu.

Jako wymyślony przykład można Generuj zakres liczb i powtarzaj go za pomocą poniższej 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;
}; 

Z następującą funkcją main,

#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 wyjście.

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 
 1
Author: R Sahu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-12 07:17:57