Apakah pernah ada perubahan perilaku diam di C ++ dengan versi standar baru?

104

(Saya mencari satu atau dua contoh untuk membuktikan maksudnya, bukan daftar.)

Pernahkah terjadi perubahan dalam standar C ++ (misalnya dari 98 menjadi 11, 11 menjadi 14 dll.) Mengubah perilaku kode pengguna yang ada, terbentuk dengan baik, dan berperilaku terdefinisi - secara diam-diam? yaitu tanpa peringatan atau kesalahan saat mengkompilasi dengan versi standar yang lebih baru?

Catatan:

  • Saya bertanya tentang perilaku yang diamanatkan standar, bukan tentang pilihan penulis pelaksana / penyusun.
  • Semakin sedikit kode yang dibuat-buat, semakin baik (sebagai jawaban atas pertanyaan ini).
  • Saya tidak bermaksud kode dengan deteksi versi seperti #if __cplusplus >= 201103L.
  • Jawaban yang melibatkan model memori baik-baik saja.
einpoklum
sumber
Komentar tidak untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Samuel Liew
3
Saya tidak mengerti mengapa pertanyaan ini ditutup. “ Pernahkah ada perubahan perilaku diam di C ++ dengan versi standar yang baru? ” Tampaknya terfokus dengan sempurna dan inti pertanyaan tampaknya tidak menyimpang dari itu.
Ted Lyngmo
Dalam pikiran saya, perubahan terbesar yang diam-diam adalah pendefinisian ulang auto. Sebelum C ++ 11, auto x = ...;deklarasikan int. Setelah itu, ia menyatakan apapun ...itu.
Raymond Chen
@RaymondChen: Perubahan ini hanya diam jika Anda secara implisit mendefinisikan int, tetapi secara eksplisit mengatakan autovariabel were -type. Saya pikir Anda mungkin dapat menghitung di satu sisi jumlah orang di dunia yang akan menulis kode semacam itu, kecuali untuk kontes kode C yang dikaburkan ...
einpoklum
Benar, itulah mengapa mereka memilihnya. Tapi itu adalah perubahan besar dalam semantik.
Raymond Chen

Jawaban:

113

Jenis kembalian string::databerubah dari const char*menjadi char*dalam C ++ 17. Hal itu tentu bisa membuat perbedaan

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

Sedikit dibuat-buat tetapi program legal ini akan mengubah keluarannya dari C ++ 14 menjadi C ++ 17.

john
sumber
7
Oh, saya bahkan tidak menyadari ada std::stringperubahan untuk C ++ 17. Jika ada, saya akan mengira perubahan C ++ 11 mungkin telah menyebabkan perubahan perilaku diam. +1.
einpoklum
9
Dibuat atau tidak, ini menunjukkan perubahan pada kode yang dibentuk dengan cukup baik.
David C. Rankin
Selain itu, perubahan tersebut didasarkan pada kasus penggunaan yang lucu namun sah saat Anda mengubah konten std :: string secara in situ, mungkin melalui fungsi lama yang beroperasi pada char *. Itu benar-benar sah sekarang: seperti halnya vektor, ada jaminan bahwa ada larik yang mendasari dan berdekatan yang dapat Anda manipulasi (Anda selalu bisa melalui referensi yang dikembalikan; sekarang dibuat lebih alami dan eksplisit). Kasus penggunaan yang mungkin dapat diedit, kumpulan data dengan panjang tetap (mis. Pesan dari beberapa jenis) yang, jika didasarkan pada std :: container, mempertahankan layanan STL seperti manajemen waktu hidup, copyability dll.
Peter - Reinstate Monica
81

Jawaban atas pertanyaan ini menunjukkan bagaimana menginisialisasi vektor menggunakan size_typenilai tunggal dapat menghasilkan perilaku yang berbeda antara C ++ 03 dan C ++ 11.

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

C ++ 03 default-membangun objek sementara dari tipe elemen Somethingdan menyalin-konstruksi setiap elemen dalam vektor dari sementara itu.

C ++ 11 default-membangun setiap elemen dalam vektor.

