Czy kiedykolwiek nastąpiły ciche zmiany zachowania w C ++ w nowych wersjach standardowych?


104

(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.

Komentarze nie służą do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
Samuel Liew

3
Nie rozumiem, dlaczego to pytanie jest zamknięte. „ Czy kiedykolwiek nastąpiły ciche zmiany zachowania w C ++ z nowymi standardowymi wersjami? ” Wydaje się być doskonale skoncentrowany, a treść pytania nie wydaje się od tego odbiegać.
Ted Lyngmo,

Moim zdaniem największą cichą przełomową zmianą jest przedefiniowanie auto. Przed C ++ 11 auto x = ...;zadeklarowano plik int. Następnie deklaruje, co ...jest.
Raymond Chen

@RaymondChen: Ta zmiana jest cicha tylko wtedy, gdy niejawnie definiujesz wartości int, ale jawnie autopodajesz 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 ...
einpoklum

To prawda, dlatego go wybrali. Ale to była ogromna zmiana w semantyce.
Raymond Chen

Odpowiedzi:


113

Zwracany typ string::datazmian z const char*na char*w C ++ 17. To z pewnością może mieć znaczenie

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());
}

Trochę wymyślony, ale ten legalny program zmieniłby swoje wyjście, przechodząc z C ++ 14 do C ++ 17.


7
Och, nawet nie zdawałem sobie sprawy, że to std::stringzmiany 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.
einpoklum

9
Czy to wymyślone, czy nie, to całkiem dobrze pokazuje zmianę w dobrze sformułowanym kodzie.
David C. Rankin

Nawiasem mówiąc, zmiana ta jest oparta na zabawnych, ale uzasadnionych przypadkach użycia, kiedy zmieniasz zawartość std :: string in situ, być może za pomocą starszych funkcji działających na char *. Teraz jest to całkowicie uzasadnione: podobnie jak w przypadku wektora, istnieje gwarancja, że ​​istnieje podstawowa, ciągła tablica, którą można manipulować (zawsze można było to przez zwrócone odwołania; teraz jest to bardziej naturalne i wyraźne). Możliwe przypadki użycia to edytowalne zestawy danych o stałej długości (np. Pewnego rodzaju wiadomości), które, jeśli są oparte na std :: container, zachowują usługi STL, takie jak zarządzanie czasem życia, możliwość kopiowania itp.
Peter - Przywróć Monikę

81

Odpowiedź na to pytanie pokazuje, jak zainicjowanie wektora przy użyciu pojedynczej size_typewartoś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 Somethingi 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 Somethingdomyś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 Somethingz nich, a v == 0następnie skopiuje dziesięć kolejnych. Na końcu wektor zawiera dziesięć obiektów o vwartoś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 vwartościach od 0 do 9 włącznie.


@einpoklum Dodałem jednak wymyślny przykład. :)
cdhowie

3
Nie sądzę, żeby to było wymyślone. Różni konstruktorzy często zachowują się inaczej, np. Przy alokacji pamięci. Właśnie zastąpiłeś jeden efekt uboczny innym (I / O).
einpoklum

17
@cdhowie W ogóle nie wymyślono. Niedawno pracowałem nad klasą UUID. Konstruktor domyślny wygenerował losowy identyfikator UUID. Nie miałem pojęcia o takiej możliwości, po prostu założyłem zachowanie C ++ 11.
john

5
Jednym z powszechnie używanych przykładów klas, w których ma to znaczenie, jest OpenCV cv::mat. Konstruktor domyślny przydziela nową pamięć, podczas gdy konstruktor kopiujący tworzy nowy widok do istniejącej pamięci.
jpa

Nie nazwałbym tego wymyślonym przykładem, jasno pokazuje różnicę w zachowaniu.
David Waterworth

51

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

7
@einpoklum Cóż, mówi się, że co najmniej tuzin z nich „zmienia znaczenie” istniejącego kodu lub powoduje, że „działają inaczej”.
cpplearner

4
Jak podsumowałbyś uzasadnienie tej konkretnej zmiany?
Nayuki

4
@Nayuki jestem pewien, że użycie tej boolwersji 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, że u8dawniej dawały literały, const char*a teraz dają const char8_t*.
leftaround około

