c++ copy - Was ist das Kopier-und-Tausch-Idiom?




and swap (5)

Was ist dieses Idiom und wann sollte es verwendet werden? Welche Probleme löst es? Ändert sich das Idiom, wenn C ++ 11 verwendet wird?

Obwohl es an vielen Orten erwähnt wurde, hatten wir keine singuläre "Was ist das" Frage und Antwort, also ist es hier. Hier ist eine unvollständige Liste von Orten, wo es zuvor erwähnt wurde:


Answers

Es gibt schon einige gute Antworten. Ich werde mich hauptsächlich auf das konzentrieren, was meiner Meinung nach fehlt - eine Erklärung der "Nachteile" mit dem Kopier-und-Tausch-Idiom ....

Was ist das Kopier-und-Tausch-Idiom?

Eine Möglichkeit, den Zuweisungsoperator im Sinne einer Tauschfunktion zu implementieren:

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

Die Grundidee ist, dass:

  • der am meisten fehleranfällige Teil der Zuweisung zu einem Objekt ist die Sicherstellung von Ressourcen, die der neue Zustand benötigt (zB Speicher, Deskriptoren)

  • *this Erfassung kann versucht werden, bevor der aktuelle Zustand des Objekts (dh *this ) rhs wird, wenn eine Kopie des neuen Werts erstellt wird, weshalb rhs als Wert akzeptiert (dh kopiert) wird und nicht als Referenz

  • Vertauschen des Status der lokalen Kopie rhs und *this ist normalerweise relativ einfach ohne potentielle Fehler / Ausnahmen zu tun, da die lokale Kopie hinterher keinen bestimmten Zustand benötigt (sie braucht nur den Zustand, in dem der Destruktor läuft, ähnlich wie bei einem Objekt wird von in> C ++ 11 verschoben )

Wann sollte es verwendet werden? (Welche Probleme löst es [/ create] ?)

  • Wenn Sie möchten, dass der zugewiesene nicht von einer Zuweisung betroffen ist, die eine Ausnahme auslöst, vorausgesetzt, Sie haben oder können einen swap mit starker Ausnahmegarantie schreiben und idealerweise einen, der nicht fehlschlagen / throw .

  • Wenn Sie eine saubere, leicht zu verstehende, robuste Möglichkeit zum Definieren des Zuweisungsoperators in Form von (einfacheren) Kopierkonstruktor-, swap und Destruktorfunktionen wünschen.

    • Die Selbstzuweisung als Kopie-und-Austausch vermeidet häufig übersehene Randfälle.

  • Wenn eine Leistungseinbuße oder eine vorübergehend höhere Ressourcennutzung durch ein zusätzliches temporäres Objekt während der Zuweisung verursacht wird, ist dies für Ihre Anwendung nicht wichtig. ⁂

swap Werfen: Es ist im Allgemeinen möglich, Datenmitglieder zuverlässig zu vertauschen, die die Objekte durch Zeiger verfolgen, aber Nicht-Zeigerdatenmitglieder, die keinen throw-freien Austausch haben, oder für den das Tauschen als X tmp = lhs; lhs = rhs; rhs = tmp; implementiert werden X tmp = lhs; lhs = rhs; rhs = tmp; X tmp = lhs; lhs = rhs; rhs = tmp; und Kopier-Konstruktion oder Zuordnung kann werfen, immer noch das Potenzial zu scheitern einige Daten Mitglieder vertauscht und andere nicht. Dieses Potential trifft sogar auf C ++ 03 std::string , da James eine andere Antwort kommentiert:

@wilhelmelt: In C ++ 03 werden keine Ausnahmen erwähnt, die möglicherweise von std :: string :: swap (das von std :: swap aufgerufen wird) ausgelöst werden. In C ++ 0x ist std :: string :: swap noexcept und darf keine Ausnahmen auslösen. - James McNellis 22. Dezember 10 um 15:24 Uhr