Dalam banyak kasus (kebanyakan?), Ini menghasilkan keadaan akhir yang setara, tetapi tidak ada alasan mereka harus melakukannya. Itu tergantung pada implementasi Somethingkonstruktor default / copy.

Lihat contoh yang dibuat-buat ini :

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 akan secara default membangun satu Somethingdengan v == 0kemudian menyalin-membangun sepuluh lagi dari yang satu itu. Pada akhirnya, vektor berisi sepuluh objek yang vnilainya 1 sampai 10, inklusif.

C ++ 11 akan membangun default setiap elemen. Tidak ada salinan yang dibuat. Pada akhirnya, vektor berisi sepuluh objek yang vnilainya 0 sampai 9, inklusif.

cdhowie.dll
sumber
@einpoklum saya menambahkan contoh yang dibuat-buat. :)
cdhowie
3
Saya tidak berpikir itu dibuat-buat. Konstruktor yang berbeda sering bertindak berbeda dengan hal-hal seperti, katakanlah, alokasi memori. Anda baru saja mengganti satu efek samping dengan yang lain (I / O).
einpoklum
17
@cdhowie Tidak dibuat-buat sama sekali. Saya baru-baru ini mengerjakan kelas UUID. Konstruktor default menghasilkan UUID acak. Saya tidak tahu tentang kemungkinan ini, saya hanya mengasumsikan perilaku C ++ 11.
john
5
Salah satu contoh kelas dunia nyata yang banyak digunakan di mana hal ini penting adalah OpenCV cv::mat. Konstruktor default mengalokasikan memori baru, sedangkan konstruktor salinan membuat tampilan baru ke memori yang ada.
jpa
Saya tidak akan menyebutnya sebagai contoh yang dibuat-buat, ini dengan jelas menunjukkan perbedaan perilaku.
David Waterworth
51

Standar memiliki daftar perubahan yang melanggar dalam Lampiran C [diff] . Banyak dari perubahan ini dapat menyebabkan perubahan perilaku diam.

Sebuah contoh:

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
sumber
7
@einpoklum Yah, setidaknya selusin dari mereka dikatakan "mengubah arti" dari kode yang ada atau membuatnya "mengeksekusi secara berbeda".
cpplearner
4
Bagaimana Anda akan meringkas alasan untuk perubahan khusus ini?
Nayuki
4
@Nayuki cukup yakin bahwa menggunakan boolversi tersebut bukanlah perubahan yang dimaksudkan, hanya efek samping dari aturan konversi lainnya. Maksud sebenarnya adalah untuk menghentikan beberapa kebingungan antara pengkodean karakter, perubahan sebenarnya adalah u8literal dulu memberi const char*tetapi sekarang memberi const char8_t*.
kiri sekitar sekitar
25

Setiap kali mereka menambahkan metode baru (dan sering kali berfungsi) ke pustaka standar, hal ini terjadi.

Misalkan Anda memiliki tipe pustaka standar:

struct example {
  void do_stuff() const;
};

cukup mudah. Dalam beberapa revisi standar, metode baru atau kelebihan beban atau apa pun ditambahkan:

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

ini diam-diam dapat mengubah perilaku program C ++ yang ada.

Ini karena kemampuan refleksi C ++ yang saat ini terbatas cukup untuk mendeteksi jika ada metode seperti itu, dan menjalankan kode yang berbeda berdasarkan padanya.

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 {};

ini hanyalah cara yang relatif sederhana untuk mendeteksi yang baru method, ada banyak cara.

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

Hal yang sama bisa terjadi saat Anda menghapus metode dari kelas.

Meskipun contoh ini secara langsung mendeteksi keberadaan sebuah metode, hal semacam ini yang terjadi secara tidak langsung bisa jadi kurang dibuat-buat. Sebagai contoh konkret, Anda mungkin memiliki mesin serialisasi yang memutuskan apakah sesuatu dapat diserialkan sebagai wadah berdasarkan apakah iterable, atau jika memiliki data yang menunjuk-ke-mentah-byte dan ukuran anggota, dengan satu lebih disukai daripada yang lain.

Standar pergi dan menambahkan .data()metode ke wadah, dan tiba-tiba perubahan jenis jalur yang digunakan untuk serialisasi.

