Wie erstelle ich eine for-Schleifenvariable const mit Ausnahme der Inkrement-Anweisung?

82

Betrachten Sie einen Standard für die Schleife:

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

Ich möchte verhindern, dass die Variable iim Hauptteil der forSchleife geändert wird.

Ich kann jedoch nicht deklarieren i, constda dies die Inkrement-Anweisung ungültig macht. Gibt es eine Möglichkeit, ieine constVariable außerhalb der Inkrement-Anweisung zu erstellen?

jhourback
quelle
4
Ich glaube, es gibt keine Möglichkeit, dies zu tun
Itay
27
Das klingt nach einer Lösung auf der Suche nach einem Problem.
Pete Becker
14
Verwandeln Sie den Körper Ihrer for-Schleife in eine Funktion mit einem const int iArgument. Die Veränderbarkeit des Index wird nur dort angezeigt, wo er benötigt wird, und Sie können das inlineSchlüsselwort verwenden, damit er keine Auswirkungen auf die kompilierte Ausgabe hat.
Monty Thibault
4
Was (oder besser gesagt, wer) könnte möglicherweise den Wert des Index außer Ihnen ändern? Misstrauen Sie sich selbst? Vielleicht ein Mitarbeiter? Ich stimme @PeteBecker zu.
Z4-Tier
4
@ Z4-Tier Ja, natürlich misstraue ich mir. Ich weiß, dass ich Fehler mache. Jeder gute Programmierer weiß es. Deshalb haben wir Dinge const, mit denen wir anfangen möchten.
Konrad Rudolph

Antworten:

119

Ab c ++ 20 können Sie Bereiche :: Ansichten :: iota wie folgt verwenden :

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

Hier ist eine Demo .


Ab c ++ 11 können Sie auch die folgende Technik verwenden, die eine IIILE (sofort aufgerufener Inline-Lambda-Ausdruck) verwendet:

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

Hier ist eine Demo .

Beachten Sie, [&,i]dass dies idurch eine nicht veränderbare Kopie erfasst wird und alles andere durch eine veränderbare Referenz erfasst wird. Das ();am Ende der Schleife bedeutet einfach, dass das Lambda sofort aufgerufen wird.

cigien
quelle
Erfordert fast ein spezielles for-Schleifenkonstrukt, da das, was dies bietet, eine sicherere Alternative zu einem sehr, sehr häufigen Konstrukt ist.
Michael Dorgan
2
@MichaelDorgan Nun, da diese Funktion von der Bibliothek unterstützt wird, lohnt es sich nicht, sie als Kernsprache hinzuzufügen.
Cigien
1
Fair, obwohl fast meine gesamte Arbeit höchstens noch C oder C ++ 11 ist. Ich
Michael Dorgan
9
Der C ++ 11-Trick, den Sie mit dem Lambda hinzugefügt haben, ist ordentlich, aber an den meisten Arbeitsplätzen, an denen ich gearbeitet habe, nicht praktikabel. Eine statische Analyse würde sich über die verallgemeinerte &Erfassung beschweren , was die explizite Erfassung jeder Referenz erzwingen würde - was dies recht macht schwerfällig. Ich vermute auch, dass dies zu einfachen Fehlern führen kann, bei denen ein Autor das vergisst ()und der Code niemals aufgerufen wird. Dies ist leicht klein genug, um auch bei der Codeüberprüfung übersehen zu werden.
Human-Compiler
1
@cigien Statische Analysetools wie SonarQube und cppcheck kennzeichnen allgemeine Erfassungen, [&]da diese mit Codierungsstandards wie AUTOSAR (Regel A5-1-2), HIC ++ und meiner Meinung nach auch MISRA (nicht sicher) in Konflikt stehen. Es ist nicht so, dass es nicht richtig ist; Es ist so, dass Organisationen diese Art von Code verbieten, um den Standards zu entsprechen. Wie für die (), die neueste nicht gcc Version Flagge nicht einmal mit -Wextra. Ich denke immer noch, dass der Ansatz ordentlich ist; Es funktioniert einfach nicht für viele Organisationen.
Human-Compiler
44