‡ Eine Zuweisungsoperatorimplementierung, die bei Zuweisung von einem bestimmten Objekt sinnvoll erscheint, kann leicht für die Selbstzuweisung fehlschlagen. Während es unvorstellbar erscheint, dass Client-Code sogar eine Selbstzuweisung versuchen würde, kann dies relativ leicht während Algo-Operationen auf Containern passieren, mit x = f(x); Code wo f ist (vielleicht nur für einige #ifdef Zweige) ein Makro ala #define f(x) x oder eine Funktion, die einen Verweis auf x , oder sogar (wahrscheinlich ineffizient, aber prägnant) Code wie x = c1 ? x * 2 : c2 ? x / 2 : x; x = c1 ? x * 2 : c2 ? x / 2 : x; ). Beispielsweise:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

Bei der Selbstzuweisung lösche der obige Code x.p_; , p_ in einer neu zugewiesenen Heap-Region an und versucht dann, die nicht initialisierten Daten darin zu lesen (Undefined Behavior), wenn dies nicht zu merkwürdig ist, versucht copy eine Selbstzuweisung zu jedem gerade zerstörten 'T'!

⁂ Das Copy-and-Swap-Idiom kann zu Ineffizienzen oder Einschränkungen aufgrund der Verwendung eines zusätzlichen temporären Parameters führen (wenn der Parameter des Operators copy-konstruiert ist):

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

Hier könnte ein handgeschriebener Client::operator= prüfen, ob *this bereits mit dem gleichen Server verbunden ist wie rhs (vielleicht einen "reset" -Code senden, wenn nützlich), während der copy-and-swap-Ansatz die Kopie aufrufen würde. Konstruktor, der wahrscheinlich geschrieben würde, um eine eigene Socket-Verbindung zu öffnen, und dann die ursprüngliche zu schließen. Dies könnte nicht nur eine Remotenetzwerkinteraktion anstelle einer einfachen prozessinternen Variablenkopie bedeuten, sondern auch Client- oder Serverlimits für Socket-Ressourcen oder Verbindungen. (Natürlich hat diese Klasse eine ziemlich schreckliche Schnittstelle, aber das ist eine andere Sache ;-P).


Ich möchte ein Wort der Warnung hinzufügen, wenn Sie sich mit C ++ 11-ähnlichen Allokations-fähigen Containern beschäftigen. Swapping und Zuweisung haben eine subtil unterschiedliche Semantik.

Zur Konkretheit betrachten wir einen Container std::vector<T, A> , wobei A ein Stateful-Allocator-Typ ist, und wir vergleichen die folgenden Funktionen:

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

Der Zweck beider Funktionen fs und fm besteht darin, a Zustand zu geben, den b anfänglich hatte. Es gibt jedoch eine versteckte Frage: Was passiert, wenn a.get_allocator() != b.get_allocator() ? Die Antwort ist: Es kommt darauf an. Lassen Sie uns AT = std::allocator_traits<A> schreiben.

  • Wenn AT::propagate_on_container_move_assignment std::true_type , dann std::true_type fm den Zuordner von a mit dem Wert von b.get_allocator() , andernfalls nicht, und a weiterhin seinen ursprünglichen Zuordner. In diesem Fall müssen die Datenelemente einzeln ausgetauscht werden, da die Speicherung von a und b nicht kompatibel ist.

  • Wenn AT::propagate_on_container_swap std::true_type , dann tauscht fs sowohl Daten als auch Zuweiser in der erwarteten Weise.

  • Wenn AT::propagate_on_container_swap std::false_type , brauchen wir eine dynamische Prüfung.

    • Wenn a.get_allocator() == b.get_allocator() , verwenden die beiden Container kompatiblen Speicher, und das Tauschen erfolgt in der üblichen Weise.
    • Wenn jedoch a.get_allocator() != b.get_allocator() , hat das Programm ein nicht definiertes Verhalten (vgl. [Container.requirements.general / 8]).

Das Ergebnis ist, dass das Auslagern zu einer nicht-trivialen Operation in C ++ 11 geworden ist, sobald Ihr Container stateful allocators unterstützt. Das ist ein etwas "fortgeschrittener Anwendungsfall", aber es ist nicht ganz unwahr- scheinlich, da Verschiebungsoptimierungen in der Regel erst dann interessant werden, wenn Ihre Klasse eine Ressource verwaltet und Speicher eine der beliebtesten Ressourcen ist.


Überblick

Warum brauchen wir das Copy-and-Swap-Idiom?

Jede Klasse, die eine Ressource verwaltet (ein Wrapper wie ein intelligenter Zeiger), muss The Big Three implementieren. Während die Ziele und die Implementierung des Kopierkonstruktors und Destruktors einfach sind, ist der Kopierzuweisungsoperator wohl der nuancierteste und schwierigste. Wie sollte es gemacht werden? Welche Fallgruben müssen vermieden werden?

Das Copy-and-Swap-Idiom ist die Lösung und unterstützt den Zuweisungs-Operator elegant dabei, zwei Dinge zu erreichen: die Vermeidung von Code-Duplikation und die Bereitstellung einer starken Ausnahme-Garantie .

Wie funktioniert es?

Conceptually funktioniert es, indem die Funktionalität des Kopierkonstruktors verwendet wird, um eine lokale Kopie der Daten zu erstellen, und dann werden die kopierten Daten mit einer swap , wobei die alten Daten mit den neuen Daten ausgetauscht werden. Die temporäre Kopie zerstört dann und nimmt die alten Daten mit. Uns bleibt eine Kopie der neuen Daten.

Um das Copy-and-Swap-Idiom zu verwenden, benötigen wir drei Dinge: einen funktionierenden Copy-Constructor, einen funktionierenden Destruktor (beide sind die Basis jedes Wrappers, sollten also trotzdem fertig sein) und eine swap Funktion.

Eine Swap-Funktion ist eine Nicht-Throw- Funktion, die zwei Objekte einer Klasse, Member für Member, vertauscht. Wir könnten versucht sein, std::swap anstatt unsere eigenen zu liefern, aber das wäre unmöglich; std::swap verwendet den Copy-Constructor und den Copy-Assignment-Operator innerhalb seiner Implementierung und wir würden letztendlich versuchen, den Zuweisungsoperator in Bezug auf sich selbst zu definieren!

(Nicht nur das, sondern unqualifizierte Aufrufe zum swap verwenden unseren angepassten Tauschoperator und überspringen die unnötige Konstruktion und Zerstörung unserer Klasse, die std::swap sich bringen würde.)

Eine eingehende Erklärung

Das Ziel

Betrachten wir einen konkreten Fall. Wir wollen in einer ansonsten nutzlosen Klasse ein dynamisches Array verwalten. Wir beginnen mit einem funktionierenden Konstruktor, einem Kopierkonstruktor und einem Destruktor:

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : nullptr)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr),
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

