Apa alasan untuk tidak menggunakan C ++ 17's [[nodiscard]] hampir di mana-mana dalam kode baru?

70

C ++ 17 memperkenalkan [[nodiscard]]atribut, yang memungkinkan pemrogram menandai fungsi dengan cara yang dikompilasi oleh kompiler jika objek yang dikembalikan dibuang oleh penelepon; atribut yang sama dapat ditambahkan ke seluruh tipe kelas.

Saya telah membaca tentang motivasi untuk fitur ini dalam proposal asli , dan saya tahu bahwa C ++ 20 akan menambahkan atribut ke fungsi standar seperti std::vector::empty, yang namanya tidak menyampaikan makna yang jelas mengenai nilai pengembalian.

Ini fitur keren dan bermanfaat. Nyatanya, itu sepertinya terlalu berguna. Di mana-mana saya baca [[nodiscard]], orang-orang mendiskusikannya seolah-olah Anda hanya akan menambahkannya ke beberapa fungsi atau tipe tertentu dan melupakan sisanya. Tetapi mengapa nilai yang tidak bisa dibuang menjadi kasus khusus, terutama ketika menulis kode baru? Bukankah nilai pengembalian yang dibuang biasanya berupa bug atau setidaknya pemborosan sumber daya?

Dan bukankah salah satu prinsip desain C ++ itu sendiri bahwa kompiler harus menangkap kesalahan sebanyak mungkin?

Jika demikian, mengapa tidak menambahkan [[nodiscard]]kode non-legacy Anda sendiri ke hampir setiap non- voidfungsi dan hampir setiap jenis kelas tunggal?

Saya sudah mencoba melakukan itu dalam kode saya sendiri, dan itu berfungsi dengan baik, kecuali sangat verbose yang mulai terasa seperti Jawa. Tampaknya jauh lebih alami untuk membuat kompiler memperingatkan tentang nilai pengembalian yang dibuang secara default kecuali untuk beberapa kasus lain di mana Anda menandai niat Anda [*] .

Karena saya telah melihat nol diskusi tentang kemungkinan ini dalam proposal standar, entri blog, pertanyaan Stack Overflow atau di mana pun di internet, saya pasti kehilangan sesuatu.

Mengapa mekanisme seperti itu tidak masuk akal dalam kode C ++ baru? Apakah verbositas satu-satunya alasan untuk tidak menggunakan [[nodiscard]]hampir di semua tempat?


[*] Secara teori, Anda mungkin memiliki sesuatu seperti [[maydiscard]]atribut, yang juga dapat ditambahkan secara surut ke fungsi seperti printfdalam implementasi perpustakaan standar.

Christian Hackl
sumber
22
Yah ada banyak fungsi di mana nilai kembali dapat secara masuk akal dibuang. operator =sebagai contoh. Dan std::map::insert.
user253751
2
@immibis: Benar. Perpustakaan standar penuh dengan fungsi-fungsi tersebut. Tetapi dalam kode saya sendiri, mereka cenderung sangat jarang; Menurut Anda apakah ini masalah kode perpustakaan vs kode aplikasi?
Christian Hackl
9
"... sangat bertele-tele bahwa itu mulai terasa seperti Jawa ..." - membaca ini dalam sebuah pertanyaan tentang C ++ membuat saya sedikit bergerak: - /
Marco13
3
@ChristianHackl Komentarnya mungkin bukan tempat yang tepat untuk "bashing bahasa", dan tentu saja, Java juga verbose dibandingkan dengan bahasa lain. Tapi file header, operator penugasan, copy konstruktor dan penggunaan yang tepat dari constbisa mengasapi sebaliknya "sederhana" kelas (atau lebih tepatnya "biasa lama objek data") jauh di C ++.
Marco13
2
@ Marco13: Saya tidak bisa membahas banyak hal dalam satu komentar. Mari kita buat singkat: Java tidak memiliki file header, yang mengurangi jumlah file tetapi meningkatkan kopling; OTOH memaksa Anda untuk menempatkan setiap kelas tingkat atas dalam file sendiri dan setiap fungsi dalam kelas. Di C ++, Anda hampir tidak pernah mengimplementasikan operator penugasan dan menyalin konstruktor sendiri; fungsi-fungsi itu lebih relevan untuk kelas perpustakaan standar seperti std::vectoratau std::unique_ptr, di mana Anda hanya perlu anggota data dalam definisi kelas Anda. Saya telah bekerja dengan kedua bahasa; Java adalah bahasa yang oke, tetapi lebih verbose.
Christian Hackl

