Jak zrobić stałą zmienną pętli for z wyjątkiem instrukcji inkrementacji?

84

Rozważ standard pętli:

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

Chcę, aby zmienna nie ibyła modyfikowana w treści forpętli.

Nie mogę jednak zadeklarować, iponieważ constpowoduje to unieważnienie instrukcji inkrementacji. Czy istnieje sposób, aby ina constzewnątrz zmienny rachunku przyrost?

jhourback
źródło
4
Uważam, że nie ma sposobu, aby to zrobić
Itay
27
To brzmi jak rozwiązanie w poszukiwaniu problemu.
Pete Becker
14
Zamień ciało pętli for w funkcję z const int iargumentem. Zmienność indeksu jest ujawniana tylko wtedy, gdy jest potrzebna, i można użyć inlinesłowa kluczowego, aby nie miało wpływu na skompilowane dane wyjściowe.
Monty Thibault
4
Co (a raczej kto) mógłby ewentualnie zmienić wartość indeksu poza… Tobą? Czy nie ufasz sobie? Może współpracownik? Zgadzam się z @PeteBecker.
Poziom Z4
5
@ Z4-tier Tak, oczywiście nie ufam sobie. Wiem, że popełniam błędy. Każdy dobry programista wie. Dlatego na constpoczątek mamy takie rzeczy .
Konrad Rudolph

Odpowiedzi:

120

Od c ++ 20 możesz używać zakresów :: views :: iota w następujący sposób:

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

Oto demo .


W języku c ++ 11 można również użyć następującej techniki, która używa IIILE (natychmiast wywołane wbudowane wyrażenie lambda):

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

Oto demo .

Zauważ, że [&,i]oznacza to, że ijest przechwytywane przez niemodyfikowalną kopię, a wszystko inne jest przechwytywane przez zmienne odniesienie. Na ();końcu pętli oznacza po prostu, że lambda jest wywoływana natychmiast.

cigien
źródło
Prawie wymaga specjalnej konstrukcji pętli for, ponieważ to, co oferuje, jest bezpieczniejszą alternatywą dla bardzo, bardzo powszechnej konstrukcji.
Michael Dorgan
2
@MichaelDorgan Cóż, teraz, gdy istnieje obsługa biblioteki dla tej funkcji, nie warto dodawać jej jako podstawowej funkcji języka.
cigien
1
W porządku, chociaż prawie cała moja prawdziwa praca to nadal co najwyżej C lub C ++ 11. Studiuję na wypadek, gdyby miało to dla mnie znaczenie w przyszłości ...
Michael Dorgan,
9
Sztuczka C ++ 11, którą dodałeś z lambdą, jest zgrabna, ale nie byłaby praktyczna w większości miejsc pracy, w których byłem. Analiza statyczna narzekałaby na uogólnione &przechwytywanie, które wymusiłoby jawne przechwycenie każdego odniesienia - co czyni to dość nieporęczny. Podejrzewam również, że może to prowadzić do łatwych błędów, w których autor zapomina o (), co sprawia, że ​​kod nigdy nie zostanie wywołany. Jest to na tyle małe, że można je pominąć również podczas przeglądu kodu.
Human-Compiler
1
@cigien Narzędzia do analizy statycznej, takie jak SonarQube i cppcheck, przechwytują ogólne informacje, [&]ponieważ są one sprzeczne ze standardami kodowania, takimi jak AUTOSAR (Rule A5-1-2), HIC ++ i myślę, że także MISRA (nie jestem pewien). Nie chodzi o to, że nie jest poprawne; chodzi o to, że organizacje zakazują tego typu kodu, aby był zgodny ze standardami. Jeśli chodzi o (), najnowsza wersja gcc nie oznacza tego nawet z -Wextra. Nadal uważam, że podejście jest zgrabne; po prostu nie działa w wielu organizacjach.
Human-Compiler
44

Dla każdego, kto lubi std::views::iotaodpowiedź Cigiena, ale nie działa w C ++ 20 lub nowszym, wdrożenie uproszczonej i lekkiej wersji std::views::iotakompatybilnego lub wyższy.

Wystarczy:

  • Podstawowy typ „ LegacyInputIterator ” (coś, co definiuje operator++i operator*), który otacza wartość całkowitą (np. int)
  • Pewna klasa podobna do „zakresu”, która ma begin()i end()zwraca powyższe iteratory. Umożliwi to pracę w forpętlach opartych na zasięgu

Uproszczona wersja może wyglądać następująco:

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

Powyżej zdefiniowałem, constexprgdzie jest obsługiwane, ale w przypadku wcześniejszych wersji C ++, takich jak C ++ 11/14, może być konieczne usunięcie, constexprjeśli nie jest to legalne w tych wersjach, aby to zrobić.

Powyższy schemat umożliwia działanie następującego kodu w wersji przed C ++ 20:

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

