Gab es jemals stille Verhaltensänderungen in C ++ mit neuen Standardversionen?

104

(Ich suche ein oder zwei Beispiele, um den Punkt zu beweisen, keine Liste.)

War es jemals so, dass eine Änderung des C ++ - Standards (z. B. von 98 auf 11, 11 auf 14 usw.) das Verhalten des vorhandenen, wohlgeformten Benutzercodes mit definiertem Verhalten im Stillen veränderte? dh ohne Warnung oder Fehler beim Kompilieren mit der neueren Standardversion?

Anmerkungen:

  • Ich frage nach standardbasiertem Verhalten, nicht nach der Auswahl der Autoren von Implementierern / Compilern.
  • Je weniger der Code erfunden ist, desto besser (als Antwort auf diese Frage).
  • Ich meine nicht Code mit Versionserkennung wie #if __cplusplus >= 201103L.
  • Antworten, die das Speichermodell betreffen, sind in Ordnung.
einpoklum
quelle
Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Samuel Liew
3
Ich verstehe nicht, warum diese Frage geschlossen ist. " Gab es jemals stille Verhaltensänderungen in C ++ mit neuen Standardversionen? " Scheint perfekt fokussiert zu sein und der Hauptteil der Frage scheint nicht davon abzuweichen.
Ted Lyngmo
In meinen Augen ist die größte stille Veränderung die Neudefinition von auto. Vor C ++ 11 wurde ein auto x = ...;deklariert int. Danach erklärt es, was auch immer ...ist.
Raymond Chen
@RaymondChen: Diese Änderung ist nur dann stumm, wenn Sie implizit Ints definiert haben, aber explizit die autoVariablen vom Typ "wer" angegeben haben. Ich denke, Sie könnten wahrscheinlich einerseits die Anzahl der Menschen auf der Welt zählen, die diese Art von Code schreiben würden, mit Ausnahme der verschleierten C-Code-Wettbewerbe ...
einpoklum
Es stimmt, deshalb haben sie es gewählt. Aber es war eine große Veränderung in der Semantik.
Raymond Chen

Antworten:

113

Der Rückgabetyp string::dataändert sich von const char*zu char*in C ++ 17. Das könnte sicherlich einen Unterschied machen

void func(char* data)
{
    cout << data << " is not const\n";
}

void func(const char* data)
{
    cout << data << " is const\n";
}

int main()
{
    string s = "xyz";
    func(s.data());
}

Ein bisschen erfunden, aber dieses legale Programm würde seine Ausgabe von C ++ 14 auf C ++ 17 ändern.

John
quelle
7
Oh, ich habe nicht einmal bemerkt, dass es std::stringÄnderungen für C ++ 17 gibt. Wenn überhaupt, hätte ich gedacht, dass die C ++ 11-Änderungen irgendwie zu einer stillen Verhaltensänderung geführt haben könnten. +1.
Einpoklum
9
Erfunden oder nicht, dies zeigt eine Änderung des wohlgeformten Codes ziemlich gut.
David C. Rankin
Abgesehen davon basiert die Änderung auf lustigen, aber legitimen Anwendungsfällen, wenn Sie den Inhalt eines std :: string vor Ort ändern , möglicherweise durch ältere Funktionen, die mit char * ausgeführt werden. Das ist jetzt völlig legitim: Wie bei einem Vektor gibt es eine Garantie dafür, dass es ein zugrunde liegendes, zusammenhängendes Array gibt, das Sie manipulieren können (Sie könnten es immer durch zurückgegebene Referenzen tun; jetzt ist es natürlicher und expliziter). Mögliche Anwendungsfälle sind bearbeitbare Datensätze mit fester Länge (z. B. Nachrichten), die, wenn sie auf einem std :: container basieren, die Dienste der STL wie Lebenszeitmanagement, Kopierbarkeit usw. beibehalten.
Peter - Reinstate Monica
81

