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

212

Как правильно использовать C ++ 11 на основе диапазона for?

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

Mr.C64
источник
6
То же самое относится и к аргументам функции.
Максим Егорушкин
3
На самом деле, это имеет мало общего с диапазоном для. То же самое можно сказать о любом auto (const)(&) x = <expr>;.
Матье М.
2
@MatthieuM: Это имеет много общего с диапазоном на основе для, конечно! Рассмотрим новичка, который видит несколько синтаксисов и не может выбрать, какую форму использовать. Смысл «вопросов и ответов» состоял в том, чтобы попытаться пролить некоторый свет и объяснить различия некоторых случаев (и обсудить случаи, которые хорошо компилируются, но являются неэффективными из-за бесполезных глубоких копий и т. Д.).
Mr.C64
2
@ Mr.C64: Насколько мне известно, это больше связано с autoв целом, чем с диапазоном; Вы можете прекрасно использовать диапазон без каких-либо auto! for (int i: v) {}отлично в порядке. Конечно, большинство вопросов, которые вы поднимаете в своем ответе, может иметь больше общего с типом, чем с auto... но из вопроса неясно, где находится болевая точка. Лично я бы боролся за снятие autoс вопроса; или, может быть, сделать явным, что независимо от того, используете ли вы autoтип или явно называете тип, вопрос сосредоточен на значении / ссылке.
Матье М.
1
@MatthieuM .: Я открыт, чтобы изменить название или отредактировать вопрос в какой-либо форме, которая может сделать их более понятными ... Опять же, я сосредоточился на обсуждении нескольких вариантов синтаксиса на основе диапазона (показ кода, который компилируется, но неэффективный, код, который не компилируется и т. д.) и пытающийся предложить какое-то руководство для кого-то (особенно на начальном уровне), приближающегося к циклам для C ++ 11 на основе диапазона.
Mr.C64

Ответы:

390

Давайте начнем с разграничения между наблюдением за элементами в контейнере и их изменением на месте.

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

Давайте рассмотрим простой пример:

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

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

Приведенный выше код печатает элементы intв 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

Как это можно прочитать из выходных данных, вызовы конструктора копирования выполняются во время итераций цикла на основе диапазона.
Это потому , что мы захватывая элементы из контейнера по значению (The 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!

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

Предположим, у нас есть 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˙s для оптимизации пространства (каждый булево значение хранится в один бит, восемь «Boolean» бит в байте).

Из-за этого (поскольку невозможно вернуть ссылку на один бит), 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> или a vector<string>).

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

Резюме

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

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

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

      for (auto elem : container)    // capture by value
  2. Для изменения элементов на месте используйте:

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

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

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


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

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

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

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

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

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

    for (auto&& elem : container)
Mr.C64
источник
7
Нет советов для общего контекста? :(
Р. Мартиньо Фернандес
11
Почему не всегда использовать auto&&? Есть ли const auto&&?
Мартин Ба
1
Я полагаю, вы упускаете случай, когда вам действительно нужна копия внутри цикла?
juanchopanza
6
«Если контейнер использует« прокси-итераторы » - и вы знаете, что он использует« прокси-итераторы »(что может быть не так в общем коде). Так что я думаю, что это действительно лучшее auto&&, так как оно auto&одинаково хорошо покрывает .
Кристиан Рау
5
Спасибо, это было действительно отличное «введение в ускоренный курс» к синтаксису и несколько советов по диапазону для программиста C #. +1.
AndrewJacksonZA
17

Там нет правильного способа использовать 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;

}

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


источник
2
Я намеревался дать некоторое руководство, с примерами кодов, компилирующих (но неэффективно), или не в состоянии компилировать, и объясняя почему, и попытаться предложить некоторые решения.
Mr.C64
2
@ Mr.C64 О, прости, я только что заметил, что это один из тех вопросов типа вопросов и ответов. Я новичок в этом сайте. Извиняюсь! Ваш ответ великолепен, я проголосовал за него - но также хотел предоставить более краткую версию для тех, кто хочет суть этого . Надеюсь, я не помешаю.
1
@ Mr.C64, в чем проблема с ОП, отвечающим на вопрос? Это просто еще один, правильный ответ.
mfontanini
1
@mfontanini: Нет абсолютно никаких проблем, если кто-то отправит ответ, даже лучше, чем мой. Конечная цель - сделать качественный вклад в сообщество (особенно для начинающих, которые могут чувствовать себя потерянными перед различными синтаксисами и различными опциями, которые предлагает C ++).
Mr.C64
4

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

for(auto&& elem : container)

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

щенок
источник
6
Но что, если контейнер возвращает только изменяемые ссылки, и я хочу прояснить, что я не хочу изменять их в цикле? Разве я не должен тогда использовать, auto const &чтобы прояснить свои намерения?
RedX
@RedX: Что такое «изменяемая ссылка»?
Гонки легкости на орбите
2
@RedX: ссылки никогда не бывают const, и они никогда не изменяются. Во всяком случае, мой ответ вам да, я бы .
Гонки легкости на орбите
4
Хотя это может сработать, я считаю, что это плохой совет по сравнению с более тонким и продуманным подходом, данным превосходным и всеобъемлющим ответом Mr.C64, приведенным выше. Сокращение до наименьшего общего знаменателя - это не то, для чего нужен C ++.
Джек Эйдли,
6
Это предложение об эволюции языка согласуется с этим «плохим» ответом: open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3853.htm
Люк Эрмит
1

Хотя первоначальной мотивацией цикла 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 
Р Саху
источник