C ++ 11'in aralık tabanlı kullanımının doğru yolu nedir?


211

C ++ 11'in menzil tabanlı kullanmanın doğru yolu nedir for?

Hangi sözdizimi kullanılmalıdır? for (auto elem : container), veya for (auto& elem : container)veya for (const auto& elem : container)? Yoksa başka mı?


6
İşlev argümanları için de aynı husus geçerlidir.
Maxim Egorushkin

3
Aslında, bunun menzil tabanlı ile ilgisi yoktur. Aynı şey herhangi biri için de söylenebilir auto (const)(&) x = <expr>;.
Matthieu M.10

2
@MatthieuM: Bu sahiptir sürü ile ilgisi elbette için aralık tabanlı! Birkaç sözdizimi gören ve hangi formu kullanacağını seçemeyen bir acemi düşünün. “Soru-Cevap” konusu, biraz ışık tutmaya ve bazı vakaların farklılıklarını açıklamaya çalışmaktı (ve iyi derlenen ancak gereksiz derin kopyalar nedeniyle bir tür verimsiz vakaları tartışmak).
Mr.C64

2
@ Mr.C64: Bana göre, bunun autogenel olarak menzil tabanlı olmaktan çok; aralık tabanlı herhangi bir olmadan mükemmel kullanabilirsiniz auto! for (int i: v) {}gayet iyi. Elbette, cevabınızda ortaya koyduğunuz noktaların çoğunun türle ilgisi daha fazla olabilir auto... ancak sorudan acı noktasının nerede olduğu açık değildir. Şahsen, autosorudan kaldırmak için yarışırdım ; ya da autotürü kullansanız da açıkça adlandırsanız da sorunun değer / referansa odaklandığını açıkça belirtin .
Matthieu M.Nisan

1
@MatthieuM .: Başlığı değiştirmek ya da soruyu daha net hale getirebilecek bir biçimde düzenlemek için açığım ... Yine, odak noktam, sözdizimleri için aralık tabanlı için çeşitli seçenekleri tartışmaktı (derleyen ancak verimsiz, derlenemeyen kod vb.) ve döngüler için C ++ 11 aralığına yaklaşan birine (özellikle başlangıç ​​seviyesinde) bazı rehberlik sunmaya çalışmak.
Mr.C64

Yanıtlar:


389

Kaptaki öğeleri gözlemlemek ve bunları yerinde değiştirmek arasında ayrım yapmaya başlayalım .

Elemanları gözlemleme

Basit bir örneği ele alalım:

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

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

Yukarıdaki kod elemanlarını (basar ints) vector:

1 3 5 7 9

Şimdi, vektör öğelerinin sadece basit tamsayılar değil, özel kopya oluşturucu vb. İle daha karmaşık bir sınıfın örnekleri olduğu başka bir durumu düşünün.

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

Yukarıdaki for (auto x : v) {...}sözdizimini bu yeni sınıfla kullanırsak:

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

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

çıktı şöyle bir şeydir:

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

Çıktıdan okunabildiğinden, döngü yinelemeleri için aralık tabanlı sırasında kopya yapıcı çağrıları yapılır.
Biz Bunun nedeni, yakalama kaptan elemanları değeri ( auto xkısmı içindefor (auto x : v) ).

Bu, verimsiz bir koddur, örneğin, bu elemanların örnekleri ise std::string, yığın bellek tahsisi yapılabilir, bellek yöneticisine pahalı geziler vb. Yapılabilir. Bu sadece bir kaptaki elemanları gözlemlemek istiyorsak işe yaramaz .

Yani, daha iyi bir sözdizimi mevcuttur: Yakalama tarafından constreferans , yani const auto&:

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

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

Şimdi çıktı:

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

Elements:
1 3 5 7 9

Sahte (ve potansiyel olarak pahalı) bir kopya oluşturucu çağrısı olmadan.

Yani, gözlemleyerek bir kap (yani salt okunur erişim için) elementler, aşağıdaki sözdizimi basit için gayet ucuz-to-kopya gibi türleri int, doublevb .:

for (auto elem : container) 

Aksi takdirde , işe yaramaz (ve potansiyel olarak pahalı) kopya yapıcı çağrılarını önlemek constiçin referans olarak yakalama genel durumda daha iyidir :

for (const auto& elem : container) 

Kaptaki öğeleri değiştirme

Kapsayıcıdaki öğeleri aralık tabanlı kullanarak değiştirmek istiyorsak for, yukarıdaki for (auto elem : container)ve for (const auto& elem : container) sözdizimleri yanlıştır.