Die Antwort auf diese Frage zeigt, wie das Initialisieren eines Vektors mit einem einzelnen size_typeWert zu einem unterschiedlichen Verhalten zwischen C ++ 03 und C ++ 11 führen kann.

std::vector<Something> s(10);

C ++ 03 erstellt standardmäßig ein temporäres Objekt des Elementtyps Somethingund kopiert jedes Element im Vektor aus diesem temporären Objekt .

C ++ 11 erstellt standardmäßig jedes Element im Vektor.

In vielen (den meisten?) Fällen führen diese zu einem äquivalenten Endzustand, aber es gibt keinen Grund dafür. Dies hängt von der Implementierung der SomethingStandard- / Kopierkonstruktoren ab.

Siehe dieses erfundene Beispiel :

class Something {
private:
    static int counter;

public:
    Something() : v(counter++) {
        std::cout << "default " << v << '\n';
    }

    Something(Something const & other) : v(counter++) {
        std::cout << "copy " << other.v << " to " << v << '\n';
    }

    ~Something() {
        std::cout << "dtor " << v << '\n';
    }

private:
    int v;
};

int Something::counter = 0;

C ++ 03 erstellt standardmäßig eine Somethingund v == 0dann zehn weitere aus dieser. Am Ende enthält der Vektor zehn Objekte mit den vWerten 1 bis einschließlich 10.

C ++ 11 erstellt standardmäßig jedes Element. Es werden keine Kopien angefertigt. Am Ende enthält der Vektor zehn Objekte mit den vWerten 0 bis einschließlich 9.

cdhowie
quelle
@einpoklum Ich habe jedoch ein erfundenes Beispiel hinzugefügt. :)
cdhowie
3
Ich denke nicht, dass es erfunden ist. Verschiedene Konstruktoren verhalten sich häufig unterschiedlich, beispielsweise bei der Speicherzuweisung. Sie haben gerade eine Nebenwirkung durch eine andere ersetzt (E / A).
Einpoklum
17
@cdhowie Überhaupt nicht erfunden. Ich habe kürzlich an einer UUID-Klasse gearbeitet. Der Standardkonstruktor hat eine zufällige UUID generiert. Ich hatte keine Ahnung von dieser Möglichkeit, ich habe nur das C ++ 11-Verhalten angenommen.
John
5
Ein weit verbreitetes Beispiel für eine Klasse in der Praxis, in der dies von Bedeutung wäre, ist OpenCV cv::mat. Der Standardkonstruktor weist neuen Speicher zu, während der Kopierkonstruktor dem vorhandenen Speicher eine neue Ansicht erstellt.
jpa
Ich würde das nicht als erfundenes Beispiel bezeichnen, es zeigt deutlich den Unterschied im Verhalten.
David Waterworth
51

Die Norm enthält eine Liste der wichtigsten Änderungen in Anhang C [diff] . Viele dieser Änderungen können zu stillen Verhaltensänderungen führen.

Ein Beispiel:

int f(const char*); // #1
int f(bool);        // #2

int x = f(u8"foo"); // until C++20: calls #1; since C++20: calls #2
cpplearner
quelle
7
@einpoklum Nun, mindestens ein Dutzend von ihnen soll die Bedeutung des vorhandenen Codes "ändern" oder sie "anders ausführen" lassen.
cpplearner
4
Wie würden Sie die Gründe für diese besondere Änderung zusammenfassen?
Nayuki
4
@Nayuki ziemlich sicher, dass die Verwendung der boolVersion per se keine beabsichtigte Änderung war, sondern nur ein Nebeneffekt anderer Konvertierungsregeln. Die eigentliche Absicht wäre es, die Verwirrung zwischen den Zeichenkodierungen zu stoppen. Die eigentliche Änderung besteht darin, dass u8Literale früher gaben const char*, jetzt aber geben const char8_t*.
Links um den
25