Jawaban:

73

Dalam kode baru yang tidak perlu kompatibel dengan standar yang lebih lama, gunakan atribut itu di tempat yang masuk akal. Tetapi untuk C ++, [[nodiscard]]membuat default yang buruk. Kamu menyarankan:

Tampaknya jauh lebih alami untuk membuat kompiler memperingatkan tentang nilai pengembalian yang dibuang secara default kecuali untuk beberapa kasus lain di mana Anda menandai niat Anda.

Itu akan tiba-tiba menyebabkan kode yang ada dan benar mengeluarkan banyak peringatan. Sementara perubahan semacam itu secara teknis dapat dianggap kompatibel-mundur karena kode yang ada masih berhasil dikompilasi, itu akan menjadi perubahan besar dalam praktik semantik.

Keputusan desain untuk bahasa dewasa yang sudah ada dengan basis kode yang sangat besar tentu berbeda dengan keputusan desain untuk bahasa yang sama sekali baru. Jika ini adalah bahasa baru, maka peringatan secara default akan masuk akal. Sebagai contoh, bahasa Nim membutuhkan nilai yang tidak dibutuhkan untuk dibuang secara eksplisit - ini akan sama dengan membungkus setiap pernyataan ekspresi dalam C ++ dengan gips (void)(...).

Sebuah [[nodiscard]]atribut yang paling berguna dalam dua kasus:

  • jika suatu fungsi tidak memiliki efek selain mengembalikan hasil tertentu, yaitu murni. Jika hasilnya tidak digunakan, panggilan itu tentu tidak berguna. Di sisi lain, membuang hasilnya tidak akan salah.

  • jika nilai kembali harus diperiksa, misalnya untuk antarmuka mirip-C yang mengembalikan kode kesalahan alih-alih melempar. Ini adalah kasus penggunaan utama. Untuk C ++ idiomatis, itu akan sangat langka.

Dua kasus ini meninggalkan ruang besar fungsi tidak murni yang mengembalikan nilai, di mana peringatan seperti itu akan menyesatkan. Sebagai contoh:

  • Pertimbangkan tipe data antrian dengan .pop()metode yang menghapus elemen dan mengembalikan salinan elemen yang dihapus. Metode seperti itu sering kali nyaman. Namun, ada beberapa kasus di mana kami hanya ingin menghapus elemen, tanpa mendapatkan salinan. Itu sangat sah, dan peringatan tidak akan membantu. Desain yang berbeda (seperti std::vector) membagi tanggung jawab ini, tetapi memiliki pengorbanan lainnya.

    Perhatikan bahwa dalam beberapa kasus, salinan harus tetap dibuat sehingga terima kasih kepada RVO untuk mengembalikan salinannya.

  • Pertimbangkan antarmuka yang lancar, di mana setiap operasi mengembalikan objek sehingga operasi lebih lanjut dapat dilakukan. Dalam C ++, contoh paling umum adalah operator penyisipan arus <<. Akan sangat merepotkan untuk menambahkan [[nodiscard]]atribut ke setiap <<kelebihan.

Contoh-contoh ini menunjukkan bahwa membuat kompilasi kode C ++ idiomatis tanpa peringatan di bawah bahasa "C ++ 17 dengan nodiscard secara default" akan sangat membosankan.

Perhatikan bahwa kode C ++ 17 baru Anda yang mengkilap (tempat Anda dapat menggunakan atribut ini) masih dapat dikompilasi bersama dengan pustaka yang menargetkan standar C ++ yang lebih lama. Kompatibilitas mundur ini sangat penting untuk ekosistem C / C ++. Jadi membuat nodiscard sebagai default akan menghasilkan banyak peringatan untuk kasus penggunaan tipikal, idiomatik - peringatan yang tidak dapat Anda perbaiki tanpa perubahan yang jauh ke kode sumber perpustakaan.

Diperdebatkan, masalahnya di sini bukanlah perubahan semantik tetapi bahwa fitur dari setiap standar C ++ berlaku pada lingkup per-kompilasi-unit dan bukan pada lingkup per-file. Jika / ketika beberapa standar C ++ masa depan menjauh dari file header, perubahan seperti itu akan lebih realistis.

