Was ist der Grund, warum C ++ 17s [[nodiscard]] in neuem Code fast überall nicht verwendet wird?

70

C ++ 17 führt das [[nodiscard]]Attribut ein, mit dem Programmierer Funktionen so kennzeichnen können, dass der Compiler eine Warnung ausgibt, wenn das zurückgegebene Objekt von einem Aufrufer verworfen wird. Das gleiche Attribut kann einem gesamten Klassentyp hinzugefügt werden.

Ich habe im ursprünglichen Vorschlag über die Motivation für dieses Feature gelesen und weiß, dass C ++ 20 das Attribut zu Standardfunktionen wie hinzufügen wird std::vector::empty, deren Namen keine eindeutige Bedeutung in Bezug auf den Rückgabewert vermitteln.

Es ist eine coole und nützliche Funktion. In der Tat scheint es fast zu nützlich. Überall, wo ich darüber lese [[nodiscard]], diskutieren die Leute darüber, als würden Sie es nur einigen ausgewählten Funktionen oder Typen hinzufügen und den Rest vergessen. Aber warum sollte ein nicht verwerfbarer Wert ein Sonderfall sein, insbesondere beim Schreiben von neuem Code? Ist ein verworfener Rückgabewert nicht in der Regel ein Fehler oder zumindest eine Verschwendung von Ressourcen?

Und gehört es nicht zu den Entwurfsprinzipien von C ++, dass der Compiler so viele Fehler wie möglich abfängt?

Wenn ja, warum fügen [[nodiscard]]Sie dann nicht zu fast jeder einzelnen Nichtfunktion voidund zu fast jeder einzelnen Klassenart Ihren eigenen Code hinzu , der kein Legacy-Code ist ?

Ich habe versucht, das in meinem eigenen Code zu tun, und es funktioniert gut, außer es ist so schrecklich ausführlich, dass es sich wie Java anfühlt. Es erscheint viel natürlicher, Compiler standardmäßig vor verworfenen Rückgabewerten warnen zu lassen , mit Ausnahme der wenigen anderen Fälle, in denen Sie Ihre Absicht markieren [*] .

Da ich in Standardvorschlägen, Blogeinträgen, Stack Overflow-Fragen oder irgendwo anders im Internet keine Diskussionen über diese Möglichkeit gesehen habe, muss mir etwas fehlen.

Warum macht eine solche Mechanik in neuem C ++ - Code keinen Sinn? Ist Ausführlichkeit der einzige Grund, nicht [[nodiscard]]fast überall zu verwenden?


[*] Theoretisch könnten Sie [[maydiscard]]stattdessen so etwas wie ein Attribut haben, das auch rückwirkend zu Funktionen wie printfin Standardbibliotheksimplementierungen hinzugefügt werden kann .

Christian Hackl
quelle
22
Nun gibt es eine ganze Reihe von Funktionen , bei denen der Rückgabewert kann sinnvoll entsorgt werden. operator =zum Beispiel. Und std::map::insert.
user253751
2
@immibis: Stimmt. Die Standardbibliothek ist voll von solchen Funktionen. Aber in meinem eigenen Code sind sie äußerst selten. Denken Sie, dass es sich um Bibliothekscode oder Anwendungscode handelt?
Christian Hackl
9
"... furchtbar ausführlich, dass es sich wie Java anfühlt ..." - als ich dies in einer Frage zu C ++
las, zuckte
3
@ChristianHackl Die Kommentare sind wahrscheinlich nicht der richtige Ort für "Sprachbashing", und natürlich ist Java im Vergleich zu anderen Sprachen auch ausführlich. Aber Header-Dateien, Zuweisungsoperatoren, Kopierkonstruktoren und die ordnungsgemäße Verwendung von constkönnen eine ansonsten "einfache" Klasse (oder vielmehr "einfaches altes Datenobjekt") in C ++ erheblich aufblähen lassen .
Marco13
2
@ Marco13: Ich kann nicht so viele Punkte in einem Kommentar ansprechen. Machen wir es kurz: Java hat keine Header-Dateien, was die Anzahl der Dateien verringert, aber die Kopplung erhöht. OTOH zwingt Sie, jede Klasse der obersten Ebene in eine eigene Datei und jede Funktion in eine Klasse zu setzen. In C ++ implementieren Sie fast nie selbst Zuweisungsoperatoren und Kopierkonstruktoren. Diese Funktionen sind relevanter für Standardbibliotheksklassen wie std::vectoroder std::unique_ptr, von denen Sie nur Datenelemente in Ihrer Klassendefinition benötigen. Ich habe mit beiden Sprachen gearbeitet; Java ist eine okayische Sprache, aber ausführlicher.
Christian Hackl