Jedes Mal, wenn sie der Standardbibliothek neue Methoden (und häufig Funktionen) hinzufügen, geschieht dies.

Angenommen, Sie haben einen Standardbibliothekstyp:

struct example {
  void do_stuff() const;
};

ziemlich einfach. In einigen Standardrevisionen wird eine neue Methode oder Überladung oder neben irgendetwas hinzugefügt:

struct example {
  void do_stuff() const;
  void method(); // a new method
};

Dies kann das Verhalten vorhandener C ++ - Programme stillschweigend ändern.

Dies liegt daran, dass die derzeit eingeschränkten Reflexionsfunktionen von C ++ ausreichen, um festzustellen, ob eine solche Methode vorhanden ist, und basierend darauf anderen Code auszuführen.

template<class T, class=void>
struct detect_new_method : std::false_type {};

template<class T>
struct detect_new_method< T, std::void_t< decltype( &T::method ) > > : std::true_type {};

Dies ist nur ein relativ einfacher Weg, um das Neue zu erkennen method. Es gibt unzählige Möglichkeiten.

void task( std::false_type ) {
  std::cout << "old code";
};
void task( std::true_type ) {
  std::cout << "new code";
};

int main() {
  task( detect_new_method<example>{} );
}

Das gleiche kann passieren, wenn Sie Methoden aus Klassen entfernen.

Während dieses Beispiel direkt die Existenz einer Methode erkennt, kann diese Art von indirektem Geschehen weniger erfunden werden. Als konkretes Beispiel könnten Sie eine Serialisierungs-Engine haben, die entscheidet, ob etwas als Container serialisiert werden kann, basierend darauf, ob es iterierbar ist, oder ob es Daten enthält, die auf Rohbytes zeigen, und ein Größenelement, wobei eines bevorzugt wird das andere.

Der Standard fügt einem .data()Container eine Methode hinzu , und plötzlich ändert der Typ, welcher Pfad für die Serialisierung verwendet wird.

Alles, was der C ++ - Standard tun kann, wenn er nicht einfrieren möchte, ist, die Art von Code, der lautlos bricht, selten oder irgendwie unvernünftig zu machen.

Yakk - Adam Nevraumont
quelle
3
Ich hätte die Frage qualifizieren sollen, um SFINAE auszuschließen, weil dies nicht ganz das ist, was ich meinte ... aber ja, das stimmt, also +1.
Einpoklum
"so etwas passiert indirekt" führte eher zu einer Aufwertung als zu einer Abwertung, da es sich um eine echte Falle handelt.
Ian Ringrose
1
Dies ist ein wirklich gutes Beispiel. Obwohl OP es nicht ausschließen, dies ist wahrscheinlich eines der am meisten wahrscheinlich Dinge zu stillen Verhalten Änderungen an bestehenden Code führen. +1
cdhowie
1
@TedLyngmo Wenn Sie den Detektor nicht reparieren können, ändern Sie das erkannte Objekt. Texas Scharfschießen!
Yakk - Adam Nevraumont
15

Oh Mann ... Der bereitgestellte Link cpplearner ist beängstigend .

Unter anderem hat C ++ 20 die C-artige Strukturdeklaration von C ++ - Strukturen nicht zugelassen.

typedef struct
{
  void member_foo(); // Ill-formed since C++20
} m_struct;

Wenn Ihnen beigebracht wurde, solche Strukturen zu schreiben (und Leute, die "C mit Klassen" unterrichten, unterrichten genau das), sind Sie fertig .