Diese Klasse verwaltet das Array fast erfolgreich, benötigt jedoch operator= , um ordnungsgemäß zu funktionieren.

Eine fehlgeschlagene Lösung

So könnte eine naive Implementierung aussehen:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

Und wir sagen, wir sind fertig; Dies verwaltet jetzt ein Array, ohne Lecks. Es leidet jedoch unter drei Problemen, die im Code als (n) .

  1. Der erste ist der Selbstzuweisungstest. Diese Überprüfung dient zwei Zwecken: Es ist ein einfacher Weg, uns davon abzuhalten, unnötigen Code bei der Selbstzuweisung auszuführen, und er schützt uns vor subtilen Fehlern (wie das Löschen des Arrays, nur um es zu kopieren und zu kopieren). Aber in allen anderen Fällen dient es lediglich dazu, das Programm zu verlangsamen und als Rauschen im Code zu wirken; Eine Selbstzuweisung tritt selten auf, daher ist diese Überprüfung meistens eine Verschwendung. Es wäre besser, wenn der Bediener ohne ihn richtig arbeiten könnte.

  2. Das zweite ist, dass es nur eine grundlegende Ausnahmegarantie bietet. Wenn new int[mSize] fehlschlägt, *this geändert. (Nämlich die Größe ist falsch und die Daten sind weg!) Für eine starke Ausnahmegarantie müsste es etwas sein wie:

    dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // (1)
        {
            // get the new data ready before we replace the old
            std::size_t newSize = other.mSize;
            int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
            std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
    
            // replace the old data (all are non-throwing)
            delete [] mArray;
            mSize = newSize;
            mArray = newArray;
        }
    
        return *this;
    }
    
  3. Der Code wurde erweitert! Was uns zum dritten Problem führt: Codeduplizierung. Unser Zuweisungs-Operator dupliziert effektiv den ganzen Code, den wir bereits anderswo geschrieben haben, und das ist eine schreckliche Sache.

