Каков правильный способ использования С++ 11?

Каков правильный способ использования С++ 11 на основе диапазона for?

Какой синтаксис следует использовать? for (auto elem : container) или for (auto& elem : container) или for (const auto& elem : container)? Или какой-то другой?

Ответ 1

Пусть начнется дифференциация между наблюдением элементов в континенте против их изменения на месте.

Наблюдение за элементами

Рассмотрим простой пример:

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

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

Вышеприведенный код печатает элементы (int s) в vector:

1 3 5 7 9

Теперь рассмотрим другой случай, когда векторные элементы являются не просто простыми целыми числами, но экземпляры более сложного класса, с настраиваемым конструктором копирования и т.д.

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

Если мы используем приведенный выше синтаксис for (auto x : v) {...} с этим новым классом:

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

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

вывод выглядит примерно так:

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

Как он может быть прочитан на выходе, вызовы copy constructor выполняются во время диапазон для итераций цикла.
Это происходит потому, что мы захватываем элементы из контейнера по значению (часть auto x в for (auto x : v)).

Это неэффективный код, например. если эти элементы являются экземплярами std::string, выделение памяти кучи может быть выполнено с дорогими поездками в диспетчер памяти и т.д. Это бесполезно, если мы просто хотим наблюдать за элементами в контейнере.

Таким образом, доступен лучший синтаксис: const ссылка, т.е. const auto&:

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

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

Теперь вывод:

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

Elements:
1 3 5 7 9

Без ложного (и потенциально дорогого) вызова конструктора копии.

Итак, когда наблюдает элементы в контейнере (то есть для доступа только для чтения), следующий синтаксис подходит для простых дешевых типов, таких как int, double и т.д.:

for (auto elem : container) 

В общем случае запись в const лучше в общем случае, чтобы избежать бесполезных (и потенциально дорогостоящих) вызовов конструктора копирования:

for (const auto& elem : container) 

Изменение элементов в контейнере

Если мы хотим изменить элементы в контейнере, используя for на основе диапазона, выше for (auto elem : container) и for (const auto& elem : container) Синтаксис неверен.

Фактически, в первом случае elem хранит копию оригинала элемент, поэтому внесенные в него изменения просто теряются и не сохраняются постоянно в контейнере, например:

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

Результат - это только начальная последовательность:

1 3 5 7 9

Вместо этого попытка использования for (const auto& x : v) просто не скомпилируется.

g++ выводит сообщение об ошибке примерно так:

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

Правильный подход в этом случае заключается в захвате с помощью ссылки const:

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

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

Выход (как ожидалось):

10 30 50 70 90

Этот синтаксис for (auto& elem : container) также работает для более сложных типов, например учитывая 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 << ' ';

вывод:

Hi Bob! Hi Jeff! Hi Connie!

Частный случай итераторов прокси-серверов

Предположим, что a vector<bool>, и мы хотим инвертировать логическое логическое состояние его элементов, используя следующий синтаксис:

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

Вышеприведенный код не компилируется.

g++ выводит сообщение об ошибке, подобное этому:

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

Проблема в том, что шаблон std::vector специализирован для bool, с реализация, которая упаковывает bool для оптимизации пространства (каждое логическое значение хранится в одном бите, восемь "булевых" битов в байте).

Из-за этого (поскольку невозможно вернуть ссылку на один бит) vector<bool> использует так называемый шаблон "прокси-итератор" . "Инитор-прокси" - это итератор, который при разыменовании не дает обычный bool &, но вместо этого возвращает (по значению) временный объект, который является класс прокси конвертируемых в bool. (См. Также Этот вопрос и связанные ответы здесь, на StackOverflow.)

Чтобы изменить на месте элементы vector<bool>, новый вид синтаксиса (используя auto&&) необходимо использовать:

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

Следующий код работает нормально:

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

и выходы:

false true true false

Обратите внимание, что синтаксис for (auto&& elem : container) также работает в других случаях обычных (не-прокси) итераторов (например, для vector<int> или vector<string>).

(Как примечание, вышеупомянутый синтаксис "наблюдения" for (const auto& elem : container) отлично работает и для итератора прокси-сервера.)

Резюме

Вышеприведенное обсуждение можно резюмировать в следующих руководствах:

  • Для элементов наблюдения используйте следующий синтаксис:

    for (const auto& elem : container)    // capture by const reference
    
    • Если объекты дешевы для копирования (например, int s, double s и т.д.), можно использовать слегка упрощенную форму:

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

  • Для изменения элементов на месте используйте:

    for (auto& elem : container)    // capture by (non-const) reference
    
    • Если в контейнере используются "итераторы прокси" (например, std::vector<bool>), используйте:

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

Конечно, если есть необходимость сделать локальную копию элемента внутри тела цикла, то захват по значению (for (auto elem : container)) является хорошим выбором.


Дополнительные примечания к генерическому коду

В общем коде, поскольку мы не можем делать предположения о том, что общий тип T дешев для копирования, в режиме наблюдения безопасно всегда использовать for (const auto& elem : container).
(Это не вызовет потенциально дорогостоящие бесполезные копии, будет отлично работать и для дешевых типов копий типа int, а также для контейнеров с использованием прокси-итераторов, таких как std::vector<bool>.)

Кроме того, в режиме изменения, если мы хотим, чтобы общий код работал и в случае прокси-итераторов, лучшим вариантом является for (auto&& elem : container).
(Это будет отлично работать также и для контейнеров с использованием обычных не-прокси-итераторов, таких как std::vector<int> или std::vector<string>.)

Таким образом, в общем коде могут быть предоставлены следующие рекомендации:

  • Для наблюдения элементов используйте:

    for (const auto& elem : container)
    
  • Для изменения элементов на месте используйте:

    for (auto&& elem : container)
    

Ответ 2

Нет правильного способа использования for (auto elem : container), или for (auto& elem : container) или for (const auto& elem : container). Вы просто выражаете то, что хотите.

Позвольте мне подробнее остановиться на этом. Позвольте прогуляться.

for (auto elem : container) ...

Это синтаксический сахар для:

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

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

}

Вы можете использовать этот, если в вашем контейнере содержатся элементы, которые дешево копировать.

for (auto& elem : container) ...

Это синтаксический сахар для:

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;

}

Используйте это, если вы хотите, например, писать элементы в контейнере.

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

Это синтаксический сахар для:

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

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

}

Как говорится в комментарии, просто для чтения. И что об этом, все правильно "правильно" при правильном использовании.

Ответ 3

Правильное средство всегда

for(auto&& elem : container)

Это гарантирует сохранение всей семантики.

Ответ 4

В то время как первоначальная мотивация цикла range-for могла быть легкостью итерации по элементам контейнера, синтаксис достаточно общий, чтобы быть полезным даже для объектов, которые не являются чисто контейнерами.

Синтаксическое требование для цикла for заключается в том, что range_expression поддерживают begin() и end() как функции - либо как функции-члены того типа, который он оценивает, либо как функции, не являющиеся членами, что принимает экземпляр типа.

В качестве надуманного примера можно создать диапазон чисел и выполнить итерацию по диапазону, используя следующий класс.

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

Со следующей функцией 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;
}

можно получить следующий вывод.

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