Bagaimana saya menulis library C ++ 11 standar atau mengapa boost sangat menakutkan. Bab 3

Kami melanjutkan petualangan.

Ringkasan bagian sebelumnya


Karena pembatasan pada kemampuan untuk menggunakan kompiler C ++ 11, dan dari kurangnya alternatif, dorongan ingin menulis implementasi sendiri dari pustaka C ++ 11 standar di atas pustaka C ++ 98 / C ++ 03 yang disertakan dengan kompiler.

Static_assert , noexcept , countof diimplementasikan, dan juga, setelah mempertimbangkan semua definisi non-standar dan fitur kompiler, informasi muncul tentang fungsi yang didukung oleh kompiler saat ini. Ini melengkapi deskripsi core.h , tetapi itu tidak akan lengkap tanpa nullptr .

Tautan ke GitHub dengan hasil untuk hari ini untuk yang tidak sabar dan yang bukan pembaca:

Komitmen dan kritik yang membangun dipersilahkan

Jadi, mari kita lanjutkan.

Daftar isi


Pendahuluan
Bab 1. Viam supervadet vadens
Bab 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif
Bab 3. Menemukan implementasi nullptr yang sempurna
Bab 4. C ++ Template Magic
.... 4.1 Kita mulai dari yang kecil
.... 4.2 Tentang berapa banyak kesalahan ajaib yang dikompilasi oleh log untuk kita
.... 4.3 Pointer dan semuanya
.... 4.4 Apa lagi yang diperlukan untuk pustaka templat
Bab 5
...

Bab 3. Menemukan implementasi nullptr yang sempurna


Setelah seluruh epik dengan makro kompiler non-standar dan penemuan "luar biasa" yang mereka sajikan, saya akhirnya bisa menambahkan nullptr dan itu menghangatkan jiwa saya. Akhirnya, Anda dapat menyingkirkan semua perbandingan ini dengan 0 atau bahkan NULL .

gambar Kebanyakan programmer mengimplementasikan nullptr sebagai
#define nullptr 0 

dan ini bisa mengakhiri bab ini. Jika Anda menginginkan diri Anda nullptr , maka ganti saja 0 dengan definisi seperti itu, karena pada dasarnya ini semua yang diperlukan untuk operasi yang benar.

Jangan lupa untuk benar-benar menulis cek, jika tidak, tiba-tiba orang lain akan ditemukan dengan definisi ini:

 #ifndef nullptr #define nullptr 0 #else #error "nullptr defined already" #endif 

Arahan preprocessor #error akan menghasilkan kesalahan dengan teks yang dapat dibaca manusia saat kompilasi, dan, ya, ini adalah arahan standar, penggunaannya jarang, tetapi dapat ditemukan.