In unserem Fall besteht der Kern aus nur zwei Zeilen (der Zuweisung und der Kopie), aber mit komplexeren Ressourcen kann dieser Code-Bloat ziemlich mühsam sein. Wir sollten danach streben, uns niemals zu wiederholen.

(Man könnte sich fragen: Wenn so viel Code benötigt wird, um eine Ressource richtig zu verwalten, was passiert, wenn meine Klasse mehr als einen verwaltet?) Obwohl dies eine berechtigte Sorge zu sein scheint und in der Tat nicht-triviale try / catch Klauseln erfordert ein Nicht-Problem, weil eine Klasse nur eine Ressource verwalten soll!

Eine erfolgreiche Lösung

Wie erwähnt, wird das Kopier-und-Austausch-Idiom alle diese Probleme beheben. Aber jetzt haben wir alle Anforderungen außer einer: eine swap Funktion. Während die Regel von Drei erfolgreich die Existenz unseres Kopierkonstruktors, Zuweisungsoperators und Destruktors mit sich bringt, sollte sie wirklich "Die Großen Drei und Eine Hälfte" heißen: Jedes Mal, wenn Ihre Klasse eine Ressource verwaltet, macht es auch Sinn, einen swap bereitzustellen Funktion.

Wir müssen unserer Klasse Swap-Funktionalität hinzufügen, und wir tun das wie folgt †:

class dumb_array
{
public:
    // ...

    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap;

        // by swapping the members of two objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

( Here ist die Erklärung, warum public friend swap .) Nun können wir nicht nur unsere dumb_array tauschen, sondern Swaps können im Allgemeinen effizienter sein; Es tauscht einfach Zeiger und Größen aus, anstatt ganze Arrays zuzuordnen und zu kopieren. Abgesehen von diesem Bonus in Funktionalität und Effizienz sind wir nun bereit, das Kopier- und Austausch-Idiom zu implementieren.

Unser Zuweisungs-Operator ist ohne weiteres:

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

Und das ist es! Mit einem Schlag werden alle drei Probleme gleichzeitig elegant angegangen.

Warum funktioniert es?

Wir bemerken zuerst eine wichtige Entscheidung: Das Parameterargument wird als Wert genommen . Man könnte genauso gut Folgendes tun (und tatsächlich tun viele naive Implementierungen des Idioms):

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

Wir verlieren eine wichtige Optimierungsmöglichkeit . Nicht nur das, sondern diese Wahl ist in C ++ 11 kritisch, was später diskutiert wird. (Im Allgemeinen ist eine bemerkenswert nützliche Richtlinie wie folgt: Wenn Sie eine Kopie von etwas in einer Funktion erstellen wollen, lassen Sie es vom Compiler in der Parameterliste tun.)

In jedem Fall ist diese Methode, um unsere Ressource zu erhalten, der Schlüssel zur Beseitigung von Code-Duplizierung: Wir können den Code aus dem Copy-Konstruktor verwenden, um die Kopie zu erstellen, und müssen nie etwas davon wiederholen. Jetzt, wo die Kopie gemacht ist, sind wir bereit zu tauschen.

Beachten Sie, dass beim Eingeben der Funktion alle neuen Daten bereits zugewiesen, kopiert und zur Verwendung bereit sind. Dies gibt uns eine starke Ausnahme-Garantie für die Freiheit: Wir werden die Funktion nicht einmal eingeben, wenn der Aufbau der Kopie fehlschlägt, und es ist daher nicht möglich, den Zustand von *this zu ändern. (Was wir vorher manuell für eine starke Ausnahmegarantie gemacht haben, tut der Compiler jetzt für uns; wie nett.)

An diesem Punkt sind wir zu Hause frei, weil swap nicht werfen ist. Wir tauschen unsere aktuellen Daten mit den kopierten Daten aus und ändern so sicher unseren Zustand, und die alten Daten werden in das temporäre übertragen. Die alten Daten werden dann freigegeben, wenn die Funktion zurückkehrt. (Wo der Gültigkeitsbereich des Parameters endet und sein Destruktor aufgerufen wird.)

Da das Idiom keinen Code wiederholt, können wir keine Fehler innerhalb des Operators einführen. Beachten Sie, dass dies bedeutet, dass wir auf die Notwendigkeit einer Selbstzuweisungsprüfung verzichten müssen, die eine einheitliche Implementierung von operator= . (Außerdem haben wir keine Leistungseinbußen bei nicht-selbst zugewiesenen Aufgaben.)

Und das ist das Kopieren-und-Tauschen-Idiom.

Was ist mit C ++ 11?

Die nächste Version von C ++, C ++ 11, macht eine sehr wichtige Änderung in der Art und Weise, wie wir Ressourcen verwalten: Die Drei-Regel ist jetzt die Vier-Regel (und eine Hälfte). Warum? Weil wir nicht nur in der Lage sein müssen, unsere Ressource zu kopieren, müssen wir sie auch konstruieren .

Zum Glück für uns ist das einfach:

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other)
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

