Apakah kode dari "The C ++ Programming Language" edisi ke-4 bagian 36.3.6 ini memiliki perilaku yang terdefinisi dengan baik?

94

Dalam Bjarne Stroustrup C ++ Bahasa Pemrograman bagian edisi 4 36.3.6 STL-seperti Operasi kode berikut digunakan sebagai contoh chaining :

void f2()
{
    std::string s = "but I have heard it works even if you don't believe in it" ;
    s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
        .replace( s.find( " don't" ), 6, "" );

    assert( s == "I have heard it works only if you believe in it" ) ;
}

Penegasan gagal dalam gcc( lihat langsung ) dan Visual Studio( lihat langsung ), tetapi tidak gagal saat menggunakan Clang ( lihat langsung ).

Mengapa saya mendapatkan hasil yang berbeda? Apakah salah satu dari kompiler ini salah mengevaluasi ekspresi rantai atau apakah kode ini menunjukkan beberapa bentuk perilaku yang tidak ditentukan atau tidak ditentukan ?

Shafik Yaghmour
sumber
Lebih baik:s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
Ben Voigt
20
Terlepas dari bug, apakah saya satu-satunya yang berpikir kode jelek seperti itu seharusnya tidak ada dalam buku?
Karoly Horvath
5
@Karolyorv Perhatikan bahwa cout << a << b << coperator<<(operator<<(operator<<(cout, a), b), c)hanya sedikit kurang jelek.
Oktalis
1
@Oktalist: :) setidaknya saya mendapatkan niat disana. itu mengajarkan pencarian nama yang bergantung pada argumen dan sintaks operator pada saat yang sama dalam format singkat ... dan tidak memberi kesan bahwa Anda seharusnya benar-benar menulis kode seperti itu.
Karoly Horvath

Jawaban:

104

Kode menunjukkan perilaku yang tidak ditentukan karena urutan evaluasi sub-ekspresi yang tidak ditentukan meskipun tidak memanggil perilaku yang tidak ditentukan karena semua efek samping dilakukan dalam fungsi yang memperkenalkan hubungan urutan antara efek samping dalam kasus ini.

Contoh ini disebutkan dalam proposal N4228: Refining Expression Evaluation Order for Idiomatic C ++ yang mengatakan hal berikut tentang kode dalam pertanyaan:

[...] Kode ini telah ditinjau oleh para ahli C ++ di seluruh dunia, dan diterbitkan (The C ++ Programming Language, edisi ke- 4 .) Namun, kerentanannya terhadap urutan evaluasi yang tidak ditentukan baru ditemukan baru-baru ini oleh sebuah alat [.. .]

Detail

Mungkin jelas bagi banyak orang bahwa argumen ke fungsi memiliki urutan evaluasi yang tidak ditentukan tetapi mungkin tidak begitu jelas bagaimana perilaku ini berinteraksi dengan panggilan fungsi yang dirantai. Tidak jelas bagi saya ketika saya pertama kali menganalisis kasus ini dan tampaknya juga tidak bagi semua pengulas ahli .

Sekilas mungkin tampak bahwa karena masing-masing replaceharus dievaluasi dari kiri ke kanan bahwa grup argumen fungsi yang sesuai harus dievaluasi sebagai grup dari kiri ke kanan juga.

Ini tidak benar, argumen fungsi memiliki urutan evaluasi yang tidak ditentukan, meskipun pemanggilan fungsi berantai memperkenalkan urutan evaluasi dari kiri ke kanan untuk setiap pemanggilan fungsi, argumen dari setiap pemanggilan fungsi hanya diurutkan sebelumnya sehubungan dengan pemanggilan fungsi anggota mereka menjadi bagiannya. dari. Secara khusus hal ini memengaruhi panggilan berikut:

s.find( "even" )

dan:

s.find( " don't" )

yang diurutkan secara tidak pasti sehubungan dengan:

s.replace(0, 4, "" )

dua findpanggilan dapat dievaluasi sebelum atau sesudah replace, yang penting karena memiliki efek samping sdengan cara yang akan mengubah hasil find, itu mengubah panjang s. Jadi bergantung pada kapan itu replacedievaluasi relatif terhadap dua findpanggilan, hasilnya akan berbeda.

Jika kita melihat ekspresi rangkaian dan memeriksa urutan evaluasi dari beberapa sub-ekspresi:

s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
^ ^       ^  ^  ^    ^        ^                 ^  ^
A B       |  |  |    C        |                 |  |
          1  2  3             4                 5  6

dan:

.replace( s.find( " don't" ), 6, "" );
 ^        ^                   ^  ^
 D        |                   |  |
          7                   8  9