Antworten:

73

Verwenden Sie in neuem Code, der nicht mit älteren Standards kompatibel sein muss, dieses Attribut, wo immer es sinnvoll ist. Aber für C ++, [[nodiscard]]macht einen schlechten Standard. Du schlägst vor:

Es erscheint viel natürlicher, Compiler standardmäßig vor verworfenen Rückgabewerten warnen zu lassen, mit Ausnahme der wenigen anderen Fälle, in denen Sie Ihre Absicht markieren.

Dies würde plötzlich dazu führen, dass vorhandener, korrekter Code viele Warnungen ausgibt. Während eine solche Änderung technisch als abwärtskompatibel angesehen werden könnte, da vorhandener Code immer noch erfolgreich kompiliert wird, wäre dies in der Praxis eine enorme Änderung der Semantik.

Entwurfsentscheidungen für eine vorhandene, ausgereifte Sprache mit einer sehr großen Codebasis unterscheiden sich notwendigerweise von Entwurfsentscheidungen für eine vollständig neue Sprache. Wenn dies eine neue Sprache wäre, wäre eine Warnung standardmäßig sinnvoll. In der Sprache Nim müssen beispielsweise nicht benötigte Werte explizit verworfen werden. Dies ähnelt dem Umbrechen jeder Ausdrucksanweisung in C ++ mit einer Umwandlung (void)(...).

Ein [[nodiscard]]Attribut ist in zwei Fällen am nützlichsten:

  • wenn eine Funktion keine Auswirkungen hat, über die Rückgabe eines bestimmten Ergebnisses hinaus, dh rein ist. Wenn das Ergebnis nicht verwendet wird, ist der Aufruf sicherlich unbrauchbar. Andererseits wäre es nicht falsch, das Ergebnis zu verwerfen.

  • ob der Rückgabewert überprüft werden muss, z. B. für eine C-ähnliche Schnittstelle, die Fehlercodes zurückgibt, anstatt sie auszulösen. Dies ist der primäre Anwendungsfall. Für idiomatisches C ++ wird das ziemlich selten sein.

Diese beiden Fälle hinterlassen einen riesigen Bereich unreiner Funktionen, die einen Wert zurückgeben, bei dem eine solche Warnung irreführend wäre. Zum Beispiel:

  • Stellen Sie sich einen Warteschlangendatentyp mit einer .pop()Methode vor, die ein Element entfernt und eine Kopie des entfernten Elements zurückgibt. Eine solche Methode ist oftmals zweckmäßig. In einigen Fällen möchten wir jedoch nur das Element entfernen, ohne eine Kopie zu erhalten. Das ist absolut legitim und eine Warnung wäre nicht hilfreich. Ein anderes Design (wie std::vector) teilt diese Verantwortlichkeiten auf, aber das hat andere Nachteile.

    Beachten Sie, dass in einigen Fällen eine Kopie angefertigt werden muss, sodass die Rücksendung der Kopie dank RVO kostenlos wäre.

  • Betrachten Sie fließende Schnittstellen, bei denen jede Operation das Objekt zurückgibt, damit weitere Operationen ausgeführt werden können. In C ++ ist der Operator zum Einfügen von Streams das häufigste Beispiel <<. Es wäre äußerst umständlich, [[nodiscard]]jeder <<Überladung ein Attribut hinzuzufügen .

Diese Beispiele zeigen, dass das Kompilieren von idiomatischem C ++ - Code ohne Warnungen unter einer Sprache "C ++ 17 mit standardmäßigem NODISCARD" ziemlich mühsam wäre.

Beachten Sie, dass Ihr glänzender neuer C ++ 17-Code (in dem Sie diese Attribute verwenden können) möglicherweise weiterhin zusammen mit Bibliotheken kompiliert wird, die auf ältere C ++ - Standards abzielen. Diese Abwärtskompatibilität ist für das C / C ++ - Ökosystem von entscheidender Bedeutung. Wenn Sie also nodiscard als Standard festlegen, werden für typische, idiomatische Anwendungsfälle viele Warnungen ausgegeben, die Sie nicht beheben können, ohne den Quellcode der Bibliothek grundlegend zu ändern.

Das Problem hierbei ist wahrscheinlich nicht die Änderung der Semantik, sondern die Tatsache, dass die Funktionen jedes C ++ - Standards für einen Bereich pro Kompilierungseinheit gelten und nicht für einen Bereich pro Datei. Wenn sich ein zukünftiger C ++ - Standard von den Header-Dateien entfernt, wäre eine solche Änderung realistischer.

