Apa cara yang benar untuk menggunakan rentang berbasis C ++ 11?

211

Apa cara yang benar untuk menggunakan rentang berbasis C ++ 11 for?

Sintaks apa yang harus digunakan? for (auto elem : container), for (auto& elem : container)atau for (const auto& elem : container)? Atau yang lainnya?

Mr.C64
sumber
6
Pertimbangan yang sama berlaku untuk argumen fungsi.
Maxim Egorushkin
3
Sebenarnya, ini tidak ada hubungannya dengan rentang berbasis untuk. Hal yang sama dapat dikatakan tentang apa pun auto (const)(&) x = <expr>;.
Matthieu M.
2
@MatthieuM: ini memiliki banyak hubungannya dengan berbagai berbasis untuk, tentu saja! Pertimbangkan seorang pemula yang melihat beberapa sintaksis dan tidak dapat memilih bentuk mana yang akan digunakan. Inti dari "T&J" adalah untuk mencoba menjelaskan, dan menjelaskan perbedaan dari beberapa kasus (dan mendiskusikan kasus-kasus yang mengkompilasi dengan baik tetapi agak tidak efisien karena salinan yang tidak berguna, dll.).
Mr.C64
2
@ Mr.C64: Sejauh yang saya ketahui, ini lebih banyak berkaitan dengan auto, secara umum, daripada berbasis range untuk; Anda dapat dengan sempurna menggunakan rentang berbasis tanpa auto! for (int i: v) {}baik-baik saja. Tentu saja, sebagian besar poin yang Anda angkat dalam jawaban Anda mungkin lebih berkaitan dengan jenis daripada dengan auto... tetapi dari pertanyaan itu tidak jelas di mana titik nyeri itu. Secara pribadi, saya akan bersaing untuk menghapus autodari pertanyaan; atau mungkin membuatnya secara eksplisit bahwa apakah Anda menggunakan autoatau secara eksplisit menyebutkan jenisnya, pertanyaannya difokuskan pada nilai / referensi.
Matthieu M.
1
@MatthieuM .: Saya terbuka untuk mengubah judul atau mengedit pertanyaan dalam beberapa bentuk yang dapat membuatnya lebih jelas ... Sekali lagi, fokus saya adalah untuk membahas beberapa opsi berbasis range untuk sintaks (menunjukkan kode yang mengkompilasi tetapi tidak efisien, kode yang gagal dikompilasi, dll.) dan mencoba menawarkan beberapa panduan kepada seseorang (terutama di tingkat pemula) mendekati kisaran C ++ 11 untuk loop.
Mr.C64

Jawaban:

389

Mari kita mulai membedakan antara mengamati elemen-elemen dalam wadah vs memodifikasi mereka di tempat.

Mengamati elemen-elemennya

Mari kita perhatikan contoh sederhana:

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

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

Kode di atas mencetak elemen intdi vector:

1 3 5 7 9

Sekarang pertimbangkan kasus lain, di mana elemen vektor bukan hanya bilangan bulat sederhana, tetapi contoh kelas yang lebih kompleks, dengan konstruktor salin ubahsuaian, dll.

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

Jika kita menggunakan for (auto x : v) {...}sintaks di atas dengan kelas baru ini:

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

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

outputnya seperti:

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

Karena dapat dibaca dari output, salin panggilan konstruktor dibuat selama range-based untuk iterasi loop.
Ini karena kita menangkap elemen dari wadah berdasarkan nilai ( auto xbagian dalam for (auto x : v)).

Ini adalah kode yang tidak efisien , misalnya, jika elemen-elemen ini adalah contoh std::string, alokasi memori tumpukan dapat dilakukan, dengan perjalanan mahal ke manajer memori, dll. Ini tidak berguna jika kita hanya ingin mengamati elemen-elemen dalam sebuah wadah.

Jadi, sintaks yang lebih baik tersedia: capture by constreference , yaitu const auto&:

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

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

Sekarang hasilnya adalah:

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

Elements:
1 3 5 7 9

Tanpa panggilan konstruktor salin palsu (dan berpotensi mahal).

Jadi, ketika mengamati unsur-unsur dalam wadah (yaitu, untuk akses read-only), sintaks berikut baik-baik saja untuk sederhana murah-ke-copy jenis, seperti int, double, dll .:

for (auto elem : container) 

Lain, menangkap dengan constreferensi lebih baik dalam kasus umum , untuk menghindari panggilan copy constructor yang tidak berguna (dan berpotensi mahal):

for (const auto& elem : container) 

Memodifikasi elemen dalam wadah

Jika kita ingin memodifikasi elemen dalam wadah menggunakan rentang berbasis for, sintaks di atas for (auto elem : container)dan for (const auto& elem : container)salah.