Catatan, kami mengabaikan fakta itu 4dan 7selanjutnya dapat dipecah menjadi lebih banyak sub-ekspresi. Begitu:

  • Adiurutkan sebelum Bdiurutkan sebelum Cdiurutkan sebelumnyaD
  • 1to 9tidak pasti diurutkan sehubungan dengan sub-ekspresi lain dengan beberapa pengecualian yang tercantum di bawah ini
    • 1untuk 3diurutkan sebelumnyaB
    • 4untuk 6diurutkan sebelumnyaC
    • 7untuk 9diurutkan sebelumnyaD

Kunci dari masalah ini adalah:

  • 4ke 9tidak pasti diurutkan sehubungan denganB

Urutan potensial pilihan evaluasi untuk 4dan 7sehubungan dengan Bmenjelaskan perbedaan hasil antara clangdan gccsaat mengevaluasi f2(). Dalam tes saya clangmengevaluasi Bsebelum mengevaluasi 4dan 7saat gccmengevaluasinya setelah. Kami dapat menggunakan program pengujian berikut untuk mendemonstrasikan apa yang terjadi dalam setiap kasus:

#include <iostream>
#include <string>

std::string::size_type my_find( std::string s, const char *cs )
{
    std::string::size_type pos = s.find( cs ) ;
    std::cout << "position " << cs << " found in complete expression: "
        << pos << std::endl ;

    return pos ;
}

int main()
{
   std::string s = "but I have heard it works even if you don't believe in it" ;
   std::string copy_s = s ;

   std::cout << "position of even before s.replace(0, 4, \"\" ): " 
         << s.find( "even" ) << std::endl ;
   std::cout << "position of  don't before s.replace(0, 4, \"\" ): " 
         << s.find( " don't" ) << std::endl << std::endl;

   copy_s.replace(0, 4, "" ) ;

   std::cout << "position of even after s.replace(0, 4, \"\" ): " 
         << copy_s.find( "even" ) << std::endl ;
   std::cout << "position of  don't after s.replace(0, 4, \"\" ): "
         << copy_s.find( " don't" ) << std::endl << std::endl;

   s.replace(0, 4, "" ).replace( my_find( s, "even" ) , 4, "only" )
        .replace( my_find( s, " don't" ), 6, "" );

   std::cout << "Result: " << s << std::endl ;
}

Hasil untuk gcc( lihat langsung )

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position  don't found in complete expression: 37
position even found in complete expression: 26

Result: I have heard it works evenonlyyou donieve in it

Hasil untuk clang( lihat langsung ):

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position even found in complete expression: 22
position don't found in complete expression: 33

Result: I have heard it works only if you believe in it

Hasil untuk Visual Studio( lihat langsung ):

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position  don't found in complete expression: 37
position even found in complete expression: 26
Result: I have heard it works evenonlyyou donieve in it

Detail dari standar

Kita tahu bahwa kecuali ditentukan evaluasi sub-ekspresi tidak diurutkan, ini dari draft C ++ 11 bagian standar 1.9 Eksekusi program yang mengatakan:

Kecuali jika disebutkan, evaluasi operan operator individu dan subekspresi ekspresi individu tidak diurutkan. [...]

dan kita tahu bahwa panggilan fungsi memperkenalkan hubungan urutan sebelum fungsi memanggil ekspresi dan argumen postfix sehubungan dengan badan fungsi, dari bagian 1.9:

[...] Saat memanggil suatu fungsi (apakah fungsinya sebaris atau tidak), setiap perhitungan nilai dan efek samping yang terkait dengan ekspresi argumen apa pun, atau dengan ekspresi postfix yang menunjukkan fungsi yang dipanggil, diurutkan sebelum eksekusi setiap ekspresi atau pernyataan di tubuh fungsi yang disebut. [...]

Kita juga tahu bahwa akses anggota kelas dan oleh karena itu rangkaian akan dievaluasi dari kiri ke kanan, dari bagian 5.2.5 Akses anggota kelas yang mengatakan:

[...] Ekspresi postfix sebelum titik atau panah dievaluasi; 64 hasil evaluasi itu, bersama dengan ekspresi-id, menentukan hasil dari seluruh ekspresi postfix.

Catatan, dalam kasus di mana ekspresi-id akhirnya menjadi fungsi anggota non-statis, ini tidak menentukan urutan evaluasi daftar ekspresi dalam ()karena itu adalah sub-ekspresi terpisah. Tata bahasa yang relevan dari 5.2 ekspresi Postfix :

postfix-expression:
    postfix-expression ( expression-listopt)       // function call
    postfix-expression . templateopt id-expression // Class member access, ends
                                                   // up as a postfix-expression

C ++ 17 perubahan

Proposal p0145r3: Refining Expression Evaluation Order for Idiomatic C ++ membuat beberapa perubahan. Menyertakan perubahan yang memberikan kode perilaku yang ditentukan dengan baik dengan memperkuat urutan aturan evaluasi untuk ekspresi-postfix dan daftar ekspresinya .

[expr.call] p5 mengatakan:

Ekspresi-postfix diurutkan sebelum setiap ekspresi dalam daftar ekspresi dan argumen default apa pun . Inisialisasi parameter, termasuk setiap penghitungan nilai terkait dan efek samping, diurutkan secara tidak pasti sehubungan dengan parameter lainnya. [Catatan: Semua efek samping evaluasi argumen diurutkan sebelum fungsi dimasukkan (lihat 4.6). —End note] [Contoh:

void f() {
std::string s = "but I have heard it works even if you don’t believe in it";
s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don’t"), 6, "");
assert(s == "I have heard it works only if you believe in it"); // OK
}

—Dan contoh]

Shafik Yaghmour
sumber
7
Saya sedikit terkejut melihat bahwa "banyak ahli" mengabaikan masalah, sudah diketahui bahwa mengevaluasi ekspresi postfix dari panggilan fungsi tidak diurutkan-sebelum mengevaluasi argumen (di semua versi C dan C ++).
MM
@ShafikYaghmour Panggilan fungsi secara tidak pasti diurutkan sehubungan dengan satu sama lain dan yang lainnya, dengan pengecualian hubungan yang diurutkan-sebelum yang Anda catat. Namun, evaluasi 1, 2, 3, 5, 6, 8, 9 "even",, "don't"dan beberapa contoh stidak diurutkan relatif satu sama lain.
TC
4
@TC tidak bukan (begitulah "bug" ini muncul). Misalnya foo().func( bar() ), dapat menelepon foo()sebelum atau sesudah menelepon bar(). The postfix-ekspresi adalah foo().func. Argumen dan ekspresi postfix diurutkan sebelum isi func(), tetapi tidak diurutkan relatif satu sama lain.
MM
@MattMcNabb Ah, benar, saya salah membaca. Anda berbicara tentang ekspresi-postfix itu sendiri daripada panggilannya. Ya, itu benar, mereka tidak diurutkan (kecuali beberapa aturan lain berlaku, tentu saja).
TC
6
Ada juga faktor yang membuat orang cenderung menganggap kode yang muncul di buku B. Stroustrup benar, jika tidak, seseorang pasti sudah menyadarinya! (terkait; pengguna SO masih menemukan kesalahan baru dalam K&R)
MM
4

Hal ini dimaksudkan untuk menambah informasi tentang masalah yang berkaitan dengan C ++ 17. Proposal ( Refining Expression Evaluation Order for Idiomatic C ++ Revision 2 ) untuk C++17mengatasi masalah yang mengutip kode di atas adalah sebagai spesimen.

Seperti yang disarankan, saya menambahkan informasi yang relevan dari proposal dan kutipan (menyoroti milik saya):

Urutan evaluasi ekspresi, seperti yang saat ini ditentukan dalam standar, melemahkan saran, idiom pemrograman populer, atau keamanan relatif fasilitas perpustakaan standar. Jebakan tidak hanya untuk pemula atau programmer yang ceroboh. Mereka mempengaruhi kita semua tanpa pandang bulu, bahkan ketika kita mengetahui aturannya.

Pertimbangkan fragmen program berikut:

void f()
{
  std::string s = "but I have heard it works even if you don't believe in it"
  s.replace(0, 4, "").replace(s.find("even"), 4, "only")
      .replace(s.find(" don't"), 6, "");
  assert(s == "I have heard it works only if you believe in it");
}

Penegasan seharusnya memvalidasi hasil yang diinginkan pemrogram. Ini menggunakan "rangkaian" panggilan fungsi anggota, praktik standar yang umum. Kode ini telah ditinjau oleh para ahli C ++ di seluruh dunia, dan diterbitkan (The C ++ Programming Language, edisi ke-4.) Namun, kerentanannya terhadap urutan evaluasi yang tidak ditentukan baru ditemukan baru-baru ini oleh sebuah alat.

Makalah ini menyarankan untuk mengubah pra- C++17aturan pada urutan evaluasi ekspresi yang dipengaruhi oleh Cdan telah ada selama lebih dari tiga dekade. Ia mengusulkan bahwa bahasa harus menjamin idiom kontemporer atau berisiko "jebakan dan sumber yang tidak jelas, bug yang sulit ditemukan" seperti yang terjadi dengan contoh kode di atas.

Usulan untuk C++17adalah untuk mengharuskan setiap ekspresi memiliki urutan evaluasi didefinisikan dengan baik :

  • Ekspresi postfix dievaluasi dari kiri ke kanan. Ini termasuk panggilan fungsi dan ekspresi pemilihan anggota.
  • Ekspresi tugas dievaluasi dari kanan ke kiri. Ini termasuk tugas gabungan.
  • Operator untuk operator shift dievaluasi dari kiri ke kanan.
  • Urutan evaluasi ekspresi yang melibatkan operator kelebihan beban ditentukan oleh urutan yang terkait dengan operator bawaan terkait, bukan aturan untuk pemanggilan fungsi.

Kode di atas berhasil dikompilasi menggunakan GCC 7.1.1dan Clang 4.0.0.

ricky m
sumber