(Szukam przykładu lub dwóch, aby to udowodnić, a nie listy).
Czy kiedykolwiek zdarzyło się, że zmiana w standardzie C ++ (np. Z 98 na 11, 11 na 14 itd.) Zmieniła zachowanie istniejącego, dobrze sformułowanego kodu użytkownika o zdefiniowanym zachowaniu - po cichu? tj. bez ostrzeżenia lub błędów podczas kompilacji z nowszą wersją standardową?
Uwagi:
- Pytam o zachowanie narzucone przez standardy, a nie o wybory autora implementacji / kompilatora.
- Im mniej wymyślny kod, tym lepiej (jako odpowiedź na to pytanie).
- Nie mam na myśli kodu z wykrywaniem wersji, takiego jak
#if __cplusplus >= 201103L
. - Odpowiedzi dotyczące modelu pamięci są w porządku.
c++
language-lawyer
standardization
einpoklum
źródło
źródło
auto
. Przed C ++ 11auto x = ...;
zadeklarowano plikint
. Następnie deklaruje, co...
jest.auto
podajesz zmienne typu były . Myślę, że pewnie można by z jednej strony policzyć liczbę ludzi na świecie, którzy napisaliby taki kod, z wyjątkiem zawodów w zaciemnionym kodzie C ...Odpowiedzi:
Zwracany typ
string::data
zmian zconst char*
nachar*
w C ++ 17. To z pewnością może mieć znaczenievoid 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()); }
Trochę wymyślony, ale ten legalny program zmieniłby swoje wyjście, przechodząc z C ++ 14 do C ++ 17.
źródło
std::string
zmiany w C ++ 17. Jeśli już, pomyślałbym, że zmiany w C ++ 11 mogły w jakiś sposób spowodować cichą zmianę zachowania. +1.Odpowiedź na to pytanie pokazuje, jak zainicjowanie wektora przy użyciu pojedynczej
size_type
wartości może skutkować różnymi zachowaniami między C ++ 03 i C ++ 11.std::vector<Something> s(10);
C ++ 03 domyślnie konstruuje tymczasowy obiekt typu elementu
Something
i kopiuje każdy element w wektorze z tego tymczasowego.C ++ 11 domyślnie konstruuje każdy element w wektorze.
W wielu (większości?) Przypadkach skutkuje to równoważnym stanem końcowym, ale nie ma powodu, aby tak było. Zależy to od implementacji
Something
domyślnych / kopiujących konstruktorów.Zobacz ten wymyślony przykład :
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 domyślnie skonstruuje jeden
Something
z nich, av == 0
następnie skopiuje dziesięć kolejnych. Na końcu wektor zawiera dziesięć obiektów ov
wartościach od 1 do 10 włącznie.C ++ 11 domyślnie skonstruuje każdy element. Żadne kopie nie są wykonywane. Na końcu wektor zawiera dziesięć obiektów o
v
wartościach od 0 do 9 włącznie.źródło
cv::mat
. Konstruktor domyślny przydziela nową pamięć, podczas gdy konstruktor kopiujący tworzy nowy widok do istniejącej pamięci.Norma zawiera listę istotnych zmian w załączniku C [różn . ] . Wiele z tych zmian może prowadzić do cichej zmiany zachowania.
Przykład:
int f(const char*); // #1 int f(bool); // #2 int x = f(u8"foo"); // until C++20: calls #1; since C++20: calls #2
źródło
bool
wersji nie było zamierzoną zmianą jako taką, tylko efektem ubocznym innych reguł konwersji. Prawdziwym zamiarem byłoby zaprzestanie niejasności między kodowaniem znaków, przy czym faktyczna zmiana polega na tym, żeu8
dawniej dawały literały,const char*
a teraz dająconst char8_t*
.Dzieje się tak za każdym razem, gdy dodają nowe metody (i często funkcje) do biblioteki standardowej.
Załóżmy, że masz standardowy typ biblioteki:
struct example { void do_stuff() const; };
dość proste. W niektórych wersjach standardowych dodawana jest nowa metoda lub przeciążenie lub obok czegokolwiek:
struct example { void do_stuff() const; void method(); // a new method };
może to po cichu zmienić zachowanie istniejących programów C ++.
Dzieje się tak, ponieważ obecnie ograniczone możliwości odbicia w C ++ są wystarczające, aby wykryć, czy taka metoda istnieje, i uruchomić na jej podstawie inny kod.
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 {};
jest to tylko stosunkowo prosty sposób na wykrycie nowego
method
, istnieje wiele sposobów.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>{} ); }
To samo może się zdarzyć, gdy usuniesz metody z klas.
Chociaż ten przykład bezpośrednio wykrywa istnienie metody, tego rodzaju rzeczy, które mają miejsce pośrednio, mogą być mniej wymyślone. Jako konkretny przykład możesz mieć aparat serializacji, który decyduje, czy coś może być serializowane jako kontener na podstawie tego, czy jest iterowalne, czy też ma dane wskazujące na surowe bajty i element członkowski rozmiaru, z jednym preferowanym względem inny.
Standard idzie i dodaje
.data()
metodę do kontenera i nagle zmienia się typ ścieżki, której używa do serializacji.Wszystko, co standard C ++ może zrobić, jeśli nie chce się zawiesić, to sprawić, że kod, który cicho się psuje, jest rzadki lub w jakiś sposób nierozsądny.
źródło
O rany ... Łącze cpplearner warunkiem jest przerażające .
Między innymi C ++ 20 nie zezwalał na deklarację struktur w stylu C dla struktur C ++.
typedef struct { void member_foo(); // Ill-formed since C++20 } m_struct;
Jeśli uczono cię pisania takich struktur (a ludzie, którzy uczą „C z klasami” uczą dokładnie tego), masz przerąbane .
źródło
typedef
swoje konstrukcje i na pewno nie zamierzam marnować na to kredy. Z całą pewnością jest to kwestia gustu i chociaż są bardzo wpływowi ludzie (Torvalds ...), którzy podzielają twój punkt widzenia, inni ludzie, tacy jak ja, zwrócą uwagę, że konwencja nazywania typów to wszystko, czego potrzeba. Zaśmiecanie kodustruct
słowami kluczowymi niewiele przyczynia się do zrozumienia, że duża litera (MyClass* object = myClass_create();
) nie przekaże. Szanuję to, jeśli chcesz mieć tostruct
w swoim kodzie. Ale ja nie chcę tego w moim.struct
tylko dla zwykłych starych typów danych iclass
wszystkiego, co ma funkcje składowe. Ale nie możesz używać tej konwencji w C, ponieważ nie maclass
w C.struct
jest w rzeczywistości POD. Sposób, w jaki piszę kod w C, dotyczy większości struktur tylko przez kod w jednym pliku i przez funkcje, które noszą nazwę swojej klasy. Zasadniczo jest to OOP bez cukru syntaktycznego. To pozwala mi faktycznie kontrolować, co zmienia się wewnątrz astruct
i które niezmienniki są gwarantowane między jego elementami. Więc mamstructs
tendencję do posiadania funkcji składowych, prywatnych implementacji, niezmienników i abstrakcji z ich członków danych. Nie brzmi jak POD, prawda?extern "C"
blokach, nie widzę żadnego problemu z tą zmianą. Nikt nie powinien definiować struktur w C ++. Nie jest to większa przeszkoda niż fakt, że C ++ ma inną semantykę niż Java. Kiedy uczysz się nowego języka programowania, być może będziesz musiał nauczyć się nowych nawyków.Oto przykład, który wyświetla 3 w C ++ 03, ale 0 w C ++ 11:
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'; }
Ta zmiana w zachowaniu została spowodowana specjalną obsługą
>>
. Przed C ++ 11>>
zawsze był prawym operatorem zmiany. W C ++ 11>>
może być również częścią deklaracji szablonu.źródło
>>
tego sposobu.Trigraphs spadły
Pliki źródłowe są kodowane w fizycznym zestawie znaków, który jest odwzorowywany w sposób zdefiniowany w implementacji na źródłowy zestaw znaków zdefiniowany w standardzie. Aby uwzględnić odwzorowania z niektórych fizycznych zestawów znaków, które natywnie nie miały wszystkich znaków interpunkcyjnych wymaganych w źródłowym zestawie znaków, język definiował trygrafy - sekwencje trzech typowych znaków, których można użyć zamiast mniej powszechnych znaków interpunkcyjnych. Do ich obsługi wymagany był preprocesor i kompilator.
W C ++ 17 trójgrafy zostały usunięte. Dlatego niektóre pliki źródłowe nie będą akceptowane przez nowsze kompilatory, chyba że zostaną najpierw przetłumaczone z fizycznego zestawu znaków na inny fizyczny zestaw znaków, który odwzorowuje jeden do jednego na źródłowy zestaw znaków. (W praktyce większość kompilatorów właśnie uczyniła interpretację trygrafów opcjonalną.) Nie jest to subtelna zmiana zachowania, ale przełomowa zmiana, która uniemożliwia kompilację wcześniej akceptowanych plików źródłowych bez zewnętrznego procesu tłumaczenia.
Więcej ograniczeń
char
Norma odnosi się również do zestawu znaków wykonania , który jest zdefiniowany w ramach implementacji, ale musi zawierać co najmniej cały zestaw znaków źródłowych oraz niewielką liczbę kodów sterujących.
Standard C ++ zdefiniowany
char
jako typ całkowity bez znaku, który może efektywnie reprezentować każdą wartość w zestawie znaków wykonania. Z przedstawicielem prawnika językowego możesz argumentować, że achar
musi mieć co najmniej 8 bitów.Jeśli Twoja implementacja używa wartości bez znaku dla
char
, to wiesz, że może ona wynosić od 0 do 255, a zatem jest odpowiednia do przechowywania każdej możliwej wartości bajtu.Ale jeśli Twoja implementacja używa podpisanej wartości, ma opcje.
Większość użyłaby dopełnienia do dwóch, dając
char
minimalny zakres od -128 do 127. To 256 unikalnych wartości.Ale inną opcją był znak + wielkość, gdzie jeden bit jest zarezerwowany, aby wskazać, czy liczba jest ujemna, a pozostałe siedem bitów wskazuje wielkość. Dałoby
char
to zakres od -127 do 127, czyli tylko 255 unikatowych wartości. (Ponieważ tracisz jedną użyteczną kombinację bitów, która reprezentuje -0.)Nie jestem pewien, czy komisja kiedykolwiek wyraźnie określiła to jako wadę, ale to dlatego, że nie można było polegać na standardzie, który gwarantuje, że podróż w obie strony z
unsigned char
dochar
iz powrotem zachowa pierwotną wartość. (W praktyce wszystkie implementacje działały, ponieważ wszystkie używały dopełnienia do dwóch dla podpisanych typów całkowitych).Dopiero niedawno (C ++ 17?) Poprawiono sformułowanie, aby zapewnić działanie w obie strony. Ta poprawka, wraz ze wszystkimi innymi wymaganiami dotyczącymi
char
, skutecznie upoważnia uzupełnienie do dwóch dla podpisanych,char
nie mówiąc tego wyraźnie (nawet jeśli standard nadal zezwala na reprezentacje znak + wielkość dla innych podpisanych typów całkowitych). Istnieje propozycja, aby wszystkie podpisane typy całkowite używały dopełnienia do dwóch, ale nie pamiętam, czy trafiło to do C ++ 20.Więc ten jest przeciwieństwem tego, czego szukasz, ponieważ daje wcześniej
nieprawidłowy,zbyt arogancki kod, naprawę wsteczną.źródło
Nie jestem pewien, czy uważasz to za przełomową zmianę poprawnego kodu, ale ...
Przed C ++ 11 kompilatory mogły, ale nie były wymagane, do usuwania kopii w pewnych okolicznościach, nawet jeśli konstruktor kopiujący ma obserwowalne efekty uboczne. Teraz mamy zagwarantowaną eliminację kopii. Zachowanie zasadniczo zmieniło się od zdefiniowanego w implementacji do wymaganego.
Oznacza to, że efekty uboczne konstruktora kopiującego mogły wystąpić w starszych wersjach, ale nigdy nie wystąpią w nowszych. Można argumentować, że poprawny kod nie powinien opierać się na wynikach zdefiniowanych w ramach implementacji, ale nie sądzę, aby to było to samo, co stwierdzenie, że taki kod jest nieprawidłowy.
źródło
Zachowanie podczas odczytywania (numerycznych) danych ze strumienia i niepowodzenia odczytu zostało zmienione od czasu C ++ 11.
Na przykład odczyt liczby całkowitej ze strumienia, gdy nie zawiera on liczby całkowitej:
#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 }
Ponieważ c ++ 11 ustawi odczytaną liczbę całkowitą na 0, gdy się nie powiedzie; w c ++ <11 liczba całkowita nie została zmieniona. To powiedziawszy, gcc, nawet gdy wymusza powrót standardu do c ++ 98 (z -std = c ++ 98) zawsze pokazuje nowe zachowanie co najmniej od wersji 4.4.7.
(Imho stare zachowanie było właściwie lepsze: po co zmieniać wartość na 0, która sama w sobie jest ważna, skoro nic nie można odczytać?)
Źródła: patrz https://en.cppreference.com/w/cpp/locale/num_get/get
źródło