Для чого правильний спосіб використання діапазону C ++ 11?


211

Який правильний спосіб використання на основі діапазону C ++ 11 for?

Який синтаксис слід використовувати? for (auto elem : container), або for (auto& elem : container)або for (const auto& elem : container)? Або якийсь інший?


6
Це ж розгляд стосується аргументів функції.
Максим Єгорушкін

3
Насправді це мало спільного з діапазоном на основі діапазону. Те саме можна сказати про будь-кого auto (const)(&) x = <expr>;.
Матьє М.

2
@MatthieuM: Звичайно, це має багато спільного з діапазоном на основі діапазону! Розглянемо початківця, який бачить кілька синтаксисів і не може вибрати, яку форму використовувати. Суть "Q&A" полягала в тому, щоб спробувати пролити трохи світла та пояснити відмінності деяких випадків (і обговорити випадки, які складають штрафи, але є неефективними через непотрібні глибокі копії тощо).
Mr.C64

2
@ Mr.C64: Що стосується мене, це стосується auto, в цілому, більше, ніж для діапазону; ви можете бездоганно використовувати діапазон на основі без будь-якого auto! for (int i: v) {}ідеально добре. Звичайно, більшість балів, які ви піднімаєте у своїй відповіді, можуть мати більше стосунку до типу, ніж до auto... але з питання не зрозуміло, де знаходиться біль. Особисто я б бачив за те, щоб зняти autoпитання; або, можливо, чітко поясніть, що незалежно від того, використовуєте ви autoабо явно називаєте тип, питання зосереджено на значенні / посиланні.
Матьє М.

1
@MatthieuM: Я готовий змінити назву або відредагувати питання в якійсь формі, яка може зробити їх більш зрозумілими ... Знову ж таки, мою увагу було обговорити кілька варіантів синтаксисів на основі діапазону (показ коду, який компілюється, але є неефективний, код, який не вдається компілювати тощо) і намагається запропонувати певні вказівки комусь (особливо на рівні початківців), наближаючись до циклів на основі діапазону C ++ 11.
Mr.C64

Відповіді:


389

Почнемо розрізняти спостереження за елементами в контейнері та змінювати їх на місці.

Спостереження за елементами

Розглянемо простий приклад:

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

Як це можна прочитати з результату, виклики конструктора копіювання здійснюються під час ітерацій циклу на основі діапазону.
Це тому, що ми захоплюємо елементи з контейнера за значенням ( 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)синтаксис працює і в інших випадках звичайних (не проксі) ітераторів (наприклад, для a 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 &&

Звичайно, якщо є необхідність зробити локальну копію елемента всередині корпусу циклу, захоплення значенням ( 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)

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 О, вибачте - я щойно помітив, що це одне з таких питань типу FAQ. Я новачок на цьому сайті. Вибачте! Ваша відповідь чудова, я її схвалив - але також хотів надати більш стисну версію для тих, хто хоче її суть . Сподіваюся, я не втручаюся.

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

Хоча початкова мотивація циклу діапазону може бути простою ітерацією над елементами контейнера, синтаксис є загальним, щоб бути корисним навіть для об'єктів, які не є суто контейнерами.

Синтаксична вимога для циклу 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 
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.