Was ist denn hier los? Erinnern Sie sich an das Ziel von move-construction: die Ressourcen einer anderen Instanz der Klasse zu übernehmen und sie in einem Zustand zu belassen, der garantiert zuweisbar und zerstörbar ist.

Was wir getan haben, ist einfach: über den Standardkonstruktor initialisieren (ein C ++ 11-Feature), dann mit other austauschen; Wir wissen, dass eine standardmäßig konstruierte Instanz unserer Klasse sicher zugewiesen und zerstört werden kann, so dass wir wissen, dass other nach dem Austausch dasselbe tun können.

(Beachten Sie, dass einige Compiler die Delegierung von Konstruktoren nicht unterstützen; in diesem Fall müssen wir die Klasse standardmäßig konstruieren. Dies ist eine unglückliche, aber glücklicherweise triviale Aufgabe.)

Warum funktioniert das?

Das ist die einzige Veränderung, die wir in unserer Klasse machen müssen. Warum funktioniert das? Erinnere dich an die immer wichtige Entscheidung, die wir getroffen haben, um den Parameter zu einem Wert und nicht zu einer Referenz zu machen:

dumb_array& operator=(dumb_array other); // (1)

Wenn nun ein other mit einem R-Wert initialisiert wird, wird er move-konstruiert . Perfekt. Genauso wie C ++ 03 unsere Copy-Constructor-Funktionalität wiederverwendet, indem das Argument by-value genommen wird, wählt C ++ 11 automatisch den move-constructor aus, wenn dies ebenfalls sinnvoll ist. (Und natürlich, wie im vorher verlinkten Artikel erwähnt, kann das Kopieren / Verschieben des Wertes einfach ganz weggelassen werden.)

Und so schließt das Kopier-und-Tausch-Idiom.

Fußnoten

* Warum setzen wir mArray auf null? Wenn weiterer Code im Operator dumb_array möglicherweise der Destruktor von dumb_array aufgerufen. und wenn dies geschieht, ohne es auf null zu setzen, versuchen wir, bereits gelöschten Speicher zu löschen! Wir vermeiden dies, indem wir ihn auf null setzen, da das Löschen von null eine Nicht-Operation ist.

† Es gibt andere Behauptungen, dass wir std::swap für unseren Typ spezialisieren, einen In-Class- swap zusammen mit einem Free-Function- swap usw. swap sollten. Aber das ist alles unnötig: Jede ordnungsgemäße Verwendung von swap erfolgt durch einen Unqualifizierten Rufen Sie an, und unsere Funktion wird über ADL . Eine Funktion wird ausreichen.

‡ Der Grund ist einfach: Sobald Sie die Ressource für sich selbst haben, können Sie sie austauschen und / oder verschieben (C ++ 11), wo auch immer sie sein muss. Und indem Sie die Kopie in der Parameterliste erstellen, maximieren Sie die Optimierung.