Tetapi dalam implementasi seperti itu, kita kehilangan salah satu poin penting yang dijelaskan dalam standar, yaitu std :: nullptr_t - tipe terpisah, contoh konstan di antaranya adalah nullptr . Dan pengembang kromium juga pernah mencoba untuk memecahkan masalah ini (sekarang ada kompiler baru dan nullptr normal) mendefinisikannya sebagai kelas yang dapat dikonversi ke pointer ke jenis apa pun. Karena, secara standar, ukuran nullptr harus sama dengan ukuran pointer ke void (dan void * juga harus mengandung pointer apa pun, kecuali pointer ke anggota kelas), kami "membakukan" implementasi ini dengan menambahkan pointer nol yang tidak digunakan:

 class nullptr_t_as_class_impl { public: nullptr_t_as_class_impl() { } nullptr_t_as_class_impl(int) { } // Make nullptr convertible to any pointer type. template<typename T> operator T*() const { return 0; } // Make nullptr convertible to any member pointer type. template<typename C, typename T> operator TC::*() { return 0; } bool operator==(nullptr_t_as_class_impl) const { return true; } bool operator!=(nullptr_t_as_class_impl) const { return false; } private: // Do not allow taking the address of nullptr. void operator&(); void *_padding; }; typedef nullptr_t_as_class_impl nullptr_t; #define nullptr nullptr_t(0) 

Konversi kelas ini ke penunjuk apa pun disebabkan oleh operator templat jenis itu, yang disebut jika ada sesuatu yang dibandingkan dengan nullptr . Yaitu, ekspresi char * my_pointer; if (my_pointer == nullptr) akan benar-benar dikonversi menjadi if (my_pointer == nullptr.operator char * ()) , yang membandingkan pointer ke 0. Operator tipe kedua diperlukan untuk mengonversi nullptr ke pointer ke anggota kelas. Dan di sini Borland C ++ Builder 6.0 “membedakan dirinya”, yang secara tak terduga memutuskan bahwa kedua operator ini identik dan dapat dengan mudah membandingkan pointer ke anggota kelas dan pointer biasa satu sama lain, sehingga ada ketidakpastian setiap kali nullptr dibandingkan dengan pointer (ini adalah bug, dan mungkin tidak hanya dengan kompiler ini). Kami sedang menulis implementasi terpisah untuk kasus ini:

 class nullptr_t_as_class_impl1 { public: nullptr_t_as_class_impl1() { } nullptr_t_as_class_impl1(int) { } // Make nullptr convertible to any pointer type. template<typename T> operator T*() const { return 0; } bool operator==(nullptr_t_as_class_impl1) const { return true; } bool operator!=(nullptr_t_as_class_impl1) const { return false; } private: // Do not allow taking the address of nullptr. void operator&(); void *_padding; }; typedef nullptr_t_as_class_impl1 nullptr_t; #define nullptr nullptr_t(0) 

Keuntungan dari tampilan nullptr ini adalah bahwa sekarang ada tipe terpisah untuk std :: nullptr_t . Kerugian? Konstanta nullptr hilang selama kompilasi dan perbandingan melalui operator ternary, kompilator tidak dapat menyelesaikannya.

 unsigned* case5 = argc > 2 ? (unsigned*)0 : nullptr; //  ,     ':'    STATIC_ASSERT(nullptr == nullptr && !(nullptr != nullptr), nullptr_should_be_equal_itself); //  , nullptr      

Dan saya ingin "dan checker dan pergi." Solusinya terlintas dalam pikiran hanya satu: enum . Anggota enumerasi dalam C ++ akan memiliki tipe terpisah mereka sendiri, dan juga akan dikonversi ke int tanpa masalah (dan sebenarnya mereka adalah konstanta integer). Properti anggota enumerasi ini akan membantu kami, karena 0 yang sangat “spesial” yang digunakan sebagai pengganti nullptr untuk pointer adalah int yang paling umum. Saya belum melihat implementasi nullptr di Internet, dan mungkin itu juga sesuatu yang buruk, tetapi saya tidak tahu mengapa. Mari kita tulis implementasi:

 #ifdef NULL #define STDEX_NULL NULL #else #define STDEX_NULL 0 #endif namespace ptrdiff_detail { using namespace std; } template<bool> struct nullptr_t_as_ulong_type { typedef unsigned long type; }; template<> struct nullptr_t_as_ulong_type<false> { typedef unsigned long type; }; template<bool> struct nullptr_t_as_ushort_type { typedef unsigned short type; }; template<> struct nullptr_t_as_ushort_type<false> { typedef nullptr_t_as_long_type<sizeof(unsigned long) == sizeof(void*)>::type type; }; template<bool> struct nullptr_t_as_uint_type { typedef unsigned int type; }; template<> struct nullptr_t_as_uint_type<false> { typedef nullptr_t_as_short_type<sizeof(unsigned short) == sizeof(void*)>::type type; }; typedef nullptr_t_as_uint_type<sizeof(unsigned int) == sizeof(void*)>::type nullptr_t_as_uint; enum nullptr_t_as_enum { _nullptr_val = ptrdiff_detail::ptrdiff_t(STDEX_NULL), _max_nullptr = nullptr_t_as_uint(1) << (CHAR_BIT * sizeof(void*) - 1) }; typedef nullptr_t_as_enum nullptr_t; #define nullptr nullptr_t(STDEX_NULL) 

Seperti yang Anda lihat di sini sedikit lebih banyak kode daripada hanya mendeklarasikan enum nullptr_t dengan anggota nullptr = 0 . Pertama, mungkin tidak ada definisi NULL . Ini harus didefinisikan dalam daftar header standar yang agak padat , tetapi seperti yang telah ditunjukkan oleh praktik, lebih baik memainkannya dengan aman dan memeriksa makro ini. Kedua, representasi enum di C ++ sesuai dengan standar yang ditentukan implementasi, yaitu tipe enumerasi dapat diwakili oleh tipe integer (dengan ketentuan bahwa tipe-tipe ini tidak boleh lebih dari int , asalkan nilai enum cocok dengan itu). Misalnya, jika Anda mendeklarasikan tes enum {_1, _2}, kompiler dapat dengan mudah menyatakannya sebagai pendek, dan sangat mungkin bahwa sizeof ( test ) ! = Sizeof (void *) . Agar implementasi nullptr mematuhi standar, Anda perlu memastikan bahwa ukuran tipe yang dipilih kompiler untuk nullptr_t_as_enum cocok dengan ukuran penunjuk, mis. sizeof dasarnya sama (void *) . Untuk melakukan ini, menggunakan templat nullptr_t_as ... , pilih tipe integer yang akan sama dengan ukuran pointer, dan kemudian atur nilai maksimum elemen dalam enumerasi kami ke nilai maksimum dari tipe integer ini.
Saya ingin memperhatikan CHAR_BIT makro yang ditentukan dalam header iklim standar. Makro ini disetel ke jumlah bit dalam satu karakter , yaitu jumlah bit per byte pada platform saat ini. Definisi standar yang berguna yang tidak perlu dilewati oleh pengembang dengan menempel delapan di mana-mana, meskipun di beberapa tempat dalam satu byte tidak ada 8 bit sama sekali .

Dan fitur lainnya adalah penugasan NULL sebagai nilai dari elemen enum . Beberapa kompiler memberikan peringatan (dan kekhawatiran mereka dapat dipahami) tentang fakta bahwa NULL ditugaskan ke "non-pengindeks". Kami mengambil namespace standar ke ptrdiff_detail lokal kami, agar tidak mengacaukan sisa namespace, dan kemudian, untuk menenangkan kompilator, kami secara eksplisit mengkonversi NULL ke std :: ptrdiff_t - jenis lain yang agak kurang digunakan dalam C ++, yang berfungsi untuk mewakili hasil operasi aritmatika (pengurangan) dengan pointer dan biasanya merupakan alias dari tipe std :: size_t ( std :: intptr_t di C ++ 11).

SFINAE


Di sini, untuk pertama kalinya dalam cerita saya, kita dihadapkan dengan fenomena seperti itu di C ++ karena kegagalan substitusi bukanlah kesalahan (SFINAE) . Singkatnya, intinya adalah bahwa ketika kompiler "melewati" fungsi yang sesuai kelebihan untuk panggilan tertentu, ia harus memeriksa semuanya, dan tidak berhenti setelah kegagalan pertama atau setelah kelebihan pertama yang cocok. Dari sini muncul pesannya tentang ambiguitas , ketika ada dua kelebihan fungsi yang dipanggil yang identik dari sudut pandang kompiler, serta kemampuan kompiler untuk memilih kelebihan fungsi yang paling tepat untuk panggilan tertentu dengan parameter tertentu. Fitur kompiler ini memungkinkan Anda untuk melakukan bagian terbesar dari semua templat "ajaib" (omong-omong hi std :: enable_if ), dan itu juga merupakan dasar dari boost dan library saya.

Karena, sebagai hasilnya, kami memiliki beberapa implementasi nullptr, kami menggunakan SFINAE "pilih" yang terbaik pada tahap kompilasi. Kami menyatakan tipe "ya" dan "tidak" untuk memeriksa ukuran fungsi penyelidikan yang dinyatakan di bawah ini.

 namespace nullptr_detail { typedef char _yes_type; struct _no_type { char padding[8]; }; struct dummy_class {}; _yes_type _is_convertable_to_void_ptr_tester(void*); _no_type _is_convertable_to_void_ptr_tester(...); typedef void(nullptr_detail::dummy_class::*dummy_class_f)(int); typedef int (nullptr_detail::dummy_class::*dummy_class_f_const)(double&) const; _yes_type _is_convertable_to_member_function_ptr_tester(dummy_class_f); _no_type _is_convertable_to_member_function_ptr_tester(...); _yes_type _is_convertable_to_const_member_function_ptr_tester(dummy_class_f_const); _no_type _is_convertable_to_const_member_function_ptr_tester(...); template<class _Tp> _yes_type _is_convertable_to_ptr_tester(_Tp*); template<class> _no_type _is_convertable_to_ptr_tester(...); } 

Di sini kita akan menggunakan prinsip yang sama seperti pada bab kedua dengan hitungan dan definisinya melalui ukuran nilai pengembalian (array elemen) dari fungsi templat COUNTOF_REQUIRES_ARRAY_ARGUMENT .

 template<class T> struct _is_convertable_to_void_ptr_impl { static const bool value = (sizeof(nullptr_detail::_is_convertable_to_void_ptr_tester((T) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)); }; 

Apa yang sedang terjadi di sini? Pertama, kompiler “ mengulangi ” kelebihan fungsi _is_convertable_to_void_ptr_tester dengan argumen tipe T dan nilai NULL (nilai tidak berperan, hanya NULL yang harus mengetikkan- T ). Hanya ada dua kelebihan - dengan tipe * kosong dan dengan daftar argumen variabel (...) . Mengganti argumen ke masing-masing kelebihan ini, kompiler akan memilih yang pertama jika tipe dilemparkan ke pointer untuk membatalkan , dan yang kedua jika gips tidak dapat dilakukan. Dengan kelebihan yang dipilih oleh kompiler, kami menggunakan sizeof untuk menentukan ukuran nilai yang dikembalikan oleh fungsi, dan karena mereka dijamin berbeda ( sizeof ( _no_type ) == 8 , sizeof ( _yes_type ) == 1 ), kami dapat menentukan ukuran kelebihan beban yang diambil oleh kompiler dan karenanya dikonversi apakah tipe kita tidak valid * atau tidak.

Kami akan menerapkan templat pemrograman yang sama lebih jauh untuk menentukan apakah objek dari jenis pilihan kami untuk mewakili nullptr_t dikonversi ke sembarang penunjuk (pada dasarnya (T) ( STDEX_NULL ) adalah definisi masa depan untuk nullptr ).

 template<class T> struct _is_convertable_to_member_function_ptr_impl { static const bool value = (sizeof(nullptr_detail::_is_convertable_to_member_function_ptr_tester((T) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)) && (sizeof(nullptr_detail::_is_convertable_to_const_member_function_ptr_tester((T) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)); }; template<class NullPtrType, class T> struct _is_convertable_to_any_ptr_impl_helper { static const bool value = (sizeof(nullptr_detail::_is_convertable_to_ptr_tester<T>((NullPtrType) (STDEX_NULL))) == sizeof(nullptr_detail::_yes_type)); }; template<class T> struct _is_convertable_to_any_ptr_impl { static const bool value = _is_convertable_to_any_ptr_impl_helper<T, int>::value && _is_convertable_to_any_ptr_impl_helper<T, float>::value && _is_convertable_to_any_ptr_impl_helper<T, bool>::value && _is_convertable_to_any_ptr_impl_helper<T, const bool>::value && _is_convertable_to_any_ptr_impl_helper<T, volatile float>::value && _is_convertable_to_any_ptr_impl_helper<T, volatile const double>::value && _is_convertable_to_any_ptr_impl_helper<T, nullptr_detail::dummy_class>::value; }; template<class T> struct _is_convertable_to_ptr_impl { static const bool value = ( _is_convertable_to_void_ptr_impl<T>::value == bool(true) && _is_convertable_to_any_ptr_impl<T>::value == bool(true) && _is_convertable_to_member_function_ptr_impl<T>::value == bool(true) ); }; 

Tentu saja, tidak mungkin untuk mengulangi semua petunjuk yang mungkin dan tidak dapat dipahami serta kombinasinya dengan pengubah volatile dan const , oleh karena itu saya membatasi diri hanya pada 9 pemeriksaan ini (dua pada pointer ke fungsi kelas, satu pada pointer ke kosong , tujuh pada pointer ke jenis yang berbeda), yang cukup cukup.

Seperti disebutkan di atas, beberapa kompiler (* khe-khe * ... Borland Builder 6.0 ... * khe *) tidak membedakan antara pointer ke tipe dan anggota kelas, oleh karena itu kami akan menulis cek pembantu lain untuk kasus ini sehingga kami kemudian dapat memilih implementasi nullptr_t yang diinginkan melalui kelas. jika dibutuhkan.

 struct _member_ptr_is_same_as_ptr { struct test {}; typedef void(test::*member_ptr_type)(void); static const bool value = _is_convertable_to_void_ptr_impl<member_ptr_type>::value; }; template<bool> struct _nullptr_t_as_class_chooser { typedef nullptr_detail::nullptr_t_as_class_impl type; }; template<> struct _nullptr_t_as_class_chooser<false> { typedef nullptr_detail::nullptr_t_as_class_impl1 type; }; 

Dan kemudian hanya tinggal memeriksa implementasi yang berbeda dari nullptr_t dan memilih kompiler yang sesuai untuk kompiler.

Memilih implementasi nullptr_t
 template<bool> struct _nullptr_choose_as_int { typedef nullptr_detail::nullptr_t_as_int type; }; template<bool> struct _nullptr_choose_as_enum { typedef nullptr_detail::nullptr_t_as_enum type; }; template<bool> struct _nullptr_choose_as_class { typedef _nullptr_t_as_class_chooser<_member_ptr_is_same_as_ptr::value>::type type; }; template<> struct _nullptr_choose_as_int<false> { typedef nullptr_detail::nullptr_t_as_void type; }; template<> struct _nullptr_choose_as_enum<false> { struct as_int { typedef nullptr_detail::nullptr_t_as_int nullptr_t_as_int; static const bool _is_convertable_to_ptr = _is_convertable_to_ptr_impl<nullptr_t_as_int>::value; static const bool _equal_void_ptr = _is_equal_size_to_void_ptr<nullptr_t_as_int>::value; }; typedef _nullptr_choose_as_int<as_int::_is_convertable_to_ptr == bool(true) && as_int::_equal_void_ptr == bool(true)>::type type; }; template<> struct _nullptr_choose_as_class<false> { struct as_enum { typedef nullptr_detail::nullptr_t_as_enum nullptr_t_as_enum; static const bool _is_convertable_to_ptr = _is_convertable_to_ptr_impl<nullptr_t_as_enum>::value; static const bool _equal_void_ptr = _is_equal_size_to_void_ptr<nullptr_t_as_enum>::value; static const bool _can_be_ct_constant = true;//_nullptr_can_be_ct_constant_impl<nullptr_t_as_enum>::value; }; typedef _nullptr_choose_as_enum<as_enum::_is_convertable_to_ptr == bool(true) && as_enum::_equal_void_ptr == bool(true) && as_enum::_can_be_ct_constant == bool(true)>::type type; }; struct _nullptr_chooser { struct as_class { typedef _nullptr_t_as_class_chooser<_member_ptr_is_same_as_ptr::value>::type nullptr_t_as_class; static const bool _equal_void_ptr = _is_equal_size_to_void_ptr<nullptr_t_as_class>::value; static const bool _can_be_ct_constant = _nullptr_can_be_ct_constant_impl<nullptr_t_as_class>::value; }; typedef _nullptr_choose_as_class<as_class::_equal_void_ptr == bool(true) && as_class::_can_be_ct_constant == bool(true)>::type type; }; 


Pertama, kami memeriksa kemungkinan mewakili nullptr_t sebagai kelas, tetapi karena saya tidak menemukan kompiler universal dari solusi independen , saya tidak menemukan objek tipe yang dapat menjadi konstanta waktu kompilasi (omong-omong, saya terbuka untuk saran mengenai subjek ini, karena kemungkinan hal ini memungkinkan), opsi ini selalu dicentang ( _can_be_ct_constant selalu salah ). Selanjutnya, kita beralih untuk memeriksa varian dengan view through enum . Jika masih tidak memungkinkan untuk ditampilkan (kompiler tidak dapat menampilkan pointer melalui enum atau ukurannya entah bagaimana salah), maka kami mencoba untuk menggambarkannya sebagai tipe integer (yang ukurannya akan sama dengan ukuran pointer untuk membatalkan ). Yah, bahkan jika ini tidak berhasil, maka kami memilih implementasi dari tipe nullptr_t via void * .

Pada titik ini, sebagian besar kekuatan SFINAE dalam kombinasi dengan templat C ++ terungkap, karena itu dimungkinkan untuk memilih implementasi yang diperlukan tanpa beralih ke makro bergantung-kompiler, dan memang ke makro (tidak seperti boost di mana semua ini akan dijejali dengan #ifdef #else # checking endif ).

Tetap hanya untuk menentukan jenis alias untuk nullptr_t di namespace stdex dan define untuk nullptr (untuk memenuhi persyaratan standar lain bahwa alamat nullptr tidak dapat diambil, serta menggunakan nullptr sebagai konstanta waktu kompilasi).

 namespace stdex { typedef detail::_nullptr_chooser::type nullptr_t; } #define nullptr (stdex::nullptr_t)(STDEX_NULL) 

Akhir bab ketiga. Pada bab keempat, akhirnya saya bisa mengetikkan type_traits dan bug apa lagi dalam kompiler yang saya temui selama pengembangan.

Terima kasih atas perhatian anda

Source: https://habr.com/ru/post/id417295/


All Articles