amon
quelle
6
Ich habe dies positiv bewertet, da es nützliche Erkenntnisse enthält. Ich bin mir jedoch noch nicht sicher, ob es die Frage wirklich beantwortet. Bei unreinen Funktionen wie popoder operator<<werden diese explizit durch mein imaginäres [[maydiscard]]Attribut gekennzeichnet, sodass keine Warnungen generiert werden. Wollen Sie damit sagen, dass solche Funktionen im Allgemeinen viel häufiger sind, als ich glauben möchte, oder im Allgemeinen viel häufiger als in meinen eigenen Codebasen? Ihr erster Absatz über vorhandenen Code ist sicherlich richtig, aber ich frage mich immer noch, ob derzeit jemand anderen seinen gesamten neuen Code mit [[nodiscard]]und wenn nicht, warum nicht peppt .
Christian Hackl
23
@ChristianHackl: Es gab eine Zeit in C ++, in der es als bewährte Methode galt, Werte als const zurückzugeben. Man kann nicht sagen returns_an_int() = 0, warum sollte das returns_a_str() = ""funktionieren? C ++ 11 zeigte, dass dies tatsächlich eine schreckliche Idee war, da dieser Code nun alle Bewegungen untersagte, da die Werte const waren. Ich denke, die richtige Lösung für diese Lektion ist "Es liegt nicht an Ihnen, was Ihre Anrufer mit Ihrem Ergebnis machen". Das Ignorieren des Ergebnisses ist eine absolut gültige Sache, die Anrufer möglicherweise tun möchten. Wenn Sie nicht wissen, dass es falsch ist (ein sehr hoher Balken), sollten Sie es nicht blockieren.
GManNickG
13
Ein Nodiscard on empty()ist auch deshalb sinnvoll, weil der Name nicht eindeutig ist: Ist es ein Prädikat is_empty()oder ein Mutator clear()? Sie würden normalerweise nicht erwarten, dass ein solcher Mutator irgendetwas zurückgibt, daher kann eine Warnung über einen Rückgabewert den Programmierer auf ein Problem aufmerksam machen. Mit einem klareren Namen wäre dieses Attribut nicht erforderlich.
Am
13
@ChristianHackl: Wie andere gesagt haben, denke ich, dass reine Funktionen ein gutes Beispiel dafür sind, wann dies verwendet werden sollte. Ich würde noch einen Schritt weiter gehen und sagen, es sollte ein [[pure]]Attribut geben, und das sollte implizieren [[nodiscard]]. Auf diese Weise ist klar, warum dieses Ergebnis nicht verworfen werden sollte. Aber abgesehen von reinen Funktionen kann ich mir keine andere Klasse von Funktionen vorstellen, die dieses Verhalten haben sollten. Und die meisten Funktionen sind nicht rein, daher denke ich nicht, dass NODISCARD als Standard die richtige Wahl ist.
GManNickG
5
@GManNickG: Nachdem versucht und fehlgeschlagen Debug - Code, der Fehlercodes ignoriert, ich stark glaube , dass die überwiegende Mehrheit der Fehlercodes müssen standardmäßig überprüft werden.
Mooing Duck
9

Gründe, warum ich nicht [[nodiscard]]fast überall wäre:

  1. (major :) Dies würde einführen Art und Weise zu viel Lärm in meinem Header.
  2. (major :) Ich habe nicht das Gefühl, dass ich starke spekulative Annahmen über den Code anderer Leute machen sollte. Wollen Sie den Rückgabewert, den ich Ihnen gebe, verwerfen? Gut, hau ab.
  3. (minor :) Sie würden eine Inkompatibilität mit C ++ 14 garantieren

Wenn Sie jetzt die Standardeinstellung festlegen, werden alle Bibliotheksentwickler gezwungen, alle Benutzer dazu zu zwingen, die Rückgabewerte nicht zu verwerfen. Das wäre schrecklich. Oder du zwingst sie, [[maydiscard]]unzählige Funktionen hinzuzufügen , was auch irgendwie schrecklich ist.

einpoklum
quelle
0

Als Beispiel: Operator << hat einen Rückgabewert, der je nach Aufruf entweder absolut benötigt oder absolut unbrauchbar ist. (std :: cout << x << y, der erste wird benötigt, weil er den Stream zurückgibt, der zweite ist überhaupt nicht von Nutzen). Vergleichen Sie nun mit printf, wo jeder den Rückgabewert verwirft, der für die Fehlerprüfung zur Verfügung steht, aber dann hat der Operator << keine Fehlerprüfung, so dass er überhaupt nicht so nützlich gewesen sein kann. Nodiscard wäre also in beiden Fällen nur schädlich.

gnasher729
quelle
Dies beantwortet eine Frage, die nicht gestellt wurde: "Was ist der Grund, nicht [[nodiscard]]überall zu verwenden?".
Christian Hackl