Niemand überhaupt
quelle
19
Wer das gelehrt hat, sollte 100 mal an die Tafel schreiben "Ich werde keine Strukturen tippen". Du solltest es nicht einmal in C machen, imho. Diese Änderung ist jedoch nicht stillschweigend: In dem neuen Standard kann "Gültiger C ++ 2017-Code (unter Verwendung von typedef für anonyme Nicht-C-Strukturen) fehlerhaft sein" und "schlecht geformt - das Programm weist Syntaxfehler oder diagnostizierbare semantische Fehler auf . ein konformer C ++ Kompilierer ist erforderlich , um eine Diagnose“auszustellen .
Peter - Monica
19
@ Peter-ReinstateMonica Nun, ich habe immer typedefmeine Strukturen und ich werde mit Sicherheit meine Kreide nicht damit verschwenden. Dies ist definitiv eine Frage des Geschmacks, und während es sehr einflussreiche Leute (Torvalds ...) gibt, die Ihre Sichtweise teilen, werden andere Leute wie ich darauf hinweisen, dass eine Namenskonvention für Typen alles ist, was benötigt wird. Das Überladen des Codes mit structSchlüsselwörtern trägt wenig zum Verständnis bei, dass ein Großbuchstabe ( MyClass* object = myClass_create();) nicht vermittelt. Ich respektiere es, wenn Sie das structin Ihrem Code wollen. Aber ich will es nicht in meinem.
cmaster
5
Das heißt, wenn Sie C ++ programmieren, ist es in der Tat eine gute Konvention, structnur für einfache alte Datentypen und classalles, was Mitgliedsfunktionen hat, zu verwenden. Aber Sie können diese Konvention in C nicht verwenden, da es in C keine gibt class.
cmaster - stellen Sie Monica
1
@ Peter-ReinstateMonica Ja, Sie können eine Methode in C nicht syntaktisch anhängen, aber das bedeutet nicht, dass ein C structtatsächlich POD ist. So wie ich C-Code schreibe, werden die meisten Strukturen nur durch Code in einer einzelnen Datei und durch Funktionen berührt, die den Namen ihrer Klasse tragen. Es ist im Grunde OOP ohne den syntaktischen Zucker. Auf diese Weise kann ich tatsächlich steuern, welche Änderungen innerhalb eines structund welche Invarianten zwischen seinen Mitgliedern garantiert sind. Daher structstendiere ich dazu, Mitgliedsfunktionen, private Implementierung, Invarianten und Abstracts von ihren Datenmitgliedern zu haben. Klingt nicht nach POD, oder?
cmaster
5
Solange sie nicht in extern "C"Blöcken verboten sind, sehe ich kein Problem mit dieser Änderung. Niemand sollte Strukturen in C ++ typisieren. Dies ist keine größere Hürde als die Tatsache, dass C ++ eine andere Semantik als Java hat. Wenn Sie eine neue Programmiersprache lernen, müssen Sie möglicherweise einige neue Gewohnheiten lernen.
Cody Grey
15

Hier ist ein Beispiel, das 3 in C ++ 03, aber 0 in C ++ 11 druckt:

template<int I> struct X   { static int const c = 2; };
template<> struct X<0>     { typedef int c; };
template<class T> struct Y { static int const c = 3; };
static int const c = 4;
int main() { std::cout << (Y<X< 1>>::c >::c>::c) << '\n'; }

Diese Verhaltensänderung wurde durch spezielle Behandlung für verursacht >>. Vor C ++ 11 >>war immer der richtige Schichtoperator. >>Kann mit C ++ 11 auch Teil einer Vorlagendeklaration sein.

Waxrat
quelle
Nun, technisch gesehen ist dies wahr, aber dieser Code war aufgrund der Verwendung >>dieser Methode zunächst "informell mehrdeutig" .
Einpoklum
11

Trigraphen fielen

Quelldateien werden in einem physischen Zeichensatz codiert, der implementierungsdefiniert dem im Standard definierten Quellzeichensatz zugeordnet wird. Um Zuordnungen von einigen physischen Zeichensätzen zu berücksichtigen, die nicht die gesamte vom Quellzeichensatz benötigte Interpunktion hatten, definierte die Sprache Trigraphen - Sequenzen von drei gemeinsamen Zeichen, die anstelle eines weniger gebräuchlichen Interpunktionszeichens verwendet werden könnten. Der Präprozessor und der Compiler mussten diese verarbeiten.

