
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
PendahuluanBab 1. Viam supervadet vadensBab 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endifBab 3. Menemukan implementasi nullptr yang sempurnaBab 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 templatBab 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 .

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