Diese Antwort ist eher eine Ergänzung und eine leichte Modifikation der obigen Antworten.

In einigen Versionen von Visual Studio (und möglicherweise anderen Compilern) gibt es einen Fehler, der wirklich nervig ist und keinen Sinn ergibt. Wenn Sie also Ihre swap Funktion wie swap deklarieren / definieren:

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... der Compiler wird Sie anschreien, wenn Sie die swap Funktion aufrufen:

Dies hat etwas damit zu tun, dass eine friend Funktion aufgerufen wird und this Objekt als Parameter übergeben wird.

Um dies zu vermeiden, verwenden Sie das Schlüsselwort friend nicht und definieren die swap Funktion neu:

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

Diesmal können Sie einfach swap aufrufen und other , was den Compiler glücklich macht:

Schließlich müssen Sie keine friend Funktion verwenden, um 2 Objekte zu tauschen. Es ist ebenso sinnvoll, eine swap mit einem other Objekt als Parameter zu swap .

Sie haben bereits Zugriff auf this Objekt, daher ist es technisch überflüssig, es als Parameter zu übergeben.


5. Häufige Fallstricke bei der Verwendung von Arrays.

5.1 Fallstricke: Vertrauenswürdige Typ-unsichere Verknüpfung.

OK, Ihnen wurde gesagt, oder Sie haben selbst herausgefunden, dass Globals (Namespace-Scope-Variablen, auf die außerhalb der Übersetzungseinheit zugegriffen werden kann) Evil ™ sind. Aber wusstest du, wie wahrlich Evil ™ sie sind? Betrachten Sie das folgende Programm, bestehend aus zwei Dateien [main.cpp] und [numbers.cpp]:

// [main.cpp]
#include <iostream>

extern int* numbers;

int main()
{
    using namespace std;
    for( int i = 0;  i < 42;  ++i )
    {
        cout << (i > 0? ", " : "") << numbers[i];
    }
    cout << endl;
}

// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

In Windows 7 kompiliert und verknüpft dies sowohl mit MinGW g ++ 4.4.1 und Visual C ++ 10.0.

Da die Typen nicht übereinstimmen, stürzt das Programm ab, wenn Sie es ausführen.

In-the-formal Erklärung: Das Programm hat Undefined Behavior (UB), und anstatt es zu stürzen, kann es einfach hängen, oder vielleicht nichts tun, oder es kann bedrohliche E-Mails an die Präsidenten der USA, Russland, Indien senden, China und die Schweiz, und lass Nasal Daemons aus deiner Nase fliegen.

Erklärung in der Praxis: In main.cpp das Array als ein Zeiger behandelt, der sich an der gleichen Adresse wie das Array befindet. Für 32-Bit-Executable bedeutet dies, dass der erste int Wert im Array als Zeiger behandelt wird. Dh, in main.cpp enthält oder enthält die Zahlenvariable (int*)1 . Dies veranlaßt das Programm, auf den Speicher ganz unten im Adreßraum zuzugreifen, was üblicherweise reserviert ist und eine Trap-Veranlassung verursacht. Ergebnis: Sie bekommen einen Absturz.

Die Compiler sind vollständig berechtigt, diesen Fehler nicht zu diagnostizieren, weil C ++ 11 § 3.5 / 10 über die Anforderung kompatibler Typen für die Deklarationen sagt,

[N3290 §3.5 / 10]
Ein Verstoß gegen diese Regel bei der Typidentität erfordert keine Diagnose.

Derselbe Absatz beschreibt die Variation, die erlaubt ist:

... Deklarationen für ein Array-Objekt können Array-Typen angeben, die sich durch das Vorhandensein oder Fehlen einer großen Array-Grenze (8.3.4) unterscheiden.

Diese erlaubte Variation beinhaltet nicht die Deklaration eines Namens als ein Array in einer Übersetzungseinheit und als einen Zeiger in einer anderen Übersetzungseinheit.

5.2 Fallstricke: Vorzeitige Optimierung ( memset & friends).

Noch nicht geschrieben

