Artış ifadesi haricinde bir for döngüsü değişkeni const oluşturulur?


84

Döngü için bir standart düşünün:

for (int i = 0; i < 10; ++i) 
{
   // do something with i
}

Döngünün igövdesinde değişkenin değiştirilmesini önlemek istiyorum for.

Ancak, ben ilan edemeyeceğini iolarak constbu artış deyimi geçersiz Güncel yapar gibi. Artış ifadesinin dışında ibir constdeğişken oluşturmanın bir yolu var mı ?


4
Bunu yapmak için hiçbir yol yoktur inanıyoruz
Itay

27
Bu, bir problem arayışında bir çözüm gibi görünüyor.
Pete Becker

14
For-loop'unuzun gövdesini const int ibağımsız değişkenli bir işleve dönüştürün . Dizinin değişkenliği yalnızca gerektiği yerde açığa çıkar ve inlineanahtar kelimeyi derlenen çıktı üzerinde hiçbir etkisi olmamasını sağlamak için kullanabilirsiniz .
Monty Thibault

4
Sizden başka ne (veya daha doğrusu, kim) endeksin değerini değiştirebilir? Kendinize güvenmiyor musunuz? Belki bir iş arkadaşı? @PeteBecker'a katılıyorum.
Z4-katmanlı

5
@ Z4-tier Evet, elbette kendime güvenmiyorum. Hatalar yaptığımı biliyorum. Her iyi programcı bilir. Bu yüzden constbaşlamaktan hoşlanacak şeylerimiz var .
Konrad Rudolph

Yanıtlar:


120

C ++ 20'den, ranges :: views :: iota'yı şu şekilde kullanabilirsiniz :

for (int const i : std::views::iota(0, 10))
{
   std::cout << i << " ";  // ok
   i = 42;                 // error
}

İşte bir demo .


C ++ 11'den, bir IIILE (hemen çağrılan satır içi lambda ifadesi) kullanan aşağıdaki tekniği de kullanabilirsiniz:

int x = 0;
for (int i = 0; i < 10; ++i) [&,i] {
    std::cout << i << " ";  // ok, i is readable
    i = 42;                 // error, i is captured by non-mutable copy
    x++;                    // ok, x is captured by mutable reference
}();     // IIILE

İşte bir demo .

[&,i]Bunun i, değiştirilemeyen kopya tarafından yakalandığı ve diğer her şeyin değiştirilebilir referans tarafından yakalandığı anlamına geldiğini unutmayın . ();Döngünün sonunda sadece Lambda hemen çağrılır anlamına gelir.


Neredeyse özel bir döngü yapısı gerektirir, çünkü bu, çok, çok yaygın bir yapıya daha güvenli bir alternatiftir.
Michael Dorgan

2
@MichaelDorgan Artık bu özellik için kütüphane desteği olduğuna göre, onu temel dil özelliği olarak eklemeye değmez.
cigien

1
Adil, ancak neredeyse tüm gerçek çalışmalarım hala en fazla C veya C ++ 11. Gelecekte benim için önemli olması durumunda çalışıyorum ...
Michael Dorgan

9
Lambda ile eklediğiniz C ++ 11 hilesi düzgün, ancak bulunduğum çoğu iş yerinde pratik olmayacak. Statik analiz, genelleştirilmiş &yakalamadan şikayet eder ve bu da her bir referansı açıkça yakalamaya zorlar - bu da bunu oldukça yapar hantal. Ayrıca bunun, bir yazarın unuttuğu ()ve kodun asla çalıştırılmamasını sağlayan kolay hatalara yol açabileceğinden şüpheleniyorum . Bu, kod incelemesinde de gözden kaçabilecek kadar küçüktür.
Human-Compiler

1
@cigien SonarQube ve cppcheck gibi statik analiz araçları, [&]AUTOSAR (Kural A5-1-2), HIC ++ gibi kodlama standartlarıyla çeliştiği için genel yakalamaları işaretler ve ayrıca MISRA (emin değilim). Doğru olmadığı için değil; kuruluşların bu tür kodların standartlara uygun olmasını yasaklamasıdır. Gelince (), en yeni gcc sürümü bu bayrak gelmez bile -Wextra. Hala yaklaşımın düzgün olduğunu düşünüyorum; pek çok kuruluşta işe yaramıyor.
Human-Compiler

44

Cigien en seven herkes için std::views::iotabir cevap ancak yukarıda C ++ 20 çalışıp çalışmadığını, bu basitleştirilmiş ve hafif sürümünü uygulamak için oldukça basittir std::views::iotauyumlu ya da üzerinde.

