Rozważ standard pętli:
for (int i = 0; i < 10; ++i)
{
// do something with i
}
Chcę, aby zmienna nie i
była modyfikowana w treści for
pętli.
Nie mogę jednak zadeklarować, i
ponieważ const
powoduje to unieważnienie instrukcji inkrementacji. Czy istnieje sposób, aby i
na const
zewnątrz zmienny rachunku przyrost?
const int i
argumentem. Zmienność indeksu jest ujawniana tylko wtedy, gdy jest potrzebna, i można użyćinline
słowa kluczowego, aby nie miało wpływu na skompilowane dane wyjściowe.const
początek mamy takie rzeczy .Odpowiedzi:
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, żei
jest 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.źródło
&
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.[&]
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.Dla każdego, kto lubi
std::views::iota
odpowiedź Cigiena, ale nie działa w C ++ 20 lub nowszym, wdrożenie uproszczonej i lekkiej wersjistd::views::iota
kompatybilnegoc ++ 11 lub wyższy.Wystarczy:
operator++
ioperator*
), który otacza wartość całkowitą (np.int
)begin()
iend()
zwraca powyższe iteratory. Umożliwi to pracę wfor
pętlach opartych na zasięguUproszczona 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,
constexpr
gdzie jest obsługiwane, ale w przypadku wcześniejszych wersji C ++, takich jak C ++ 11/14, może być konieczne usunięcie,constexpr
jeś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::iota
i klasycznefor
rozwią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 podstawowymfor
odpowiednikiem -loop.Uwaga: Funkcja
iota
pomocnicza służy tylko do zapewnienia parzystości funkcji w rozwiązaniu C ++ 20std::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.źródło
int
, a następnie tworzenie klasy „range”, aby zwrócić początek / koniecWersja 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 ...)
źródło
_
. Przydałoby się trochę wyjaśnienia (np. Zakres). W przeciwnym razie tak, ładnie KISSy.i_
byłoby bardziej zgodne._i
jest nadal modyfikowalna w pętli.std::views::iota
jest 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.)_Uppercase
idouble__underscore
identyfikatory są zastrzeżone._lowercase
identyfikatory są zarezerwowane tylko w zakresie globalnym.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 }
źródło
Jeśli nie masz dostępu do c ++ 20, 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::vector
Podejście jest najgorsze.( Zobacz podręczną ławkę online )
źródło
vector
. Jeśli zasięg jest bardzo duży, może to być złe.std::vector
jest 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 donew
+delete
i może faktycznie zapisując w tej pamięci.const i
po 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.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
źródło
{..}
. 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ł!#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 }
źródło
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
for
pę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.
źródło
[&]
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.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
źródło