Bahkan, dalam kasus sebelumnya, elemmenyimpan salinan elemen asli, sehingga modifikasi yang dilakukan hanya hilang dan tidak disimpan secara terus-menerus dalam wadah, misalnya:

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

Output hanyalah urutan awal:

1 3 5 7 9

Sebaliknya, upaya menggunakan for (const auto& x : v)gagal untuk dikompilasi.

g ++ menampilkan pesan kesalahan seperti ini:

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

Pendekatan yang benar dalam hal ini diambil dengan non- constreferensi:

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

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

Outputnya (seperti yang diharapkan):

10 30 50 70 90

for (auto& elem : container)Sintaks ini juga berfungsi untuk tipe yang lebih kompleks, misalnya mempertimbangkan 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 << ' ';

outputnya adalah:

Hi Bob! Hi Jeff! Hi Connie!

Kasus khusus dari iterator proxy

Misalkan kita punya vector<bool>, dan kita ingin membalikkan keadaan logis boolean dari elemen-elemennya, menggunakan sintaks di atas:

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

Kode di atas gagal dikompilasi.

g ++ menampilkan pesan kesalahan yang mirip dengan ini:

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

Masalahnya adalah bahwa std::vectortemplate khusus untuk bool, dengan implementasi yang paket yang bools ruang mengoptimalkan (setiap nilai boolean disimpan dalam satu bit, delapan "boolean" bit dalam byte).

Karena itu (karena tidak mungkin untuk mengembalikan referensi ke satu bit), vector<bool>menggunakan pola yang disebut "proxy iterator" . "Proxy iterator" adalah iterator yang, ketika didereferensi, tidak menghasilkan objek biasa bool &, melainkan mengembalikan (berdasarkan nilai) objek sementara , yang merupakan kelas proxy yang dapat dikonversibool . (Lihat juga pertanyaan ini dan jawaban terkait di sini di StackOverflow.)

Untuk memodifikasi elemen tempat vector<bool>, jenis sintaks baru (menggunakan auto&&) harus digunakan:

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

Kode berikut berfungsi dengan baik:

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

dan output:

false true true false

Perhatikan bahwa for (auto&& elem : container)sintaks juga berfungsi dalam kasus lain iterator biasa (non-proxy) (misalnya untuk a vector<int>atau a vector<string>).

(Sebagai catatan, sintaks "mengamati" yang disebutkan di atas for (const auto& elem : container)berfungsi dengan baik juga untuk kasus proxy iterator.)

Ringkasan

Diskusi di atas dapat diringkas dalam pedoman berikut:

  1. Untuk mengamati elemen, gunakan sintaks berikut:

    for (const auto& elem : container)    // capture by const reference
    • Jika objek murah untuk disalin (seperti ints, doubles, dll.), Dimungkinkan untuk menggunakan formulir yang sedikit disederhanakan:

      for (auto elem : container)    // capture by value
  2. Untuk memodifikasi elemen di tempat, gunakan:

    for (auto& elem : container)    // capture by (non-const) reference
    • Jika wadah menggunakan "iterators proxy" (seperti std::vector<bool>), gunakan:

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

Tentu saja, jika ada kebutuhan untuk membuat salinan lokal dari elemen di dalam tubuh loop, menangkap dengan nilai ( for (auto elem : container)) adalah pilihan yang baik.


Catatan tambahan tentang kode generik

Dalam kode generik , karena kita tidak dapat membuat asumsi tentang tipe generik Tyang murah untuk disalin, dalam mode pengamatan aman untuk selalu digunakan for (const auto& elem : container).
(Ini tidak akan memicu salinan berpotensi tidak berguna yang mahal, akan berfungsi dengan baik juga untuk jenis yang murah untuk menyalin seperti int, dan juga untuk wadah yang menggunakan proxy-iterators, seperti std::vector<bool>.)

Selain itu, dalam mode modifikasi , jika kita ingin kode generik berfungsi juga dalam kasus proxy-iterator, opsi terbaik adalah for (auto&& elem : container).
(Ini akan berfungsi dengan baik juga untuk kontainer menggunakan non-proxy-iterators biasa, seperti std::vector<int>atau std::vector<string>.)

Jadi, dalam kode generik , pedoman berikut dapat diberikan:

  1. Untuk mengamati elemen, gunakan:

    for (const auto& elem : container)
  2. Untuk memodifikasi elemen di tempat, gunakan:

    for (auto&& elem : container)