Aslında, önceki durumda, orijinal öğenin elembir kopyasını saklar , böylece üzerinde yapılan değişiklikler kaybolur ve kapta kalıcı olarak saklanmaz, örneğin:

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

Çıktı sadece başlangıç ​​dizisidir:

1 3 5 7 9

Bunun yerine, kullanma girişimi for (const auto& x : v)derlenemez.

g ++ şöyle bir hata mesajı verir:

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

Bu durumda doğru yaklaşım constreferanssız olarak yakalanmaktadır :

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

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

Çıktı (beklendiği gibi):

10 30 50 70 90

Bu for (auto& elem : container)sözdizimi daha karmaşık türler için de kullanılabilir, örneğin 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 << ' ';

çıktı:

Hi Bob! Hi Jeff! Hi Connie!

Proxy yineleyicilerinin özel durumu

Varsayalım vector<bool>ve yukarıdaki sözdizimini kullanarak öğelerinin mantıksal boole durumunu ters çevirmek istiyoruz:

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

Yukarıdaki kod derlenemedi.

g ++ aşağıdakine benzer bir hata iletisi verir:

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

Sorun, std::vectorşablonun alanı optimize etmek için s paketleyen bir uygulamayla (her boolean değeri bir bitte, sekiz "boolean" biti bir baytta saklanması) için uzman olmasıdır .boolbool

(Tek bit başvuru döndürmek mümkün değildir beri) Bu nedenle, vector<bool>sözde kullanır "vekil yineleyici" desen. "Proxy yineleyici", kayıttan çıkarıldığında sıradan bir sonuç vermeyenbool & , bunun yerine dönüştürülebilir bir proxy sınıfı olan geçici bir nesneyi (değere göre) döndüren bir yineleyicidir . (Ayrıca bu soruya ve ilgili cevaplara bakınız)bool StackOverflow'daki .)

Öğelerini yerinde değiştirmek vector<bool>için yeni bir tür sözdizimi (kullanma auto&&) kullanılmalıdır:

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

Aşağıdaki kod iyi çalışıyor:

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

ve çıktılar:

false true true false

for (auto&& elem : container)Sözdiziminin diğer sıradan (proxy olmayan) yineleyicilerde de işe yaradığını unutmayın (örneğin a vector<int>veya avector<string> ).

(Bir yan not olarak, yukarıda belirtilen "gözlemleme" sözdizimi for (const auto& elem : container), proxy yineleyici durumu için de işe yarar.)

özet

Yukarıdaki tartışma aşağıdaki kılavuzlarda özetlenebilir:

  1. İçin gözlem elemanları aşağıdaki sözdizimini kullanın:

    for (const auto& elem : container)    // capture by const reference
    • Nesnelerin kopyalanması ucuzsa ( ints, doubles vb.), Biraz basitleştirilmiş bir form kullanmak mümkündür:

      for (auto elem : container)    // capture by value
  2. İçin modifiye yer, kullanımda unsurları:

    for (auto& elem : container)    // capture by (non-const) reference
    • Kapsayıcı "proxy yineleyicileri" kullanıyorsa (gibi std::vector<bool>), şunu kullanın:

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

Elbette , öğenin döngü gövdesi içinde yerel bir kopyasını yapmaya ihtiyaç varsa , değer ( for (auto elem : container)) ile yakalamak iyi bir seçimdir.


Genel kod hakkında ek notlar

Gelen jenerik kod biz genel tür hakkında varsayımlar yapamazsınız çünkü Tkopyasına ucuz olma, içinde gözlemleyerek modunu her zaman kullanmak güvenlidir for (const auto& elem : container).
(Bu, potansiyel olarak pahalı yararsız kopyaları tetiklemez, aynı zamanda kopyalanması ucuz türler intiçin ve ayrıca proxy yineleyicileri kullanan kaplar için de iyi çalışır std::vector<bool>.)

Ayrıca, mod değiştirme modunda, genel kodun proxy yineleyiciler durumunda da çalışmasını istiyorsak , en iyi seçenek for (auto&& elem : container).
(Bu, std::vector<int>veya gibi sıradan proxy olmayan yineleyiciler kullanan kaplar için de iyi çalışır std::vector<string>.)

Dolayısıyla, genel kodda aşağıdaki yönergeler sağlanabilir:

  1. İçin gözlem elemanları, kullanın:

    for (const auto& elem : container)
  2. İçin modifiye yer, kullanımda unsurları:

    for (auto&& elem : container)