Który wygeneruje ten sam zestaw co rozwiązanie C ++ 20 std::views::iotai klasyczne forrozwiązanie pętli po optymalizacji.

Działa to z dowolnymi kompilatorami zgodnymi z C ++ 11 (np. Kompilatorami takimi jak gcc-4.9.4) i nadal tworzy prawie identyczny zestaw z podstawowym forodpowiednikiem -loop.

Uwaga: Funkcja iotapomocnicza służy tylko do zapewnienia parzystości funkcji w rozwiązaniu C ++ 20 std::views::iota; ale realistycznie, możesz również bezpośrednio skonstruować iota_range{...}zamiast dzwonić iota(...). Ten pierwszy przedstawia po prostu łatwą ścieżkę aktualizacji, jeśli użytkownik chce przejść na C ++ 20 w przyszłości.

Człowiek-kompilator
źródło
3
Wymaga trochę schematu, ale w rzeczywistości nie jest to aż tak skomplikowane, jeśli chodzi o to, co robi. W rzeczywistości jest to tylko podstawowy wzorzec iteratora, ale opakowywanie int, a następnie tworzenie klasy „range”, aby zwrócić początek / koniec
Human-Compiler
1
Nie jest to super ważne, ale dodałem również rozwiązanie C ++ 11, którego nikt inny nie opublikował, więc możesz nieco
przeformułować
Nie jestem pewien, kto zagłosował przeciw, ale byłbym wdzięczny za informację zwrotną, jeśli uważasz, że moja odpowiedź jest niezadowalająca, abym mógł ją poprawić. Głosowanie w dół to świetny sposób na pokazanie, że według Ciebie odpowiedź nie odpowiada adekwatnie do pytania, ale w tym przypadku nie ma żadnej krytyki ani oczywistych błędów w odpowiedzi, które mogę poprawić.
Human-Compiler,
@ Human-Compiler W tym samym czasie dostałem także DV, a oni też nie komentowali, dlaczego :( Chyba komuś nie podobają się abstrakcje zakresu. Nie
martwiłbym się
1
„zgromadzenie” to rzeczownik masy, taki jak „bagaż” lub „woda”. Normalne sformułowanie brzmiałoby: „zostanie skompilowany do tego samego zestawu, co C ++ 20…”. Wyjście asm kompilatora dla pojedynczej funkcji nie jest pojedynczym zestawem, jest to „assembler” (sekwencja instrukcji asemblera).
Peter Cordes
29

Wersja KISS ...

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

    // use i here
}

Jeśli twój przypadek użycia ma na celu zapobieżenie przypadkowej modyfikacji indeksu pętli, powinno to uczynić taki błąd oczywistym. (Jeśli chcesz zapobiec celowym modyfikacjom, cóż, powodzenia ...)

Artelius
źródło
11
Myślę, że uczysz niewłaściwej lekcji używania magicznych identyfikatorów, które zaczynają się od _. Przydałoby się trochę wyjaśnienia (np. Zakres). W przeciwnym razie tak, ładnie KISSy.
Yunnosch
14
Wywołanie zmiennej „ukrytej” i_byłoby bardziej zgodne.
Yirkha
9
Nie jestem pewien, jak to odpowiada na pytanie. Zmienna pętli _ijest nadal modyfikowalna w pętli.
cigien
4
@cigien: IMO, to częściowe rozwiązanie jest na tyle warte, że bez C ++ 20 std::views::iotajest w pełni kuloodporne. Tekst odpowiedzi wyjaśnia jego ograniczenia i sposób, w jaki próbuje odpowiedzieć na pytanie. Kilka zbyt skomplikowanych C ++ 11 sprawia, że ​​lekarstwo jest gorsze niż choroba, jeśli chodzi o łatwe do odczytania i łatwe w utrzymaniu IMO. Jest to nadal bardzo łatwe do odczytania dla każdego, kto zna C ++ i wydaje się rozsądne jako idiom. (Ale należy unikać nazw z czołowym podkreśleniem.)
Peter Cordes
5
Tylko @Yunnosch _Uppercasei double__underscoreidentyfikatory są zastrzeżone. _lowercaseidentyfikatory są zarezerwowane tylko w zakresie globalnym.
Roman Odaisky
13

Czy nie mógłbyś po prostu przenieść części lub całej zawartości pętli for w funkcji, która akceptuje i jako stałą?

Jest to mniej optymalne niż niektóre proponowane rozwiązania, ale jeśli to możliwe, jest to dość proste.

Edycja: tylko przykład, ponieważ wydaje mi się być niejasny.

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

void looper ( const int v )
{
    // do your thing here
}
Al rl
źródło
12

Jeśli nie masz dostępu do , typowa przeróbka za pomocą funkcji

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

teraz możesz

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

( Zobacz demo )