Semua standar C ++ dapat melakukannya, jika tidak ingin dibekukan, adalah membuat jenis kode yang diam-diam rusak menjadi langka atau entah bagaimana tidak masuk akal.

Yakk - Adam Nevraumont
sumber
3
Saya seharusnya telah memenuhi syarat pertanyaan untuk mengecualikan SFINAE karena ini bukan yang saya maksud ... tapi ya, itu benar, jadi +1.
einpoklum
"Hal semacam ini terjadi secara tidak langsung" menghasilkan suara positif daripada suara negatif karena ini adalah jebakan nyata.
Ian Ringrose
1
Ini adalah contoh yang sangat bagus. Meskipun OP bermaksud untuk mengecualikannya, ini mungkin salah satu hal yang paling mungkin menyebabkan perubahan perilaku diam pada kode yang ada. +1
cdhowie
1
@TedLyngmo Jika Anda tidak dapat memperbaiki detektor, ubah hal yang terdeteksi. Tembak jitu Texas!
Yakk - Adam Nevraumont
15

Oh boy ... Link cpplearner disediakan adalah menakutkan .

Antara lain, C ++ 20 melarang deklarasi struct C-style dari C ++ struct.

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

Jika Anda diajari menulis struktur seperti itu (dan orang-orang yang mengajar "C dengan kelas" mengajarkan persis seperti itu) Anda kacau .

Tidak ada sama sekali
sumber
20
Siapa pun yang mengajar itu harus menulis 100 kali di papan tulis "Saya tidak akan mengetikkan struktur". Anda bahkan tidak harus melakukannya di C, imho. Bagaimanapun, perubahan itu tidak diam: Dalam standar baru, "Kode C ++ 2017 yang valid (menggunakan typedef pada struct anonim, non-C) mungkin tidak berbentuk" dan "tidak berbentuk - program memiliki kesalahan sintaks atau kesalahan semantik yang dapat didiagnosis . Kompiler C ++ yang sesuai diperlukan untuk mengeluarkan diagnostik " .
Peter - Kembalikan Monica
19
@ Peter-ReinstateMonica Yah, saya selalu typedefstruct saya, dan saya pasti tidak akan menyia-nyiakan kapur saya untuk itu. Ini pasti masalah selera, dan meskipun ada orang yang sangat berpengaruh (Torvalds ...) yang memiliki pandangan yang sama dengan Anda, orang lain seperti saya akan menunjukkan, bahwa hanya diperlukan konvensi penamaan untuk jenis. Mengacaukan kode dengan structkata kunci menambah sedikit pemahaman bahwa huruf kapital ( MyClass* object = myClass_create();) tidak akan menyampaikan. Saya menghormati jika Anda menginginkan structkode Anda. Tapi aku tidak ingin itu ada di dalam diriku.
cmaster
5
Meskipun demikian, saat memprogram C ++, itu memang konvensi yang baik untuk digunakan structhanya untuk tipe data biasa-lama, dan classapa pun yang memiliki fungsi anggota. Tetapi Anda tidak dapat menggunakan konvensi itu di C karena tidak ada classdi C.
cmaster - kembalikan monica
1
@ Peter-ReinstateMonica Ya, Anda tidak bisa melampirkan metode sintaksis di C, tapi itu tidak berarti C structsebenarnya POD. Cara saya menulis kode C, kebanyakan struktur hanya tersentuh oleh kode dalam satu file dan oleh fungsi yang membawa nama kelasnya. Ini pada dasarnya OOP tanpa gula sintaksis. Ini memungkinkan saya untuk benar-benar mengontrol perubahan apa di dalam a struct, dan invarian mana yang dijamin di antara anggotanya. Jadi, saya structscenderung memiliki fungsi anggota, implementasi pribadi, invarian, dan abstrak dari anggota data mereka. Tidak terdengar seperti POD, bukan?
cmaster
6
Selama tidak dilarang di extern "C"blok, saya tidak melihat ada masalah dengan perubahan ini. Tidak ada yang harus mengetikkan struct dalam C ++. Ini bukan rintangan yang lebih besar daripada kenyataan bahwa C ++ memiliki semantik yang berbeda dari Java. Saat Anda mempelajari bahasa pemrograman baru, Anda mungkin perlu mempelajari beberapa kebiasaan baru.
Cody Gray
15