5.3 Fallstricke: Verwenden des C-Idioms, um die Anzahl der Elemente zu erhalten.

Mit tiefer C Erfahrung ist es natürlich zu schreiben ...

#define N_ITEMS( array )   (sizeof( array )/sizeof( array[0] ))

Da ein array bei Bedarf zum Zeiger auf das erste Element abfällt, kann der Ausdruck sizeof(a)/sizeof(a[0]) auch als sizeof(a)/sizeof(*a) . Es bedeutet dasselbe, und egal, wie es geschrieben steht, es ist das C-Idiom, um die Zahlenelemente des Arrays zu finden.

Hauptfehler: Das C-Idiom ist nicht typsicher. Zum Beispiel der Code ...

#include <stdio.h>

#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))

void display( int const a[7] )
{
    int const   n = N_ITEMS( a );          // Oops.
    printf( "%d elements.\n", n );
}

int main()
{
    int const   moohaha[]   = {1, 2, 3, 4, 5, 6, 7};

    printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
    display( moohaha );
}

übergibt einen Zeiger an N_ITEMS und führt daher höchstwahrscheinlich zu einem falschen Ergebnis. In Windows 7 als ausführbare 32-Bit-Datei kompiliert ...

7 Elemente, Display aufrufen ...
1 Elemente.

  1. Der Compiler schreibt int const a[7] in nur int const a[] .
  2. Der Compiler schreibt int const a[] nach int const* a .
  3. N_ITEMS wird daher mit einem Zeiger aufgerufen.
  4. Für eine ausführbare 32-Bit- sizeof(array) (Größe eines Zeigers) ist dann 4.
  5. sizeof(*array) entspricht sizeof(int) , was für eine ausführbare 32-Bit-Datei auch 4 ist.

Um diesen Fehler zur Laufzeit zu erkennen, können Sie ...

#include <assert.h>
#include <typeinfo>

#define N_ITEMS( array )       (                               \
    assert((                                                    \
        "N_ITEMS requires an actual array as argument",        \
        typeid( array ) != typeid( &*array )                    \
        )),                                                     \
    sizeof( array )/sizeof( *array )                            \
    )

7 Elemente, Display aufrufen ...
Assertion failed: ("N_ITEMS benötigt ein aktuelles Array als Argument", typeid (a)! = Typeid (& * a)), Datei runtime_detect ion.cpp, Zeile 16

Diese Anwendung hat die Runtime aufgefordert, sie auf ungewöhnliche Weise zu beenden.
Bitte kontaktieren Sie das Support-Team der Anwendung für weitere Informationen.

Die Laufzeitfehlererkennung ist besser als keine Erkennung, aber es verschwendet ein wenig Prozessorzeit und vielleicht viel mehr Programmierzeit. Besser mit der Erkennung zur Kompilierzeit! Wenn Sie Arrays lokaler Typen mit C ++ 98 nicht unterstützen möchten, können Sie Folgendes tun:

#include <stddef.h>

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

#define N_ITEMS( array )       n_items( array )

Kompiliert diese Definition in das erste vollständige Programm, mit g ++, habe ich ...

M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: In der Funktion 'void display (const int *)':
compile_time_detection.cpp: 14: error: keine passende Funktion für den Aufruf von 'n_items (const int * &)'

M: \ Anzahl> _

Wie es funktioniert: Das Array wird als Verweis auf n_items und daher nicht zum Zeiger auf das erste Element, und die Funktion kann nur die Anzahl der vom Typ angegebenen Elemente zurückgeben.

Mit C ++ 11 können Sie dies auch für Arrays des lokalen Typs verwenden, und es ist das typsichere C ++ - Idiom zum Auffinden der Anzahl von Elementen eines Arrays.

5.4 C ++ 11 & C ++ 14 Fallstricke: Verwenden einer constexpr Array- constexpr .

Mit C ++ 11 und später ist es natürlich, aber wie Sie sehen werden gefährlich !, um die C ++ 03-Funktion zu ersetzen

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

mit

using Size = ptrdiff_t;

template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }

Die wesentliche Änderung ist die Verwendung von constexpr , wodurch diese Funktion eine Kompilierzeitkonstante erzeugen kann.

Zum Beispiel kann im Gegensatz zur C ++ 03-Funktion eine solche Kompilierzeitkonstante verwendet werden, um ein Array derselben Größe wie ein anderes zu deklarieren:

// Example 1
void foo()
{
    int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
    constexpr Size n = n_items( x );
    int y[n] = {};
    // Using y here.
}

Aber bedenken Sie diesen Code mit der constexpr Version:

// Example 2
template< class Collection >
void foo( Collection const& c )
{
    constexpr int n = n_items( c );     // Not in C++14!
    // Use c here
}

auto main() -> int
{
    int x[42];
    foo( x );
}

Die Falle: -pedantic-errors Juli 2015 kompiliert das obige mit MinGW-64 5.1.0 mit -pedantic-errors , und testet mit den Online-Compilern auf gcc.godbolt.org/ , auch mit clang 3.0 und clang 3.2, aber nicht mit clang 3.3, 3.4.1, 3.5.0, 3.5.1, 3.6 (rc1) oder 3.7 (experimentell). Und wichtig für die Windows-Plattform, kompiliert es nicht mit Visual C ++ 2015. Der Grund ist eine C ++ 11 / C ++ 14-Anweisung über die Verwendung von Referenzen in constexpr Ausdrücken:

C ++ 11 C ++ 14 $ 5,19 / 2 neunter Gedankenstrich

Ein Bedingungsausdruck e ist ein Kernkonstantenausdruck, es sei denn, die Auswertung von e nach den Regeln der abstrakten Maschine (1.9) würde einen der folgenden Ausdrücke auswerten:

  • ein ID-Ausdruck , der sich auf eine Variable oder ein Datenelement des Referenztyps bezieht, es sei denn, der Verweis hat eine vorangehende Initialisierung und beides
    • es wird mit einem konstanten Ausdruck oder initialisiert
    • es ist ein nicht statisches Datenelement eines Objekts, dessen Lebensdauer mit der Auswertung von e begann;

Man kann immer ausführlicher schreiben

// Example 3  --  limited

using Size = ptrdiff_t;

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = std::extent< decltype( c ) >::value;
    // Use c here
}

... aber dies schlägt fehl, wenn Collection kein unformatiertes Array ist.

To deal with collections that can be non-arrays one needs the overloadability of an n_items function, but also, for compile time use one needs a compile time representation of the array size. And the classic C++03 solution, which works fine also in C++11 and C++14, is to let the function report its result not as a value but via its function result type . For example like this:

// Example 4 - OK (not ideal, but portable and safe)

#include <array>
#include <stddef.h>

using Size = ptrdiff_t;

template< Size n >
struct Size_carrier
{
    char sizer[n];
};

template< class Type, Size n >
auto static_n_items( Type (&)[n] )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

template< class Type, size_t n >        // size_t for g++
auto static_n_items( std::array<Type, n> const& )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

#define STATIC_N_ITEMS( c ) \
    static_cast<Size>( sizeof( static_n_items( c ).sizer ) )

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = STATIC_N_ITEMS( c );
    // Use c here
    (void) c;
}

auto main() -> int
{
    int x[42];
    std::array<int, 43> y;
    foo( x );
    foo( y );
}

About the choice of return type for static_n_items : this code doesn't use std::integral_constant because with std::integral_constant the result is represented directly as a constexpr value, reintroducing the original problem. Instead of a Size_carrier class one can let the function directly return a reference to an array. However, not everybody is familiar with that syntax.

About the naming: part of this solution to the constexpr -invalid-due-to-reference problem is to make the choice of compile time constant explicit.

Hopefully the oops-there-was-a-reference-involved-in-your- constexpr issue will be fixed with C++17, but until then a macro like the STATIC_N_ITEMS above yields portability, eg to the clang and Visual C++ compilers, retaining type safety.

Related: macros do not respect scopes, so to avoid name collisions it can be a good idea to use a name prefix, eg MYLIB_STATIC_N_ITEMS .







c++ copy-constructor assignment-operator c++-faq copy-and-swap