Für alle, die Cigiens std::views::iotaAntwort mögen, aber nicht in C ++ 20 oder höher arbeiten, ist es ziemlich einfach, eine vereinfachte und leichtgewichtige Version von std::views::iotakompatibel zu implementieren oder höher.

Alles was es erfordert ist:

  • Ein grundlegender " LegacyInputIterator " -Typ (etwas, der definiert operator++und operator*), der einen ganzzahligen Wert umschließt (z. B. ein int)
  • Eine "range" -ähnliche Klasse, die die obigen Iteratoren hat begin()und end()zurückgibt. Dadurch kann es in bereichsbasierten forSchleifen arbeiten

Eine vereinfachte Version davon könnte sein:

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

Ich habe oben definiert, constexprwo es unterstützt wird, aber für frühere Versionen von C ++ wie C ++ 11/14 müssen Sie möglicherweise entfernen, constexprwo es in diesen Versionen nicht legal ist, um dies zu tun.

Mit dem obigen Boilerplate kann der folgende Code in Pre-C ++ 20 verwendet werden:

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

Die den Willen erzeugen gleiche Anordnung wie die C ++ 20 - std::views::iotaLösung und der klassischen for-loop Lösung , wenn optimiert.

Dies funktioniert mit allen C ++ 11-kompatiblen Compilern (z. B. Compilern wie gcc-4.9.4) und erzeugt immer noch eine nahezu identische Assembly wie ein Basic- forLoop-Gegenstück.

Hinweis: Die iotaHilfsfunktion dient nur der Feature-Parität mit der C ++ 20- std::views::iotaLösung. aber realistisch könnte man auch direkt ein konstruieren iota_range{...}anstatt aufzurufen iota(...). Ersteres bietet nur einen einfachen Upgrade-Pfad, wenn ein Benutzer in Zukunft zu C ++ 20 wechseln möchte.

Human-Compiler
quelle
3
Es erfordert ein bisschen Boilerplate, aber es ist eigentlich gar nicht so kompliziert in Bezug auf das, was es tut. Es ist eigentlich nur ein grundlegendes Iteratormuster, aber das Umschließen einer intund das Erstellen einer "Range" -Klasse, um den Anfang / das Ende zurückzugeben
Human-Compiler
1
Nicht besonders wichtig, aber ich habe auch eine C ++ 11-Lösung hinzugefügt, die sonst niemand veröffentlicht hat. Vielleicht möchten Sie die erste Zeile Ihrer Antwort leicht
umformulieren
Ich bin mir nicht sicher, wer abgelehnt hat, aber ich würde mich über ein Feedback freuen, wenn Sie der Meinung sind, dass meine Antwort unbefriedigend ist, damit ich sie verbessern kann. Downvoting ist eine großartige Möglichkeit, um zu zeigen, dass Sie der Meinung sind, dass eine Antwort die Frage nicht angemessen anspricht. In diesem Fall gibt es jedoch keine Kritikpunkte oder offensichtlichen Fehler in der Antwort, für die ich mich verbessern könnte.
Human-Compiler
@ Human-Compiler Ich habe zur gleichen Zeit auch einen DV bekommen und sie haben auch nicht kommentiert, warum :( Vermutlich mag jemand die Bereichsabstraktionen nicht. Ich würde mir darüber keine Sorgen machen.
cigien
1
"Versammlung" ist ein Massennomen wie "Gepäck" oder "Wasser". Die normale Formulierung wäre "wird in derselben Assembly wie C ++ 20 kompiliert ...". Der ASM - Ausgabe des Compilers für eine einzige Funktion ist nicht eine singuläre Montag, es ist „assembly“ (eine Folge von Assemblersprachenanweisungen).
Peter Cordes
29

Die KISS Version ...

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

    // use i here
}