Mr.C64
sumber
7
Tidak ada saran untuk konteks umum? :(
R. Martinho Fernandes
11
Kenapa tidak selalu menggunakan auto&&? Apakah ada const auto&&?
Martin Ba
1
Saya kira Anda kehilangan kasus di mana Anda benar-benar membutuhkan salinan di dalam loop?
juanchopanza
6
"Jika wadah menggunakan" iterators proxy "" - dan Anda tahu itu menggunakan "iterators proxy" (yang mungkin tidak terjadi dalam kode generik). Jadi saya pikir yang terbaik memang auto&&, karena mencakup auto&sama baiknya.
Christian Rau
5
Terima kasih, itu adalah "pengantar kursus kilat" yang benar-benar hebat untuk sintaksis dan beberapa kiat untuk kisaran berbasis untuk, untuk programmer C #. +1.
AndrewJacksonZA
17

Tidak ada cara yang benar untuk digunakan for (auto elem : container), atau for (auto& elem : container)atau for (const auto& elem : container). Anda hanya mengekspresikan apa yang Anda inginkan.

Biarkan saya menguraikan itu. Ayo jalan-jalan.

for (auto elem : container) ...

Ini adalah gula sintaksis untuk:

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

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

}

Anda dapat menggunakan ini jika wadah Anda mengandung elemen yang murah untuk disalin.

for (auto& elem : container) ...

Ini adalah gula sintaksis untuk:

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;

}

Gunakan ini ketika Anda ingin menulis ke elemen dalam wadah secara langsung, misalnya.

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

Ini adalah gula sintaksis untuk:

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

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

}

Seperti kata komentar, hanya untuk membaca. Dan itu saja, semuanya "benar" ketika digunakan dengan benar.


sumber
2
Saya bermaksud memberikan beberapa panduan, dengan kompilasi kode sampel (tetapi tidak efisien), atau gagal untuk mengkompilasi, dan menjelaskan mengapa, dan mencoba mengusulkan beberapa solusi.
Mr.C64
2
@ Mr.C64 Oh, maaf - saya baru memperhatikan bahwa ini adalah salah satu dari pertanyaan jenis-FAQ. Saya baru di situs ini. Permintaan maaf! Jawaban Anda luar biasa, saya membenarkannya - tetapi juga ingin memberikan versi yang lebih ringkas bagi mereka yang menginginkan intinya . Semoga aku tidak mengganggu.
1
@ Mr.C64 apa masalahnya dengan OP menjawab pertanyaan juga? Itu hanyalah jawaban yang valid.
mfontanini
1
@mfontanini: Sama sekali tidak ada masalah jika seseorang memposting beberapa jawaban, bahkan lebih baik dari saya. Tujuan akhirnya adalah untuk memberikan kontribusi yang berkualitas kepada masyarakat (terutama bagi pemula yang mungkin merasa agak tersesat di depan sintaksis yang berbeda dan opsi berbeda yang ditawarkan C ++).
Mr.C64
4

Cara yang benar selalu

for(auto&& elem : container)

Ini akan menjamin pelestarian semua semantik.

Anak anjing
sumber
6
Tetapi bagaimana jika wadah hanya mengembalikan referensi yang dapat dimodifikasi dan saya ingin menjelaskan bahwa saya tidak ingin memodifikasinya dalam loop? Tidakkah seharusnya saya gunakan auto const &untuk memperjelas maksud saya?
RedX
@RedX: Apa itu "referensi yang dapat dimodifikasi"?
Lightness Races dalam Orbit
2
@RedX: Referensi tidak pernah const, dan mereka tidak pernah bisa berubah. Bagaimanapun, jawaban saya untuk Anda adalah ya, saya akan melakukannya .
Lightness Races dalam Orbit
4
Meskipun ini mungkin berhasil, saya merasa ini adalah saran yang buruk dibandingkan dengan pendekatan yang lebih bernuansa dan dipertimbangkan yang diberikan oleh jawaban Mr.C64 yang luar biasa dan komprehensif yang diberikan di atas. Mengurangi ke penyebut yang paling tidak umum bukanlah untuk tujuan C ++.
Jack Aidley
6
Proposal evolusi bahasa ini setuju dengan jawaban "buruk" ini: open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3853.htm
Luc Hermitte
1

Sementara motivasi awal dari range-for loop mungkin adalah kemudahan iterasi atas elemen-elemen dari sebuah wadah, sintaksisnya cukup umum untuk berguna bahkan untuk objek yang bukan murni wadah.

Persyaratan sintaksis untuk for-loop adalah range_expressiondukungan itu begin()dan end()sebagai salah satu fungsi - baik sebagai fungsi anggota dari tipe yang dievaluasi atau sebagai fungsi non-anggota yang mengambil instance dari tipe tersebut.

Sebagai contoh yang dibuat-buat, seseorang dapat menghasilkan rentang angka dan beralih pada rentang menggunakan kelas berikut.

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

Dengan mainfungsi berikut ,

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

seseorang akan mendapatkan output berikut.

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
sumber