Berikut adalah contoh yang mencetak 3 di C ++ 03 tetapi 0 di 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'; }

Perubahan perilaku ini disebabkan oleh penanganan khusus untuk >>. Sebelum C ++ 11, >>selalu menjadi operator shift kanan. Dengan C ++ 11, >>dapat menjadi bagian dari deklarasi template juga.

Waxrat
sumber
Yah, secara teknis ini benar, tetapi kode ini "secara informal ambigu" untuk memulai karena penggunaan >>cara itu.
einpoklum
11

Trigraf jatuh

File sumber dikodekan dalam himpunan karakter fisik yang dipetakan dalam cara implementasi yang ditentukan ke himpunan karakter sumber , yang ditentukan dalam standar. Untuk mengakomodasi pemetaan dari beberapa kumpulan karakter fisik yang tidak memiliki semua tanda baca yang diperlukan oleh kumpulan karakter sumber, bahasa menentukan trigraf — urutan tiga karakter umum yang dapat digunakan sebagai pengganti karakter tanda baca yang kurang umum. Preprocessor dan compiler diperlukan untuk menangani ini.

Di C ++ 17, trigraf telah dihapus. Jadi beberapa file sumber tidak akan diterima oleh kompiler yang lebih baru kecuali mereka diterjemahkan pertama kali dari kumpulan karakter fisik ke beberapa kumpulan karakter fisik lainnya yang memetakan satu-ke-satu ke kumpulan karakter sumber. (Dalam praktiknya, sebagian besar kompiler hanya membuat interpretasi trigraf opsional.) Ini bukan perubahan perilaku halus, tetapi perubahan yang merusak mencegah file sumber yang sebelumnya dapat diterima untuk dikompilasi tanpa proses terjemahan eksternal.

Lebih banyak kendala char

Standar juga mengacu pada himpunan karakter eksekusi , yang didefinisikan implementasi, tetapi harus berisi setidaknya seluruh himpunan karakter sumber ditambah sejumlah kecil kode kontrol.

Standar C ++ didefinisikan charsebagai tipe integral yang mungkin tidak bertanda tangan yang dapat secara efisien mewakili setiap nilai dalam set karakter eksekusi. Dengan representasi dari seorang pengacara bahasa, Anda dapat berargumen bahwa a charharus minimal 8 bit.

Jika implementasi Anda menggunakan nilai unsigned untuk char, maka Anda tahu itu dapat berkisar dari 0 hingga 255, dan karenanya cocok untuk menyimpan setiap nilai byte yang mungkin.

Tetapi jika implementasi Anda menggunakan nilai yang ditandatangani, itu memiliki opsi.

Sebagian besar akan menggunakan komplemen dua, memberikan charkisaran minimum -128 hingga 127. Itu berarti 256 nilai unik.

Tetapi opsi lain adalah tanda + magnitudo, di mana satu bit dicadangkan untuk menunjukkan apakah bilangan tersebut negatif dan tujuh bit lainnya menunjukkan besarnya. Itu akan memberikan charkisaran -127 hingga 127, yang hanya 255 nilai unik. (Karena Anda kehilangan satu kombinasi bit yang berguna untuk mewakili -0.)

Saya tidak yakin panitia pernah secara eksplisit menetapkan ini sebagai cacat, tetapi itu karena Anda tidak dapat mengandalkan standar untuk menjamin perjalanan pulang pergi dari dan unsigned charke charbelakang akan mempertahankan nilai aslinya. (Dalam praktiknya, semua implementasi melakukannya karena mereka semua menggunakan dua komplemen untuk tipe integral bertanda tangan.)

Baru-baru ini (C ++ 17?) Kata-katanya diperbaiki untuk memastikan perjalanan bolak-balik. Perbaikan itu, bersama dengan semua persyaratan lainnya char, secara efektif mengamanatkan pelengkap dua untuk ditandatangani chartanpa mengatakan secara eksplisit (bahkan ketika standar terus memungkinkan representasi tanda + besaran untuk jenis integral bertanda tangan lainnya). Ada proposal yang meminta semua tipe integral bertanda tangan menggunakan komplemen dua, tapi saya tidak ingat apakah itu berhasil menjadi C ++ 20.