25

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.


3
Powinienem był wykluczyć SFINAE, ponieważ nie do końca to miałem na myśli ... ale tak, to prawda, więc +1.
einpoklum

„coś takiego dzieje się pośrednio” skutkowało głosowaniem za, a nie przeciw, ponieważ jest to prawdziwa pułapka.
Ian Ringrose

1
To naprawdę dobry przykład. Mimo że OP miał go wykluczyć, jest to prawdopodobnie jedna z najbardziej prawdopodobnych rzeczy, które powodują ciche zmiany zachowania w istniejącym kodzie. +1
cdhowie

1
@TedLyngmo Jeśli nie możesz naprawić wykrywacza, zmień wykrytą rzecz. Strzelanie wyborowe w Teksasie!
Yakk - Adam Nevraumont

15

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 .



19
@ Peter-ReinstateMonica Cóż, zawsze mam typedefswoje 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 kodu structsłowami kluczowymi niewiele przyczynia się do zrozumienia, że ​​duża litera ( MyClass* object = myClass_create();) nie przekaże. Szanuję to, jeśli chcesz mieć to structw swoim kodzie. Ale ja nie chcę tego w moim.
cmaster

5
To powiedziawszy, podczas programowania C ++ rzeczywiście dobrą konwencją jest używanie structtylko dla zwykłych starych typów danych i classwszystkiego, co ma funkcje składowe. Ale nie możesz używać tej konwencji w C, ponieważ nie ma classw C.
cmaster - przywróć monikę

1
@ Peter-ReinstateMonica Tak, cóż, nie możesz dołączyć metody syntaktycznie w C, ale to nie znaczy, że C structjest 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 a structi które niezmienniki są gwarantowane między jego elementami. Więc mam structstendencję do posiadania funkcji składowych, prywatnych implementacji, niezmienników i abstrakcji z ich członków danych. Nie brzmi jak POD, prawda?
cmaster

6
O ile nie są one zabronione w 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.
Cody Gray

15

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.


Cóż, z technicznego punktu widzenia to prawda, ale ten kod był „nieformalnie niejednoznaczny” na początku z powodu zastosowania >>tego sposobu.
einpoklum

11

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 charjako 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 a charmusi 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 charminimalny 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 charto 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 chardo chariz 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, charnie 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ą.


Część trygrafów nie jest odpowiedzią na to pytanie - to nie jest cicha zmiana. I IIANM, druga część to zmiana zachowania zdefiniowanego w implementacji na ściśle nakazane, o co też nie pytałem.
einpoklum

10

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.


1
Myślałem, że to „wymaganie” zostało dodane w C ++ 17, a nie w C ++ 11? (Zobacz tymczasową materializację .)
cdhowie

@cdhowie: Myślę, że masz rację. Nie miałem pod ręką standardów, kiedy to pisałem i prawdopodobnie zbytnio ufałem niektórym wynikom wyszukiwania.
Adrian McCarthy

Zmiana w zachowaniu zdefiniowanym przez implementację nie liczy się jako odpowiedź na to pytanie.
einpoklum

7

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


Ale nie wspomniano o żadnej zmianie dotyczącej returnType. Tylko 2 przeciążenia wiadomości są dostępne od czasu C ++ 11
kompilacja powiodła się

Czy to zdefiniowane zachowanie zarówno w C ++ 98, jak iw C ++ 11? A może zachowanie zostało zdefiniowane?
einpoklum

Kiedy cppreference.com ma rację: "jeśli wystąpi błąd, v pozostaje niezmienione. (Do C ++ 11)" Zatem zachowanie zostało zdefiniowane przed C ++ 11 i zmienione.
DanRechtsaf,

W moim rozumieniu zachowanie ss> a zostało rzeczywiście zdefiniowane, ale w bardzo częstym przypadku, gdy odczytujesz niezainicjowaną zmienną, zachowanie c ++ 11 będzie używać niezainicjowanej zmiennej, która jest niezdefiniowanym zachowaniem. W ten sposób domyślna konstrukcja na wypadek awarii chroni przed bardzo powszechnym niezdefiniowanym zachowaniem.
Rasmus Damgaard Nielsen
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.