Wenn Ihr Anwendungsfall nur darin besteht, eine versehentliche Änderung des Schleifenindex zu verhindern, sollte dies einen solchen Fehler offensichtlich machen. (Wenn Sie absichtliche Änderungen verhindern möchten , viel Glück ...)

Artelius
quelle
11
Ich denke, Sie lehren die falsche Lektion, magische Identifikatoren zu verwenden, die mit beginnen _. Und ein bisschen Erklärung (zB Umfang) wäre hilfreich. Ansonsten ja, schön KISSy.
Yunnosch
14
Das Aufrufen der "versteckten" Variablen i_wäre konformer.
Yirkha
9
Ich bin mir nicht sicher, wie dies die Frage beantwortet. Die Schleifenvariable kann _iin der Schleife noch geändert werden.
Cigien
4
@cigien: IMO, diese Teillösung ist so weit, dass es sich lohnt, auf C ++ 20 zu verzichten, std::views::iotaum einen vollständig kugelsicheren Weg zu finden. Der Text der Antwort erklärt seine Grenzen und wie sie versucht, die Frage zu beantworten. Eine Reihe von überkompliziertem C ++ 11 macht die Heilung in Bezug auf einfach zu lesende, leicht zu wartende IMO schlechter als die Krankheit. Dies ist für alle, die C ++ kennen, immer noch sehr einfach zu lesen und erscheint als Redewendung vernünftig. (Sollte aber vermeiden, führende Unterstrichnamen zu verwenden.)
Peter Cordes
5
Nur @Yunnosch _Uppercaseund double__underscoreBezeichner sind reserviert. _lowercaseBezeichner sind nur im globalen Bereich reserviert.
Roman Odaisky
13

Wenn Sie keinen Zugriff auf haben , typische Überarbeitung mit einer Funktion

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

jetzt könntest du

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

( Siehe eine Demo )


Update : Inspiriert von dem Kommentar von @ Human-Compiler habe ich mich gefragt, ob die gegebenen Antworten einen Unterschied in Bezug auf die Leistung haben. Es stellt sich heraus, dass mit Ausnahme dieses Ansatzes alle anderen Ansätze überraschenderweise die gleiche Leistung aufweisen (für den Bereich [0, 10)). Der std::vectorAnsatz ist der schlechteste.

Geben Sie hier die Bildbeschreibung ein

( Siehe Online-Schnellbank )

JeJo
quelle
4
Obwohl dies für Pre-C ++ 20 funktioniert, ist der Overhead ziemlich hoch, da die Verwendung erforderlich ist vector. Wenn der Bereich sehr groß ist, kann dies schlecht sein.
Human-Compiler
@ Human-Compiler: A std::vectorist relativ gesehen ziemlich schrecklich, wenn der Bereich ebenfalls klein ist, und könnte sehr schlecht sein, wenn dies eine kleine innere Schleife sein sollte, die viele, viele Male lief. Einige Compiler (wie das Klirren mit libc ++, aber nicht libstdc ++) können das Neu- / Löschen einer Zuordnung optimieren, die der Funktion nicht entgeht. Andernfalls kann dies leicht der Unterschied zwischen einer kleinen, vollständig abgewickelten Schleife und einem Aufruf von new+ sein deleteund vielleicht tatsächlich in dieser Erinnerung speichern.
Peter Cordes
IMO, der kleine Vorteil von const iist in den meisten Fällen einfach nicht den Aufwand wert, ohne C ++ 20-Möglichkeiten, die es billig machen. Insbesondere bei Bereichen mit Laufzeitvariablen, die es für den Compiler weniger wahrscheinlich machen, alles weg zu optimieren.
Peter Cordes
13

Könnten Sie nicht einfach einen Teil oder den gesamten Inhalt Ihrer for-Schleife in eine Funktion verschieben, die i als const akzeptiert?