Tek gereken şudur:

  • Bir integral değeri (ör. Bir ) saran temel bir " LegacyInputIterator " türü ( operator++ve tanımlayan bir şey )operator*int
  • Yukarıdaki yineleyicileri olan begin()ve end()döndüren bazı "aralık" benzeri sınıflar . Bu, menzile dayalı fordöngülerde çalışmasına izin verecektir.

Bunun basitleştirilmiş bir versiyonu şunlar olabilir:

#include <iterator>

// This is just a class that wraps an 'int' in an iterator abstraction
// Comparisons compare the underlying value, and 'operator++' just
// increments the underlying int
class counting_iterator
{
public:
    // basic iterator boilerplate
    using iterator_category = std::input_iterator_tag;
    using value_type = int;
    using reference  = int;
    using pointer    = int*;
    using difference_type = std::ptrdiff_t;

    // Constructor / assignment
    constexpr explicit counting_iterator(int x) : m_value{x}{}
    constexpr counting_iterator(const counting_iterator&) = default;
    constexpr counting_iterator& operator=(const counting_iterator&) = default;

    // "Dereference" (just returns the underlying value)
    constexpr reference operator*() const { return m_value; }
    constexpr pointer operator->() const { return &m_value; }

    // Advancing iterator (just increments the value)
    constexpr counting_iterator& operator++() {
        m_value++;
        return (*this);
    }
    constexpr counting_iterator operator++(int) {
        const auto copy = (*this);
        ++(*this);
        return copy;
    }

    // Comparison
    constexpr bool operator==(const counting_iterator& other) const noexcept {
        return m_value == other.m_value;
    }
    constexpr bool operator!=(const counting_iterator& other) const noexcept {
        return m_value != other.m_value;
    }
private:
    int m_value;
};

// Just a holder type that defines 'begin' and 'end' for
// range-based iteration. This holds the first and last element
// (start and end of the range)
// The begin iterator is made from the first value, and the
// end iterator is made from the second value.
struct iota_range
{
    int first;
    int last;
    constexpr counting_iterator begin() const { return counting_iterator{first}; }
    constexpr counting_iterator end() const { return counting_iterator{last}; }
};

// A simple helper function to return the range
// This function isn't strictly necessary, you could just construct
// the 'iota_range' directly
constexpr iota_range iota(int first, int last)
{
    return iota_range{first, last};
}

Yukarıdakileri constexprdesteklendiği yerle tanımladım , ancak C ++ 11/14 gibi önceki C ++ constexprsürümleri için, bunu yapmak için bu sürümlerde yasal olmayan yerleri kaldırmanız gerekebilir .

Yukarıdaki standart şablon, aşağıdaki kodun C ++ 20 öncesi ile çalışmasını sağlar:

for (int const i : iota(0, 10))
{
   std::cout << i << " ";  // ok
   i = 42;                 // error
}

Optimize edildiğinde C ++ 20 çözümü ve klasik döngü çözümü ile aynı montajı üretecektir .std::views::iotafor

Bu, herhangi bir C ++ 11 uyumlu derleyici ile çalışır (örneğin, derleyiciler gibi gcc-4.9.4) ve yine de temel bir döngü karşılığı ile neredeyse aynı derlemeyi üretir for.

Not:iota yardımcı fonksiyon sadece C ++ 20 özellik parite için std::views::iotaçözelti; ama gerçekçi olarak, aramak iota_range{...}yerine doğrudan bir de oluşturabilirsiniz iota(...). İlki, bir kullanıcı gelecekte C ++ 20'ye geçmek isterse kolay bir yükseltme yolu sunar.


3
Biraz standart metin gerektirir, ancak aslında ne yaptığı açısından o kadar da karmaşık değil. Aslında bu sadece basit bir yineleme kalıbı, ancak başlangıç int/ bitişi döndürmek için bir "aralık" sınıfı oluşturup bir "aralık" sınıfı oluşturuyor
Human-Compiler

1
Çok önemli değil, ama başka hiç kimsenin göndermediği bir c ++ 11 çözümü de ekledim, bu yüzden cevabınızın ilk satırını biraz değiştirmek
isteyebilirsiniz

Kimin olumsuz oy verdiğinden emin değilim, ancak cevabımın tatmin edici olmadığını düşünüyorsanız, iyileştirebilmem için biraz geri bildirim almaktan memnuniyet duyarım. Olumsuz oylama, bir cevabın soruyu yeterince ele almadığını düşündüğünüzü göstermenin harika bir yoludur, ancak bu durumda, cevapta iyileştirebileceğim mevcut eleştiriler veya bariz hatalar yoktur.
Human-Compiler