Aktualizacja : Zainspirowany komentarzem @ Human-Compiler , zastanawiałem się, czy podane odpowiedzi mają jakąkolwiek różnicę w przypadku wydajności. Okazuje się, że poza tym podejściem, dla wszystkich innych podejść zaskakująco mają takie same wyniki (dla zakresu [0, 10)). Plikstd::vectorPodejście jest najgorsze.

wprowadź opis obrazu tutaj

( Zobacz podręczną ławkę online )

JeJo
źródło
4
Chociaż działa to dla pre-c ++ 20, wiąże się to z dość dużym obciążeniem, ponieważ wymaga użycia vector. Jeśli zasięg jest bardzo duży, może to być złe.
Human-Compiler
@ Human-Compiler: A std::vectorjest dość okropne w skali względnej, jeśli zakres jest również mały, i mogłoby być bardzo złe, gdyby to miała być mała wewnętrzna pętla, która działała wiele razy. Niektóre kompilatory (takie jak clang z libc ++, ale nie libstdc ++) mogą zoptymalizować nowy / usunąć alokację, która nie ucieka z funkcji, ale w przeciwnym razie może to łatwo być różnicą między małą w pełni rozwiniętą pętlą a wywołaniem do new+ deletei może faktycznie zapisując w tej pamięci.
Peter Cordes
IMO, niewielka korzyść z tego const ipo prostu nie jest warta narzutów w większości przypadków, bez C ++ 20 sposobów, które sprawiają, że jest tani. Zwłaszcza w przypadku zakresów zmiennych środowiska uruchomieniowego, które zmniejszają prawdopodobieństwo optymalizacji wszystkiego przez kompilator.
Peter Cordes
10

A oto wersja C ++ 11:

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

Oto demo na żywo

Vlad Feinstein
źródło
6
Nie jest to skalowane, jeśli o maksymalnej liczbie decyduje wartość czasu wykonywania.
Human-Compiler
12
@ Human-Compiler Po prostu rozszerz listę do żądanej wartości i przekompiluj dynamicznie cały program;)
Monty Thibault
5
Nie wspomniałeś, o co chodzi {..}. Musisz coś dołączyć, aby ta funkcja była aktywna. Na przykład, twój kod się zepsuje, jeśli nie dodasz odpowiednich nagłówków: godbolt.org/z/esbhra . Przekazywanie <iostream>innych nagłówków to zły pomysł!
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.
    }
  }
}

Uwaga: musimy zagnieździć zakres ze względu na zdumiewającą głupotę języka: zmienna zadeklarowana w for(...)nagłówku jest uważana za znajdującą się na tym samym poziomie zagnieżdżenia, co zmienne zadeklarowane w instrukcji {...}złożonej. Oznacza to na przykład, że:

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

Co? Czy nie otworzyliśmy właśnie kędzierzawego aparatu? Co więcej, jest to niespójne:

void fun(int i)
{
  int i = 42; // OK
}
Kaz
źródło
1
To z pewnością najlepsza odpowiedź. Wykorzystanie „cieniowania zmiennych” w C ++, aby spowodować, że identyfikator zostanie rozwiązany do zmiennej const ref odwołującej się do oryginalnej zmiennej krokowej, jest eleganckim rozwiązaniem. A przynajmniej najbardziej elegancki z dostępnych.
Max Barraclough
4

Jednym prostym podejściem, które nie zostało tu jeszcze wspomniane, które działa w dowolnej wersji C ++, jest utworzenie funkcjonalnego opakowania wokół zakresu, podobnego do tego, co std::for_each w przypadku iteratorów. Użytkownik jest wtedy odpowiedzialny za przekazanie argumentu funkcjonalnego jako wywołania zwrotnego, które będzie wywoływane w każdej iteracji.

Na przykład:

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

Gdzie zastosowanie byłoby:

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

Wszystko starsze niż C ++ 11 utknęłoby, przekazując silnie nazwany wskaźnik funkcji do for_each(podobnie dostd::for_each ), ale nadal działa.

Oto demo


Chociaż może to nie być idiomatyczne dla forpętli w C ++ , to podejście jest dość powszechne w innych językach. Funkcjonalne opakowania są naprawdę eleganckie ze względu na możliwość komponowania się w złożone oświadczenia i mogą być bardzo ergonomiczne w użyciu.

Ten kod jest również łatwy do napisania, zrozumienia i utrzymania.

Człowiek-kompilator
źródło
Jedynym ograniczeniem, o którym należy pamiętać w przypadku tego podejścia, jest to, że niektóre organizacje zakazują przechwytywania domyślnego na lambdach (np. [&]Lub [=]), aby zachować zgodność z określonymi standardami bezpieczeństwa, co może spowodować nadmuchanie lambda, gdy każdy element członkowski będzie musiał zostać przechwycony ręcznie. Nie wszystkie organizacje to robią, więc wymieniam to tylko jako komentarz, a nie w odpowiedzi.
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);
}

może to nazwać for_i

Bez kosztów https://godbolt.org/z/e7asGj

Hrisip
źródło