Dalam pidatonya di CppCon 2018, Herb Sutter menyampaikan kepada publik prestasinya dalam dua arah. Pertama-tama, ini adalah kontrol variabel seumur hidup (Lifetime), yang memungkinkan mendeteksi seluruh kelas bug pada tahap kompilasi. Kedua, ini adalah proposal yang diperbarui pada metaclasses , yang akan memungkinkan menghindari duplikasi kode, setelah menggambarkan perilaku kategori kelas dan kemudian menghubungkannya ke kelas-kelas tertentu dengan satu baris.
Kata Pengantar: lebih = lebih mudah?!
Tuduhan C ++ terdengar bahwa standar tersebut tumbuh tanpa alasan dan tanpa ampun. Tetapi bahkan konservatif yang paling bersemangat tidak akan berpendapat bahwa konstruksi baru seperti range-for (siklus pengumpulan) dan otomatis (setidaknya untuk iterator) membuat kode lebih sederhana. Anda dapat mengembangkan perkiraan kriteria yang harus dipenuhi (setidaknya satu, idealnya semua) ekstensi bahasa baru untuk menyederhanakan kode dalam praktik:
- Kurangi, sederhanakan kode, hapus kode duplikat (range-for, auto, lambda, Metaclasses)
- Membuat kode aman lebih mudah untuk ditulis, mencegah kesalahan dan kasus khusus (smart pointer, Lifetimes)
- Ganti sepenuhnya fitur lama yang kurang berfungsi (typedef → using)
Herb Sutter mengidentifikasi "C ++ modern" - subset fitur yang memenuhi standar pengkodean modern (seperti Pedoman Inti C ++ ), dan menganggap standar penuh sebagai "mode kompatibilitas", yang tidak perlu diketahui oleh semua orang. Dengan demikian, jika "C ++ modern" tidak tumbuh, maka semuanya baik-baik saja.
Memeriksa masa pakai variabel (Seumur Hidup)
Grup Verifikasi Seumur Hidup baru sekarang tersedia sebagai bagian dari Pemeriksa Pedoman Inti untuk Dentang dan Visual C ++. Tujuannya bukan untuk mencapai ketelitian dan akurasi absolut, seperti pada Rust, tetapi untuk melakukan pemeriksaan sederhana dan cepat dalam fungsi individu.
Prinsip dasar verifikasi
Dari sudut pandang analisis seumur hidup, jenis dibagi menjadi 3 kategori:
- Nilai adalah apa yang Pointer dapat tunjukkan.
- Pointer - mengacu pada Nilai, tetapi tidak mengontrol masa pakainya. Mungkin menggantung (penunjuk menggantung). Contoh:
T*
, T&
, iterators, std::observer_ptr<T>
, std::string_view
, gsl::span<T>
- Pemilik - mengontrol umur Nilai. Biasanya dapat menghapus nilainya lebih cepat dari jadwal. Contoh:
std::unique_ptr<T>
, std::shared_ptr<T>
, std::vector<T>
, std::string
, gsl::owner<T*>
Pointer dapat berada di salah satu dari kondisi berikut:
- Tunjuk ke nilai yang disimpan di tumpukan
- Tunjuk ke Nilai yang terkandung "di dalam" oleh beberapa Pemilik
- Kosong (nol)
- Hang (tidak valid)
Pointer dan Nilai
Untuk setiap Pointer p dilacak pset(p) - set nilai yang dapat ditunjukkannya. Saat menghapus Nilai, ini terjadi di semua pset diganti oleh tidakvalid . Saat mengakses Nilai Pointer p sedemikian rupa tidakvalid∈pset(p) mengeluarkan kesalahan.
string_view s;
Menggunakan anotasi, Anda dapat mengonfigurasi operasi mana yang akan dianggap operasi mengakses Nilai. Secara default: *
, ->
, []
, begin()
, end()
.
Harap perhatikan bahwa peringatan dikeluarkan hanya pada saat akses ke Indeks yang tidak valid. Jika Nilai dihapus, tetapi tidak ada yang pernah mengakses Pointer ini, maka semuanya beres.
Rambu dan Pemilik
Jika Pointer p menunjukkan nilai yang terkandung dalam pemilik o lalu ini pset(p)=o′ .
Metode dan fungsi yang mengambil pemilik, dibagi menjadi:
- Operasi Akses Nilai Pemilik. Default:
*
, ->
, []
, begin()
, end()
- Akses operasi ke pemilik itu sendiri, pointer
v.clear()
, seperti v.clear()
. Secara default, ini semua adalah operasi non-const lainnya - Akses operasi ke Pemilik itu sendiri, pointer tidak valid, seperti
v.empty()
. Secara default, ini semua adalah operasi const.
Pemilik Konten Lama Diumumkan t i d a k v a l i d setelah penghapusan Pemilik atau pada aplikasi operasi yang tidak valid.
Aturan-aturan ini cukup untuk mendeteksi banyak bug khas dalam kode C ++:
string_view s;
vector<int> v = get_ints(); int* p = &v[5];
std::string_view s = "foo"s; cout << s[0];
vector<int> v = get_ints(); for (auto i = v.begin(); i != v.end(); ++i) {
std::optional<std::vector<int>> get_data();
Melacak parameter fungsi seumur hidup
Ketika kita mulai berurusan dengan fungsi dalam C ++ yang mengembalikan Pointer, kita hanya bisa menebak tentang hubungan antara masa pakai parameter dan nilai balik. Jika suatu fungsi menerima dan mengembalikan Pointer dari jenis yang sama, maka asumsi dibuat bahwa fungsi "mendapatkan" nilai pengembalian dari salah satu parameter input:
auto f(int* p, int* q) -> int*;
Fungsi mencurigakan mudah dideteksi yang mengambil hasil entah dari mana:
std::reference_wrapper<int> get_data() {
Karena dimungkinkan untuk memberikan nilai sementara ke parameter const T&
parameter, mereka tidak diperhitungkan, kecuali jika hasilnya tidak ada di tempat lain untuk mengambil:
template <typename T> const T& min(const T& x, const T& y);
using K = std::string; using V = std::string; const V& find_or_default(const std::map<K, V>& m, const K& key, const V& def);
Juga diyakini bahwa jika suatu fungsi menerima sebuah pointer (bukan referensi), maka itu bisa menjadi nullptr, dan pointer ini tidak dapat digunakan sebelum membandingkan dengan nullptr.
Kesimpulan Kontrol Waktu Seumur Hidup
Saya ulangi bahwa Lifetime belum merupakan proposal untuk standar C ++, tetapi upaya berani untuk menerapkan pemeriksaan seumur hidup di C ++, di mana, tidak seperti Rust, misalnya, tidak pernah ada penjelasan yang sesuai. Pada awalnya, akan ada banyak positif palsu, tetapi seiring waktu, heuristik akan meningkat.
Pertanyaan dari audiens
Apakah pemeriksaan grup Seumur Hidup memberikan jaminan matematis yang akurat dari tidak adanya petunjuk yang menggantung?
Secara teoritis, akan dimungkinkan (dalam kode baru) untuk menggantung banyak anotasi pada kelas dan fungsi, dan sebagai gantinya kompiler akan memberikan jaminan tersebut. Tetapi pemeriksaan ini dikembangkan mengikuti prinsip 80:20, yaitu, Anda dapat menangkap sebagian besar kesalahan menggunakan sejumlah kecil aturan dan menerapkan minimum anotasi.
Metaclass dalam beberapa cara melengkapi kode kelas yang diterapkannya, dan juga berfungsi sebagai nama untuk sekelompok kelas yang memenuhi persyaratan tertentu. Misalnya, seperti yang ditunjukkan di bawah ini, metaclass interface
akan membuat semua fungsi publik dan murni virtual untuk Anda.
Tahun lalu, Herb Sutter membuat proyek metaclass pertamanya ( lihat di sini ). Sejak itu, sintaks yang diajukan saat ini telah berubah.
Sebagai permulaan, sintaks untuk menggunakan metaclasses telah berubah:
Sudah menjadi lebih lama, tetapi sekarang ada sintaksis alami untuk menerapkan beberapa metaclasses sekaligus: class(meta1, meta2)
.
Sebelumnya, metaclass adalah seperangkat aturan untuk memodifikasi kelas. Sekarang metaclass adalah fungsi constexpr yang mengambil kelas lama (dideklarasikan dalam kode) dan membuat yang baru.
Yaitu, fungsi mengambil satu parameter - meta-informasi tentang kelas lama (tipe parameter tergantung pada implementasinya), membuat elemen kelas (fragmen), dan kemudian menambahkannya ke badan kelas baru menggunakan instruksi __generate
.
Fragmen dapat dibuat menggunakan __fragment
, __inject
, idexpr(…)
. Pembicara memilih untuk tidak fokus pada tujuan mereka, karena bagian ini masih akan berubah sebelum diserahkan kepada komite standardisasi. Nama-nama itu sendiri dijamin akan diubah, garis bawah ganda ditambahkan secara khusus untuk memperjelas hal ini. Penekanan dalam laporan ini adalah pada contoh-contoh yang melangkah lebih jauh.
antarmuka
template <typename T> constexpr void interface(T source) {
Anda mungkin berpikir bahwa pada baris (1) dan (2) kami memodifikasi kelas asli, tetapi tidak. Harap dicatat bahwa kami mengulangi fungsi-fungsi kelas asli dengan menyalin, memodifikasi fungsi-fungsi ini, dan kemudian menyisipkannya ke kelas baru.
Aplikasi Metaclass:
class(interface) Shape { int area() const; void scale_by(double factor); };
Debugging Mutex
Misalkan kita memiliki data aman non-utas yang dilindungi oleh mutex. Debugging dapat difasilitasi jika, dalam rakitan debug, pada setiap panggilan, diperiksa apakah proses saat ini telah mengunci mutex ini. Untuk melakukan ini, kelas TestableMutex sederhana ditulis:
class TestableMutex { public: void lock() { m.lock(); id = std::this_thread::get_id(); } void unlock() { id = std::thread::id{}; m.unlock(); } bool is_held() { return id == std::this_thread::get_id(); } private: std::mutex m; std::atomic<std::thread::id> id; };
Selanjutnya, di kelas MyData kami, kami ingin setiap bidang publik suka
vector<int> v;
Ganti dengan + pengambil:
private: vector<int> v_; public: vector<int>& v() { assert(m_.is_held()); return v_; }
Untuk fungsi, kita juga dapat melakukan transformasi serupa.
Tugas-tugas tersebut diselesaikan menggunakan makro dan pembuatan kode. Herb Sutter menyatakan perang terhadap makro: mereka tidak aman, mengabaikan semantik, ruang nama, dll. Seperti apa solusinya pada metaclasses:
constexpr void guarded_with_mutex() { __generate __fragment class { TestableMutex m_;
Cara menggunakannya:
class(guarded) MyData { vector<int> v; Widget* w; }; MyData& x = findData("foo"); xv().clear();
aktor
Yah, bahkan jika kita melindungi beberapa objek dengan mutex, sekarang semuanya aman, tidak ada klaim kebenaran. Tetapi jika suatu objek sering dapat diakses oleh banyak utas secara paralel, mutex akan kelebihan beban, dan akan ada overhead yang besar untuk mengambilnya.
Solusi mendasar untuk masalah mutex kereta adalah konsep aktor, ketika suatu objek memiliki antrian permintaan, semua panggilan ke objek diantrekan dan dieksekusi satu demi satu di utas khusus.
Biarkan kelas Aktif berisi implementasi dari semua ini - pada kenyataannya, kumpulan thread / pelaksana dengan utas tunggal. Nah, metaclasses akan membantu menyingkirkan kode duplikat dan mengantre semua operasi:
class(active) ImageFilter { public: ImageFilter(std::function<void(Buffer*)> w) : work(std::move(w)) {} void apply(Buffer* b) { work(b); } private: std::function<void(Buffer*)> work; }
class(active) log { std::fstream f; public: void info(…) { f << …; } };
properti
Ada properti di hampir semua bahasa pemrograman modern, dan siapa pun yang tidak mengimplementasikannya berdasarkan C ++: Qt, C ++ / CLI, semua jenis makro jelek. Namun, mereka tidak akan pernah ditambahkan ke standar C ++, karena mereka sendiri dianggap fitur yang terlalu sempit, dan selalu ada harapan bahwa beberapa proposal akan mengimplementasikannya sebagai kasus khusus. Ya, mereka dapat diimplementasikan pada metaclasses!
Anda dapat mengatur pengambil dan penyetel Anda sendiri:
class Date { public: class(property<int>) MonthClass { int month; auto get() { return month; } void set(int m) { assert(m > 0 && m < 13); month = m; } } month; }; Date date; date.month = 15;
Idealnya, saya ingin menulis property int month { … }
, tetapi bahkan implementasi seperti itu akan menggantikan kebun binatang C ++ ekstensi yang menciptakan properti.
Metaclasses adalah fitur baru yang besar untuk bahasa yang sudah kompleks. Apakah itu sepadan? Berikut adalah beberapa manfaatnya:
- Biarkan programmer mengekspresikan niat mereka dengan lebih jelas (saya ingin menulis aktor)
- Mengurangi duplikasi kode dan menyederhanakan pengembangan dan pemeliharaan kode yang mengikuti pola tertentu
- Hilangkan beberapa kelompok kesalahan umum (itu akan cukup untuk mengurus semua seluk-beluk sekali)
- Bolehkan untuk menyingkirkan makro? (Herb Sutter sangat berperang)
Pertanyaan dari audiens
Bagaimana cara debug metaclasses?
Setidaknya untuk Dentang, ada fungsi intrinsik yang, jika dipanggil, akan mencetak isi sebenarnya dari kelas pada waktu kompilasi, yaitu, apa yang diperoleh setelah menerapkan semua metaclasses.
Dulu dikatakan bisa mendeklarasikan non-anggota seperti swap dan hash di metaclasses. Kemana dia pergi?
Sintaks akan dikembangkan lebih lanjut.
Mengapa kita perlu metaclasses jika konsep telah diadopsi untuk standardisasi?
Ini adalah hal yang berbeda. Metaclasses diperlukan untuk mendefinisikan bagian-bagian kelas, dan konsep memeriksa untuk melihat apakah kelas cocok dengan pola tertentu menggunakan contoh kelas. Bahkan, metaclasses dan konsep bekerja dengan baik bersama. Misalnya, Anda dapat mendefinisikan konsep iterator dan metaclass dari "iterator tipikal" yang mendefinisikan beberapa operasi berlebihan melalui sisanya.