7
Genel bağlamlar için tavsiye yok mu? :(
R. Martinho Fernandes

11
Neden her zaman kullanılmıyor auto&&? Var mı const auto&&?
Martin Ba

1
Sanırım aslında döngü içinde bir kopyaya ihtiyacınız olan davayı kaçırıyorsunuz?
juanchopanza

6
Vekil yineleyiciler " 'kapsayıcı kullandığı takdirde'" - ve sen biliyorsun (jenerik kodunda durum olmayabilir) vekil yineleyicinızı "kullandığı". Bu yüzden bence en iyisi, eşit derecede iyi auto&&kapsadığı için auto&.
Christian Rau

5
Teşekkür ederim, bu sözdizimi için gerçekten büyük bir "kurs kursu giriş" ve bir C # programcı için, aralık için bazı ipuçları oldu. +1.
AndrewJacksonZA

17

Diye bir şey yok doğru yolu kullanmak for (auto elem : container)veya, for (auto& elem : container)ya da for (const auto& elem : container). Sadece ne istediğini ifade edersin.

Bunu biraz açıklayayım. Hadi bir gezintiye çıkalım.

for (auto elem : container) ...

Bu şunun için sözdizimsel şeker:

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

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

}

Kabınız kopyalanması ucuz öğeler içeriyorsa bunu kullanabilirsiniz.

for (auto& elem : container) ...

Bu şunun için sözdizimsel şeker:

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;

}

Örneğin, kaptaki öğelere doğrudan yazmak istediğinizde bunu kullanın.

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

Bu şunun için sözdizimsel şeker:

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

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

}

Yorumun dediği gibi, sadece okumak için. Ve bununla ilgili, düzgün kullanıldığında her şey "doğru".


2
Örnek kodları derleyerek (ancak verimsiz) veya derlemeyi başaramadığını ve nedenini açıklayarak biraz rehberlik sağlamayı ve bazı çözümler önermeyi amaçladım.
Mr.C64

2
@ Mr.C64 Oh, özür dilerim - bunun bu SSS tipi sorulardan biri olduğunu fark ettim. Bu sitede yeniyim. Özür! Cevabınız harika, ben bunu iptal ettim - ama aynı zamanda bunun özünü isteyenler için daha kısa bir versiyon sunmak istedim . Umarım izinsiz girmem.

1
@ Mr.C64 OP'nin soruyu cevaplamasında da sorun nedir? Bu sadece başka, geçerli bir cevap.
mfontanini

1
@mfontanini: Birisi benimkinden bile daha iyi bir cevap gönderirse kesinlikle sorun yok. Nihai amaç topluluğa kaliteli bir katkı sağlamaktır (özellikle C ++ 'nın sunduğu sözdizimleri ve farklı seçeneklerin önünde kaybolan yeni başlayanlar için).
Mr.C64

4

Doğru araçlar her zaman

for(auto&& elem : container)

Bu, tüm anlambilimin korunmasını garanti eder.


6
Ancak kap yalnızca değiştirilebilir başvuruları döndürürse ve döngüde bunları değiştirmek istemediğimi netleştirmek istersem ne olur? O zaman auto const &niyetimi netleştirmek için kullanmamalı mıyım ?
RedX

@RedX: "Değiştirilebilir referans" nedir?
Yörüngedeki Hafiflik Yarışları

2
@RedX: Referanslar asla const, değişmezler . Her neyse, sana cevabım evet, isterim .
Yörüngedeki Hafiflik Yarışları

4
Bu işe yarayabilir olsa da, bunun Mr.C64'ün yukarıda verilen mükemmel ve kapsamlı cevabının verdiği daha incelikli ve düşünülmüş yaklaşıma kıyasla zayıf bir tavsiye olduğunu hissediyorum. En az ortak paydaya düşürmek C ++ için değildir.
Jack Aidley

6
Bu dil evrimi önerisi bu "zayıf" yanıtı kabul eder: open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3853.htm
Luc

1

Range-for döngüsünün ilk motivasyonu, bir kabın elemanları üzerinde yineleme kolaylığı olsa da, sözdizimi, yalnızca kap olmayan nesneler için bile kullanışlı olacak kadar geneldir.

For-loop'un sözdizimsel gereksinimi, range_expressiondestek begin()veend() her iki işlev olarak - değerlendirdiği türün üye işlevleri olarak veya türün bir örneğini alan üye olmayan işlevler olarak işlevidir.

Bir örnek olarak, bir sayı aralığı üretilebilir ve aşağıdaki sınıfı kullanarak aralık üzerinde yineleme yapılabilir.

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

Aşağıdaki mainişlevle,

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

biri aşağıdaki çıktıyı alır.

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 
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.