amon
sumber
6
Saya telah membatalkan ini, karena mengandung wawasan yang bermanfaat. Namun belum yakin apakah itu benar-benar menjawab pertanyaan. Ubah fungsi yang tidak murni seperti popatau operator<<, yang akan ditandai secara eksplisit oleh [[maydiscard]]atribut imajiner saya , jadi tidak ada peringatan yang dihasilkan. Apakah Anda mengatakan fungsi-fungsi seperti itu pada umumnya jauh lebih umum daripada yang ingin saya percayai, atau jauh lebih umum pada umumnya daripada pada basis kode saya sendiri? Paragraf pertama Anda tentang kode yang ada tentu benar, tetapi masih membuat saya bertanya-tanya apakah ada orang lain yang membumbui semua kode baru mereka [[nodiscard]], dan jika tidak, mengapa tidak.
Christian Hackl
23
@ChristianHackl: Ada waktu dalam sejarah C ++ di mana itu dianggap praktik yang baik untuk mengembalikan nilai sebagai const. Anda tidak bisa mengatakannya returns_an_int() = 0, jadi mengapa harus returns_a_str() = ""bekerja? C ++ 11 menunjukkan ini sebenarnya ide yang buruk, karena sekarang semua kode ini melarang bergerak karena nilainya adalah const. Saya pikir pengambilan yang tepat dari pelajaran itu adalah "itu tidak terserah Anda apa yang dilakukan penelepon Anda dengan hasil Anda". Mengabaikan hasilnya adalah hal yang sangat valid yang mungkin ingin dilakukan oleh penelepon, dan kecuali Anda tahu itu salah (bilah yang sangat tinggi), Anda tidak boleh memblokirnya.
GManNickG
13
Sebuah nodiscard on empty()juga masuk akal karena namanya cukup ambigu: apakah itu sebuah predikat is_empty()atau apakah itu sebuah mutator clear()? Anda biasanya tidak akan mengharapkan mutator seperti itu untuk mengembalikan apa pun, sehingga peringatan tentang nilai pengembalian dapat mengingatkan programmer tentang suatu masalah. Dengan nama yang lebih jelas, atribut ini tidak akan seperlunya.
Amon
13
@ChristianHackl: Seperti yang orang lain katakan, saya pikir fungsi murni contoh yang baik kapan ini harus digunakan Saya akan melangkah lebih jauh dan mengatakan harus ada [[pure]]atribut, dan itu harus menyiratkan [[nodiscard]]. Dengan begitu jelas mengapa hasil ini tidak boleh dibuang. Tapi selain fungsi murni, saya tidak bisa memikirkan kelas fungsi lain yang seharusnya memiliki perilaku ini. Dan sebagian besar fungsi tidak murni, maka saya tidak menganggap nodiscard sebagai default adalah pilihan yang tepat.
GManNickG
5
@ GMNickG: Setelah mencoba dan gagal men-debug kode yang mengabaikan kode kesalahan, saya sangat percaya sebagian besar kode kesalahan perlu diperiksa secara default.
Mooing Duck
9

Alasan saya [[nodiscard]]hampir tidak akan kemana-mana:

  1. (utama :) ini akan memperkenalkan cara terlalu banyak kebisingan di header saya.
  2. (utama :) Saya tidak merasa harus membuat asumsi spekulatif yang kuat tentang kode orang lain. Anda ingin membuang nilai pengembalian yang saya berikan? Baik, jatuhkan dirimu.
  3. (kecil :) Anda akan menjamin ketidakcocokan dengan C ++ 14

Sekarang, jika Anda menjadikannya default, Anda akan memaksa semua pengembang perpustakaan untuk memaksa semua penggunanya untuk tidak membuang nilai pengembalian. Itu akan mengerikan. Atau Anda memaksa mereka untuk menambahkan [[maydiscard]]banyak sekali fungsi, yang juga agak mengerikan.

einpoklum - mengembalikan Monica
sumber
0

Sebagai contoh: operator << memiliki nilai balik yang bergantung pada panggilan yang benar-benar diperlukan atau sama sekali tidak berguna. (std :: cout << x << y, yang pertama diperlukan karena mengembalikan aliran, yang kedua tidak berguna sama sekali). Sekarang bandingkan dengan printf di mana semua orang membuang nilai kembali yang ada untuk pengecekan kesalahan, tetapi kemudian operator << tidak memiliki pengecekan kesalahan sehingga tidak mungkin bermanfaat di tempat pertama. Jadi, dalam kedua kasus, nodiscard hanya akan merusak.

gnasher729
sumber
Ini menjawab pertanyaan yang tidak diajukan, yaitu "Apa alasan untuk tidak menggunakan di [[nodiscard]]mana - mana?".
Christian Hackl