@ Human-Compiler Aynı zamanda bir DV de aldım ve neden ikisine de yorum yapmadılar :( Sanırım birisi aralık soyutlamalarını beğenmiyor. Bunun için endişelenmem.
cigien

1
"montaj", "bagaj" veya "su" gibi toplu bir isimdir. Normal cümleleme olurdu "aynı derlenir montaj ... C ++ 20 olarak". Tek bir işlev için derleyici'nın asm çıkış değil bir çok "montaj" var (assembly bir talimat dizisi) tekil düzeneği.
Peter Cordes

29

KISS versiyonu ...

for (int _i = 0; _i < 10; ++_i) {
    const int i = _i;

    // use i here
}

Kullanım durumunuz yalnızca döngü dizininin yanlışlıkla değiştirilmesini önlemek içinse, bu tür bir hatayı açık hale getirmelidir. ( Kasıtlı değişikliği önlemek istiyorsanız , iyi şanslar ...)


11
Sanırım ile başlayan sihirli tanımlayıcıları kullanmak için yanlış ders veriyorsunuz _. Ve biraz açıklama (örneğin kapsam) yardımcı olacaktır. Aksi takdirde, evet, güzelce KISSy.
Yunnosch

14
"Gizli" değişkeni çağırmak i_daha uyumlu olacaktır.
Yirkha

9
Bunun soruyu nasıl yanıtladığını bilmiyorum. Döngü değişkeni, döngüde _ihala değiştirilebilir olandır.
cigien

4
@cigien: IMO, bu kısmi çözüm std::views::iotatamamen kurşun geçirmez bir yol için C ++ 20 olmadan gitmeye değer . Cevabın metni, sınırlamalarını ve soruyu nasıl cevaplamaya çalıştığını açıklar. Çok karmaşık C ++ 11, okunması kolay, bakımı kolay IMO açısından tedaviyi hastalıktan daha kötü hale getiriyor. Bu, C ++ 'yı bilen herkes için okuması hala çok kolay ve bir deyim olarak makul görünüyor. (Ancak baştaki alt çizgi isimlerinden kaçınmalısınız.)
Peter Cordes

5
@Yunnosch yalnızca _Uppercaseve double__underscoretanımlayıcılar saklıdır. _lowercasetanımlayıcılar yalnızca genel kapsamda ayrılmıştır.
Roman Odaisky

13

For döngüsünüzün içeriğinin bir kısmını veya tamamını, i'yi const olarak kabul eden bir işlevde taşıyamaz mısınız?

Önerilen bazı çözümlerden daha az optimaldir, ancak mümkünse bunu yapmak oldukça basittir.

Düzenleme: Belirsiz olma eğiliminde olduğum için sadece bir örnek.

for (int i = 0; i < 10; ++i) 
{
   looper( i );
}

void looper ( const int v )
{
    // do your thing here
}

12

Erişiminiz yoksa , bir işlev kullanarak tipik makyaj

#include <vector>
#include <numeric> // std::iota

std::vector<int> makeRange(const int start, const int end) noexcept
{
   std::vector<int> vecRange(end - start);
   std::iota(vecRange.begin(), vecRange.end(), start);
   return vecRange;
}

şimdi yapabilirsin

for (const int i : makeRange(0, 10))
{
   std::cout << i << " ";  // ok
   //i = 100;              // error
}

( Bir Demoya bakın )


Güncelleme : @ Human-Compiler'ın yorumundan esinlenerek , verilen cevapların performans durumunda herhangi bir fark olup olmadığını merak ediyordum. Bu yaklaşım haricinde, diğer tüm yaklaşımlar için şaşırtıcı bir şekilde aynı performansa sahip olduğu ortaya çıktı (menzil için [0, 10)). std::vectorYaklaşım kötüsü.

görüntü açıklamasını buraya girin

( Çevrimiçi Hızlı Bench'e bakın )


4
Bu pre-c ++ 20 için çalışsa da, kullanımı gerektirdiğinden oldukça büyük miktarda ek yüke sahiptir vector. Aralık çok genişse bu kötü olabilir.
Human-Compiler

@ Human-Compiler: A std::vector, eğer aralık da küçükse, göreceli bir ölçekte oldukça korkunçtur ve bunun birçok kez çalışan küçük bir iç döngü olması gerekiyorsa çok kötü olabilir. Bazı derleyiciler (libc ++ ile clang gibi, ancak libstdc ++ değil), işlevden kaçmayan bir ayırmanın yeni / silinmesini optimize edebilir, ancak aksi takdirde bu, küçük, tamamen açılmış bir döngü ile new+ çağrısı arasındaki fark olabilir. deleteve belki de aslında bu hafızaya kaydediliyor.
Peter Cordes

IMO'nun küçük faydası, const içoğu durumda, onu ucuza getiren C ++ 20 yolları olmadan, ek yüke değmez. Özellikle derleyicinin her şeyi optimize etme olasılığını azaltan çalışma zamanı değişken aralıklarında.
Peter Cordes

10

Ve işte bir C ++ 11 sürümü:

for (int const i : {0,1,2,3,4,5,6,7,8,9,10})
{
    std::cout << i << " ";
    // i = 42; // error
}

İşte canlı demo


6
Maksimum sayı bir çalışma zamanı değeri tarafından kararlaştırılırsa bu ölçeklenmez.
Human-Compiler

12
@ Human-Compiler Listeyi istenen değere kadar genişletin ve tüm programınızı dinamik olarak yeniden derleyin;)
Monty Thibault

5
Durumun ne olduğundan bahsetmedin {..}. Bu özelliği etkinleştirmek için bir şeyler eklemeniz gerekir. Örneğin, uygun başlıklar eklemezseniz kodunuz kırılır : godbolt.org/z/esbhra . Üzerinde Geçiş <iostream>diğer başlıklar için kötü bir fikir!
JeJo

6
#include <cstdio>
  
#define protect(var) \
  auto &var ## _ref = var; \
  const auto &var = var ## _ref

int main()
{
  for (int i = 0; i < 10; ++i) 
  {
    {
      protect(i);
      // do something with i
      //
      printf("%d\n", i);
      i = 42; // error!! remove this and it compiles.
    }
  }
}

Not: Dildeki şaşırtıcı bir aptallık nedeniyle kapsamı iç içe geçirmemiz gerekir: for(...)başlıkta bildirilen değişken, {...}bileşik ifadede belirtilen değişkenlerle aynı iç içe geçme seviyesinde kabul edilir . Bu, örneğin şu anlama gelir:

for (int i = ...)
{
  int i = 42; // error: i redeclared in same scope
}

Ne? Kıvırcık ayracı açmamış mıydık? Üstelik tutarsız:

void fun(int i)
{
  int i = 42; // OK
}

1
Bu kolayca en iyi cevaptır. Tanımlayıcının, orijinal adım değişkenine referans veren bir const ref değişkenine çözümlenmesine neden olmak için C ++ 'ın' değişken gölgelemesinden 'yararlanmak, zarif bir çözümdür. Ya da en azından, mevcut en zarif olanı.
Max Barraclough

4

C ++ 'ın herhangi bir sürümünde çalışan, burada henüz bahsedilmeyen basit bir yaklaşım std::for_each, yineleyicilerde yapılana benzer şekilde, bir aralık etrafında işlevsel bir sarmalayıcı oluşturmaktır . Kullanıcı daha sonra işlevsel bir argümanı her yinelemede çağrılacak bir geri arama olarak iletmekten sorumludur.

Örneğin:

// A struct that holds the start and end value of the range
struct numeric_range
{
    int start;
    int end;

    // A simple function that wraps the 'for loop' and calls the function back
    template <typename Fn>
    void for_each(const Fn& fn) const {
        for (auto i = start; i < end; ++i) {
            const auto& const_i = i;
            fn(const_i);
        }
    }
};

Kullanımın nerede olacağı:

numeric_range{0, 10}.for_each([](const auto& i){
   std::cout << i << " ";  // ok
   //i = 100;              // error
});

C ++ 11'den daha eski herhangi bir şey , kesin olarak adlandırılmış bir işlev göstericisini for_each(benzerine std::for_each) geçirirken takılı kalır , ancak yine de çalışır.

İşte bir demo


Bu, C ++ 'for daki döngüler için deyimsel olmasa da , bu yaklaşım diğer dillerde oldukça yaygındır. İşlevsel sarmalayıcılar, karmaşık ifadelerde birleştirilebilirlikleri açısından gerçekten şıktır ve kullanım için çok ergonomik olabilir.

Bu kodun yazılması, anlaşılması ve bakımı da kolaydır.


Bu yaklaşımın farkında olunması gereken bir sınırlama, bazı kuruluşların lambdalarda (örneğin [&]veya [=]) belirli güvenlik standartlarıyla uyumlu olması için varsayılan yakalamaları yasaklamasıdır ; bu, lambdayı her üyenin manuel olarak yakalanması gerektiğinde şişirebilir. Tüm kuruluşlar bunu yapmaz, bu yüzden bunu yanıt olarak değil sadece yorum olarak belirtiyorum.
Human-Compiler

0
template<class T = int, class F>
void while_less(T n, F f, T start = 0){
    for(; start < n; ++start)
        f(start);
}

int main()
{
    int s = 0;
    
    while_less(10, [&](auto i){
        s += i;
    });
    
    assert(s == 45);
}

belki ara for_i

Ek yük yok https://godbolt.org/z/e7asGj

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.