Jadi satu ini adalah semacam kebalikan dari apa yang Anda cari karena memberikan sebelumnya yang salah terlalu lancang kode memperbaiki berlaku surut.

Adrian McCarthy
sumber
Bagian trigraf bukanlah jawaban untuk pertanyaan ini - itu bukan perubahan diam. Dan, IIANM, bagian kedua adalah perubahan implementasi yang didefinisikan menjadi perilaku yang diamanatkan secara ketat, yang juga bukan itu yang saya tanyakan.
einpoklum
10

Saya tidak yakin apakah Anda akan menganggap ini sebagai perubahan yang melanggar ke kode yang benar, tapi ...

Sebelum C ++ 11, penyusun diizinkan, tetapi tidak diharuskan, untuk menghilangkan salinan dalam keadaan tertentu, bahkan ketika pembuat salinan memiliki efek samping yang dapat diamati. Sekarang kami telah menjamin penghapusan salinan. Perilaku pada dasarnya berubah dari yang ditentukan oleh implementasi menjadi diperlukan.

Ini berarti bahwa efek samping konstruktor salinan Anda mungkin terjadi dengan versi yang lebih lama, tetapi tidak akan pernah terjadi dengan yang lebih baru. Anda dapat berargumen bahwa kode yang benar seharusnya tidak bergantung pada hasil yang ditentukan implementasi, tetapi menurut saya itu tidak sama dengan mengatakan kode seperti itu tidak benar.

Adrian McCarthy
sumber
1
Saya pikir "persyaratan" ini ditambahkan di C ++ 17, bukan C ++ 11? (Lihat perwujudan sementara .)
cdhowie
@cdhowie: Saya pikir Anda benar. Saya tidak memiliki standar saat menulis ini dan saya mungkin terlalu memercayai beberapa hasil pencarian saya.
Adrian McCarthy
Perubahan pada perilaku yang ditentukan implementasi tidak dihitung sebagai jawaban untuk pertanyaan ini.
einpoklum
7

Perilaku saat membaca (numerik) data dari aliran, dan gagal membaca, diubah sejak c ++ 11.

Misalnya, membaca integer dari aliran, sementara itu tidak mengandung integer:

#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 
}

Karena c ++ 11 akan menyetel integer baca ke 0 jika gagal; di c ++ <11 integer tidak berubah. Meskipun demikian, gcc, meskipun memaksa standar kembali ke c ++ 98 (dengan -std = c ++ 98) selalu menampilkan perilaku baru setidaknya sejak versi 4.4.7.

(Imho perilaku lama sebenarnya lebih baik: mengapa mengubah nilai menjadi 0, yang dengan sendirinya valid, ketika tidak ada yang bisa dibaca?)

Referensi: lihat https://en.cppreference.com/w/cpp/locale/num_get/get

DanRechtsaf
sumber
Tetapi tidak ada perubahan yang disebutkan tentang returnType. Hanya 2 kelebihan berita yang tersedia sejak C ++ 11
Build Berhasil
Apakah ini perilaku yang didefinisikan di C ++ 98 dan di C ++ 11? Atau apakah perilaku tersebut telah ditentukan?
einpoklum
Ketika cppreference.com benar: "jika terjadi kesalahan, v dibiarkan tidak berubah. (Hingga C ++ 11)" Jadi perilaku didefinisikan sebelum C ++ 11, dan diubah.
DanRechtsaf
Menurut pemahaman saya, perilaku untuk ss> a memang ditentukan, tetapi untuk kasus yang sangat umum di mana Anda membaca variabel yang tidak diinisialisasi, perilaku c ++ 11 akan menggunakan variabel yang tidak diinisialisasi, yang merupakan perilaku tidak terdefinisi. Jadi konstruksi default pada kegagalan melindungi dari perilaku tidak terdefinisi yang sangat umum.
Rasmus Damgaard Nielsen