Seltsame Aufzählung im Destruktor

83

Derzeit lese ich den Quellcode von Protocol Bufferund habe einen seltsamen enumCode gefunden, der hier definiert ist

  ~scoped_ptr() {
    enum { type_must_be_complete = sizeof(C) };
    delete ptr_;
  }

  void reset(C* p = NULL) {
    if (p != ptr_) {
      enum { type_must_be_complete = sizeof(C) };
      delete ptr_;
      ptr_ = p;
    }
  }

Warum enum { type_must_be_complete = sizeof(C) };ist das hier definiert? was wird es verwendet?

zangw
quelle
2
Wenn ich so sicher sein will, dann würde ich mich lieber ptr_im sizeofas als sizeof(*ptr_)verwenden sizeof(C).
Nawaz

Antworten:

81

Dieser Trick vermeidet UB, indem sichergestellt wird, dass die Definition von C verfügbar ist, wenn dieser Destruktor kompiliert wird. Andernfalls schlägt die Kompilierung fehl, da der sizeofunvollständige Typ (vorwärts deklarierte Typen) nicht bestimmt werden kann, aber die Zeiger verwendet werden können.

In der kompilierten Binärdatei würde dieser Code optimiert und hätte keine Auswirkung.

Beachten Sie Folgendes : Das Löschen eines unvollständigen Typs kann ein undefiniertes Verhalten ab 5.3.5 / 5: sein.

Wenn das zu löschende Objekt zum Zeitpunkt des Löschens einen unvollständigen Klassentyp aufweist und die vollständige Klasse über einen nicht trivialen Destruktor oder eine Freigabefunktion verfügt, ist das Verhalten undefiniert .

g++ gibt sogar die folgende Warnung aus:

Warnung: Mögliches Problem beim Aufrufen des Löschoperators festgestellt:
Warnung: 'p' hat unvollständige
Typwarnung: Vorwärtsdeklaration von 'Struktur C'

Mohit Jain
quelle
1
"Das Löschen eines unvollständigen Typs ist ein undefiniertes Verhalten" ist falsch. Es ist nur UB, wenn der Typ einen nicht trivialen Destruktor hat. Das Problem, das mit diesem kleinen Trick behoben wird, ist genau, dass das Löschen eines unvollständigen Typs nicht immer UB ist, sodass die Sprache dies unterstützt.
Prost und hth. - Alf
Danke @ Cheersandhth.-Alf Mein Punkt war, dass es UB sein könnte, also ist diese Codezeile im Allgemeinen UB. Bearbeitet.
Mohit Jain
32

sizeof(C)schlägt beim Kompilieren fehl , wenn Ces sich nicht um einen vollständigen Typ handelt. Wenn Sie einen lokalen Bereich festlegen enum, wird die Anweisung zur Laufzeit harmlos.

Auf diese Weise schützt sich der Programmierer vor sich selbst: Das Verhalten eines nachfolgenden delete ptr_auf einen unvollständigen Typ ist undefiniert, wenn er einen nicht trivialen Destruktor hat.

Bathseba
quelle
1
Können Sie erklären, warum C zu diesem Zeitpunkt ein vollständiger Typ sein muss - ist es erforderlich, die vollständige Typdefinition zu haben, um ihn aufzurufen delete? Und wenn ja, warum fängt der Compiler es überhaupt nicht ab?
Peter Hull
1
Geht es nicht eher darum zu vermeiden C = void? Wenn Ces sich nur um einen undefinierten Typ handeln würde, würde die deleteAnweisung dann nicht bereits fehlschlagen?
Kerrek SB
1
Sieht so aus, als hätte Mohit Jain die Antwort.
Peter Hull
1
−1 "Wenn Sie eine lokale Bereichsaufzählung festlegen, wird die Anweisung zur Laufzeit harmlos." ist bedeutungslos. Es tut mir Leid.
Prost und hth. - Alf
1
@SteveJessop danke. Der Teil, den ich vermisst habe, war, dass das Löschen eines unvollständigen Typs UB ist.
Peter Hull
28

Um das zu verstehen enum, betrachten Sie zunächst den Destruktor ohne ihn:

~scoped_ptr() {
    delete ptr_;
}