In C ++ 17 wurden Trigraphen entfernt. Daher werden einige Quelldateien von neueren Compilern nur akzeptiert, wenn sie zuerst vom physischen Zeichensatz in einen anderen physischen Zeichensatz übersetzt werden, der dem Quellzeichensatz eins zu eins zugeordnet ist. (In der Praxis haben die meisten Compiler die Interpretation von Trigraphen nur optional gemacht.) Dies ist keine subtile Verhaltensänderung, sondern eine grundlegende Änderung, die verhindert, dass zuvor akzeptable Quelldateien ohne einen externen Übersetzungsprozess kompiliert werden.

Weitere Einschränkungen für char

Der Standard bezieht sich auch auf den Ausführungszeichensatz , der implementierungsdefiniert ist, jedoch mindestens den gesamten Quellzeichensatz sowie eine kleine Anzahl von Steuercodes enthalten muss.

Der C ++ - Standard, der charals möglicherweise vorzeichenloser Integraltyp definiert ist und jeden Wert im Ausführungszeichensatz effizient darstellen kann. Mit der Darstellung eines Sprachrechtsanwalts können Sie argumentieren, dass a charmindestens 8 Bit sein muss.

Wenn Ihre Implementierung einen vorzeichenlosen Wert für verwendet char, wissen Sie, dass dieser zwischen 0 und 255 liegen kann und daher zum Speichern jedes möglichen Bytewerts geeignet ist.

Wenn Ihre Implementierung jedoch einen signierten Wert verwendet, stehen Optionen zur Verfügung.

Die meisten würden das Zweierkomplement verwenden, was chareinen Mindestbereich von -128 bis 127 ergibt. Das sind 256 eindeutige Werte.

Eine andere Option war Vorzeichen + Größe, wobei ein Bit reserviert ist, um anzuzeigen, ob die Zahl negativ ist, und die anderen sieben Bits die Größe angeben. Dies würde chareinen Bereich von -127 bis 127 ergeben, was nur 255 eindeutigen Werten entspricht. (Weil Sie eine nützliche Bitkombination verlieren, um -0 darzustellen.)

Ich bin nicht sicher , dass der Ausschuß immer dies als Mangel ausdrücklich bezeichnet, aber es war , weil Sie nicht auf dem Standard verlassen konnten eine Hin- und Rückfahrt zu gewährleisten , von unsigned charzu charund würde wieder den ursprünglichen Wert erhalten. (In der Praxis haben alle Implementierungen dies getan, weil sie alle das Zweierkomplement für vorzeichenbehaftete Integraltypen verwendeten.)

Erst kürzlich (C ++ 17?) Wurde der Wortlaut festgelegt, um eine Rundauslösung zu gewährleisten. Dieser Fix, zusammen mit allen anderen Anforderungen an char, schreibt effektiv das Zweierkomplement für signierte charZeichen vor, ohne dies ausdrücklich zu sagen (auch wenn der Standard weiterhin Vorzeichen + Größenrepräsentationen für andere signierte Integraltypen zulässt). Es gibt einen Vorschlag, wonach alle signierten Integraltypen das Zweierkomplement verwenden müssen, aber ich kann mich nicht erinnern, ob es in C ++ 20 geschafft hat.

Dies ist also genau das Gegenteil von dem, wonach Sie suchen, da es zuvor falschen, übermäßig anmaßenden Code eine rückwirkende Korrektur gibt.

Adrian McCarthy
quelle
Der Trigraphenteil ist keine Antwort auf diese Frage - das ist keine stille Änderung. Und, IIANM, der zweite Teil ist eine Änderung des implementierungsdefinierten zu einem streng vorgeschriebenen Verhalten, nach dem ich auch nicht gefragt habe.
Einpoklum
10