Es ist weniger optimal als einige vorgeschlagene Lösungen, aber wenn möglich, ist dies recht einfach.

Edit: Nur ein Beispiel, da ich eher unklar bin.

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

void looper ( const int v )
{
    // do your thing here
}
Al rl
quelle
10

Und hier ist eine C ++ 11-Version:

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

Hier ist eine Live-Demo

Vlad Feinstein
quelle
6
Dies wird nicht skaliert, wenn die maximale Anzahl durch einen Laufzeitwert festgelegt wird.
Human-Compiler
12
@ Human-Compiler Erweitern Sie einfach die Liste auf den gewünschten Wert und kompilieren Sie Ihr gesamtes Programm dynamisch neu;)
Monty Thibault
5
Sie haben nicht erwähnt, was der Fall ist {..}. Sie müssen etwas hinzufügen, um diese Funktion zu aktivieren. Zum Beispiel wird Ihr Code beschädigt , wenn Sie keine richtigen Header hinzufügen: godbolt.org/z/esbhra . Es <iostream>ist eine schlechte Idee, für andere Header weiterzuleiten !
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.
    }
  }
}

Hinweis: Wir müssen den Gültigkeitsbereich aufgrund einer erstaunlichen Dummheit in der Sprache verschachteln: Die im for(...)Header deklarierte Variable befindet sich auf derselben Verschachtelungsebene wie die in der {...}zusammengesetzten Anweisung deklarierten Variablen . Dies bedeutet zum Beispiel:

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

Was? Haben wir nicht einfach eine geschweifte Klammer geöffnet? Darüber hinaus ist es inkonsistent:

void fun(int i)
{
  int i = 42; // OK
}
Kaz
quelle
1
Dies ist leicht die beste Antwort. Es ist eine elegante Lösung, C ++ 's' Variable Shadowing 'zu nutzen, um zu bewirken, dass der Bezeichner in eine const ref-Variable aufgelöst wird, die auf die ursprüngliche Schrittvariable verweist. Oder zumindest die eleganteste auf dem Markt.
Max Barraclough
4

Ein einfacher Ansatz, der hier noch nicht erwähnt wurde und in jeder Version von C ++ funktioniert, besteht darin, einen funktionalen Wrapper um einen Bereich zu erstellen, ähnlich wie std::for_eachdies bei Iteratoren der Fall ist. Der Benutzer ist dann dafür verantwortlich, ein Funktionsargument als Rückruf zu übergeben, der bei jeder Iteration aufgerufen wird.

Zum Beispiel:

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

Wo die Verwendung wäre:

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

Alles, was älter als C ++ 11 ist, würde stecken bleiben und einen stark benannten Funktionszeiger an for_each(ähnlich wie std::for_each) übergeben, aber es funktioniert immer noch.

Hier ist eine Demo


Obwohl dies für forSchleifen in C ++ möglicherweise nicht idiomatisch ist , ist dieser Ansatz in anderen Sprachen weit verbreitet. Funktionale Wrapper sind aufgrund ihrer Zusammensetzbarkeit in komplexen Aussagen sehr elegant und können sehr ergonomisch verwendet werden.

Dieser Code ist auch einfach zu schreiben, zu verstehen und zu warten.

Human-Compiler
quelle
Eine Einschränkung, die bei diesem Ansatz zu beachten ist, besteht darin, dass einige Organisationen die Erfassung von Standardeinstellungen für Lambdas (z. B. [&]oder [=]) verbieten , um bestimmte Sicherheitsstandards einzuhalten. Dies kann dazu führen, dass das Lambda aufgebläht wird und jedes Mitglied manuell erfasst werden muss. Nicht alle Organisationen tun dies, daher erwähne ich dies nur als Kommentar und nicht in der Antwort.
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);
}

vielleicht nenn es for_i

Kein Overhead https://godbolt.org/z/e7asGj

Hrisip
quelle