wo ptr_ist ein C*. Wenn der Typ Czu diesem Zeitpunkt unvollständig ist, dh der Compiler nur weiß struct C;, dass (1) für die C-Instanz, auf die verwiesen wird , ein standardmäßig generierter Nichtstun-Destruktor verwendet wird. Es ist unwahrscheinlich, dass dies für ein Objekt, das von einem intelligenten Zeiger verwaltet wird, richtig ist.

Wenn das Löschen über einen Zeiger auf einen unvollständigen Typ immer ein undefiniertes Verhalten hatte, könnte der Standard nur verlangen, dass der Compiler es diagnostiziert und fehlschlägt. Aber es ist genau definiert, wenn der eigentliche Destruktor trivial ist: Wissen, das der Programmierer haben kann, der Compiler jedoch nicht. Warum die Sprache dies definiert und zulässt, ist mir ein Rätsel, aber C ++ unterstützt viele Praktiken, die heute nicht als Best Practices angesehen werden.

Ein vollständiger Typ hat eine bekannte Größe und wird daher sizeof(C)genau dann kompiliert, wenn Ces sich um einen vollständigen Typ handelt - mit bekanntem Destruktor. So kann es als Wache verwendet werden. Ein Weg wäre einfach

(void) sizeof(C);  // Type must be complete

Ich würde vermuten, dass der Compiler mit einigen Compilern und Optionen diese optimiert, bevor er feststellen kann, dass sie nicht kompiliert werden sollten und dass dies enumein Weg ist, um ein solches nicht konformes Compilerverhalten zu vermeiden:

enum { type_must_be_complete = sizeof(C) };

Eine alternative Erklärung für die Wahl enumeines Ausdrucks und nicht nur eines verworfenen Ausdrucks ist einfach die persönliche Präferenz.

Oder wie James T. Hugget in einem Kommentar zu dieser Antwort vorschlägt : „Die Aufzählung kann eine Möglichkeit sein, beim Kompilieren eine pseudo-portable Fehlermeldung zu erstellen.“


(1) Der standardmäßig generierte Do-Nothing-Destruktor für einen unvollständigen Typ war ein Problem mit old std::auto_ptr. Es war so heimtückisch, dass es seinen Weg in einen GOTW-Artikel über die PIMPL-Sprache fand , der vom Vorsitzenden des internationalen C ++ - Standardisierungskomitees Herb Sutter verfasst wurde. Natürlich wird man heutzutage, da dies std::auto_ptrveraltet ist, stattdessen einen anderen Mechanismus verwenden.

Prost und hth. - Alf
quelle
4
Die Aufzählung kann eine Möglichkeit sein, beim Kompilieren eine pseudo-portable Fehlermeldung zu erstellen.
Brice M. Dempsey
1
Ich denke, diese Antwort erklärt die Motivation des Codes sehr gut, aber ich möchte hinzufügen, dass (1) einige Compiler sizeof(T)für unvollständige Typen 0 ausgewertet haben, anstatt die Kompilierung fehlzuschlagen. Dies ist jedoch ein nicht konformes Verhalten. (2) Seit C ++ 11 static_assert((sizeof(T) > 0), "T must be a complete type");wäre die Verwendung eine überlegene (und idiomatische) Lösung.
5gon12eder
@ 5gon12eder; Bitte geben Sie ein Beispiel für einen C ++ - Compiler an, der " sizeof(T)für unvollständige Typen auf 0 ausgewertet" hat.
Prost und hth. - Alf
1
Ich habe noch nie einen solchen Compiler verwendet und wäre nicht überrascht zu hören, dass sie inzwischen ausgestorben sind. Und selbst wenn dies nicht der Fall ist, ist es eine gültige Entscheidung, sich nicht um eine nicht konforme Implementierung zu kümmern. Dennoch beide libstdc ++ und libc ++ Verwendung static_assert(sizeof(T) > 0, "…");in ihren jeweiligen Implementierungen std::unique_ptrum sicherzustellen , dass die Art abgeschlossen ist ...
5gon12eder
1
… Also ich denke es ist sicher zu sagen, dass dies idiomatisch ist. Da das Bewerten sizeof(T)in einem booleschen Kontext genau dem Testen entspricht sizeof(T) > 0, spielt es keine Rolle, außer vielleicht aus ästhetischen Gründen.
5gon12eder
3

Vielleicht ist ein Trick definiert, um sicher zu sein C.

Hieronymus
quelle