Ich bin mir nicht sicher, ob Sie dies als eine bahnbrechende Änderung des korrekten Codes betrachten würden, aber ...

Vor C ++ 11 war es Compilern unter bestimmten Umständen gestattet, aber nicht erforderlich, Kopien zu entfernen, selbst wenn der Kopierkonstruktor beobachtbare Nebenwirkungen aufweist. Jetzt haben wir die Kopierentscheidung garantiert. Das Verhalten ging im Wesentlichen von implementierungsdefiniert zu erforderlich über.

Dies bedeutet, dass die Nebenwirkungen Ihres Kopierkonstruktors möglicherweise bei älteren Versionen aufgetreten sind, bei neueren jedoch niemals . Sie könnten argumentieren, dass der richtige Code nicht auf implementierungsdefinierten Ergebnissen beruhen sollte, aber ich denke nicht, dass dies das Gleiche ist, als würde man sagen, dass ein solcher Code falsch ist.

Adrian McCarthy
quelle
1
Ich dachte, diese "Anforderung" wurde in C ++ 17 hinzugefügt, nicht in C ++ 11? (Siehe vorübergehende Materialisierung .)
cdhowie
@cdhowie: Ich denke du hast recht. Ich hatte die Standards nicht zur Hand, als ich das schrieb, und ich habe wahrscheinlich zu viel Vertrauen in einige meiner Suchergebnisse gesetzt.
Adrian McCarthy
Eine Änderung des implementierungsdefinierten Verhaltens zählt nicht als Antwort auf diese Frage.
Einpoklum
7

Das Verhalten beim Lesen (numerischer) Daten aus einem Stream und beim Fehlschlagen des Lesens wurde seit c ++ 11 geändert.

Beispiel: Lesen einer Ganzzahl aus einem Stream, die keine Ganzzahl enthält:

#include <iostream>
#include <sstream>

int main(int, char **) 
{
    int a = 12345;
    std::string s = "abcd";         // not an integer, so will fail
    std::stringstream ss(s);
    ss >> a;
    std::cout << "fail = " << ss.fail() << " a = " << a << std::endl;        // since c++11: a == 0, before a still 12345 
}

Da c ++ 11 die gelesene Ganzzahl auf 0 setzt, wenn dies fehlschlägt; Bei c ++ <11 wurde die Ganzzahl nicht geändert. Trotzdem zeigt gcc, selbst wenn der Standard auf c ++ 98 zurückgesetzt wird (mit -std = c ++ 98), zumindest seit Version 4.4.7 immer ein neues Verhalten.

(Imho war das alte Verhalten eigentlich besser: Warum den Wert auf 0 ändern, was für sich genommen gültig ist, wenn nichts gelesen werden konnte?)

Referenz: siehe https://en.cppreference.com/w/cpp/locale/num_get/get

DanRechtsaf
quelle
Es wird jedoch keine Änderung bezüglich returnType erwähnt. Nur 2 Nachrichtenüberflutung seit C ++ 11 verfügbar
Build erfolgreich
War dieses Verhalten sowohl in C ++ 98 als auch in C ++ 11 definiert? Oder wurde das Verhalten definiert?
Einpoklum
Wenn cppreference.com richtig ist: "Wenn ein Fehler auftritt, bleibt v unverändert. (Bis C ++ 11)" Das Verhalten wurde also vor C ++ 11 definiert und geändert.
DanRechtsaf
Nach meinem Verständnis wurde das Verhalten für ss> a zwar definiert, aber für den sehr häufigen Fall, dass Sie in eine nicht initialisierte Variable lesen, verwendet das c ++ 11-Verhalten eine nicht initialisierte Variable, bei der es sich um undefiniertes Verhalten handelt. Die Standardkonstruktion für Fehler schützt somit vor einem sehr häufigen undefinierten Verhalten.
Rasmus Damgaard Nielsen