我如何编写标准的C ++ 11库,或者为什么boost如此令人恐惧。 第三章

我们继续冒险。

先前部分的摘要


由于使用C ++ 11编译器的能力受到限制,并且由于缺乏替代性,boost希望在编译器随附的C ++ 98 / C ++ 03库之上编写自己的标准C ++ 11库实现。

实现了static_assertnoexceptcountof ,并且在考虑了所有非标准定义和编译器功能之后,出现了有关当前编译器支持的功能的信息。 这样就完成了core.h的描述,但是如果没有nullptr ,它将是不完整的。

链接到GitHub,为不耐烦的读者和非读者提供今天的结果:

欢迎有建设性的批评和批评

因此,让我们继续。

目录


引言
第1章。Viam supervadet vadens
第2章。#ifndef __CPP11_SUPPORT__#定义__COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif
第3章。找到理想的nullptr实现
第4章C ++模板魔术
.... 4.1我们从小处着手
.... 4.2关于日志为我们编译了多少个奇迹般的错误
.... 4.3指针和所有所有
.... 4.4模板库还需要什么
第五章
...

第3章。找到理想的nullptr实现


在经历了非标准的编译器宏的宏大史诗以及它们所展现的“奇妙”发现之后,我终于可以添加nullptr了 ,这让我很振奋。 最后,您可以使用0甚至NULL摆脱所有这些比较。

图片 大多数程序员将nullptr实现为
#define nullptr 0 

这可能已经结束了本章。 如果您自己想要nullptr ,那么只需将0替换为这样的定义,因为从本质上讲,这是正确操作所需要的。

不要忘记真正写一张支票,否则突然会有其他人发现此定义:

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

预处理程序指令#error会在编译时产生人类可读文本错误,是的,这是一个标准指令,很少使用,但是可以找到。

但是在这样的实现中,我们错过了标准中描述的要点之一,即std :: nullptr_t-一种单独的类型,其常量实例为nullptr 。 铬开发人员曾经尝试解决此问题(现在有一个更新的编译器和常规的nullptr ),将其定义为可以转换为任何类型的指针的类。 由于按标准,由于nullptr的大小应等于指向void的指针的大小(并且void *还应包含任何指针,但指向类成员的指针除外),因此我们通过添加未使用的null指针来“标准化”此实现:

 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) 

此类到任何指针的转换归因于该类型的模板运算符,如果将其与nullptr进行比较,则会调用该运算符。 也就是说,表达式char * my_pointer; if(my_pointer == nullptr)实际上将转换为if(my_pointer == nullptr.operator char *()) ,它将指针与0进行比较。需要第二个类型运算符将nullptr转换为指向类成员的指针。 在这里,Borland C ++ Builder 6.0是“杰出的自己”,他意外地认为这两个运算符是相同的,并且可以轻松地将指向类成员的指针与普通指针相互比较,因此每次将nullptr与指针(这是一个错误,也许不仅限于此编译器)。 我们正在为这种情况编写一个单独的实现:

 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) 

nullptr视图的优点在于,现在为std :: nullptr_t提供了一个单独的类型。 劣势? 在通过三元运算符进行编译和比较时, nullptr常量会丢失 ,编译器无法解析它。

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

我想“和棋子一起走”。 解决方案只有一个: enum 。 C ++中的枚举成员将具有自己单独的类型,并且也将毫无问题地转换为int (实际上,它们是整数常量)。 枚举成员的此属性将为我们提供帮助,因为最常见的int是使用非常“特殊”的0代替指针的nullptr 。 我在互联网上还没有看到过nullptr的这种实现,也许这也很糟糕,但是我不知道为什么。 让我们编写一个实现:

 #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) 

如您所见,这里的代码比仅声明成员nullptr = 0的 枚举 nullptr_t多了一些代码。 首先,可能没有NULL定义。 应该在相当标准的标头列表中定义它,但是如实践所示,最好安全地播放它并检查该宏。 其次,根据实现定义的标准,即C ++,C ++中的枚举表示形式 枚举类型可以用任何整数类型表示(前提是这些类型不能超过int ,前提是枚举适合它)。 例如,如果您声明枚举测试{_1,_2},则编译器可以轻松地将其表示为short,那么很有可能sizeof( test != Sizeof(void *) 。 为了使nullptr实现符合标准,您需要确保编译器为nullptr_t_as_enum选择的类型的大小与指针的大小相匹配,即 基本上等于sizeof(void *) 。 为此,请使用nullptr_t_as ...模板,选择一个等于指针大小的整数类型,然后将枚举中元素的最大值设置为该整数类型的最大值。
我要注意标准climits标头中定义的宏CHAR_BIT 。 此宏设置为一个字符中的位数,即 当前平台上每字节的位数。 一个有用的标准定义,开发人员不应有的通过在所有位置使用八位数字来绕开,尽管在一个字节中的某些地方根本没有8位

另一个功能是将NULL分配为enum元素的值。 一些编译器会向NULL分配“非索引器”这一事实发出警告(并且可以理解他们的担忧)。 我们将标准名称空间取出到本地ptrdiff_detail中 ,以免使名称空间的其余部分杂乱无章,然后,为了使编译器平静,我们将NULL显式转换为std :: ptrdiff_t -C ++中另一种使用不足的类型,用于表示算术运算的结果(减)带有指针,通常是std :: size_t类型的别名(在C ++ 11中为std :: intptr_t )。

FINAE


在这里,这是我故事中的第一次,我们在C ++中遇到这样的现象,即替换失败不是错误(SFINAE) 。 简而言之,其本质在于,当编译器“遍历”特定调用的适当函数重载时,它应检查所有这些重载,并且在第一次失败或第一次适当的重载之后不要停止。 从歧义出发,来自编译器的观点有两个相同的重载,以及编译器为具有特定参数的特定调用选择最合适的函数重载的能力,由此产生了有关歧义的信息。 编译器的此功能使您可以对所有模板“魔术”进行最大的贡献 (通过std :: enable_if方式 ),并且它也是boost和我的库的基础。

结果,由于有几个nullptr实现,因此我们在编译阶段使用SFINAE“选择”最佳方法。 我们声明类型“是”和“否”,以检查下面声明的探测函数的大小

 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(...); } 

在这里,我们将使用与第二章相同的原理,即countof及其通过模板函数COUNTOF_REQUIRES_ARRAY_ARGUMENT 返回值(元素数组)的sizeof进行定义。

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

这是怎么回事 首先,编译器“ 迭代_is_convertable_to_void_ptr_tester函数的重载,该重载具有类型T的参数和NULL值 (该值不起作用,只是NULL必须为Type T )。 只有两个重载-void *类型和变量参数列表(...) 。 将参数替换为这些重载中的每一个,如果类型强制转换为指向void的指针,则编译器将选择第一个,如果无法执行强制转换 ,则选择第二个。 使用编译器选择的重载后,我们使用sizeof来确定函数返回的值的大小,并且由于保证了它们的不同( sizeof( _no_type == 8sizeof( _yes_type == 1 ),我们可以确定编译器选择并转换的重载大小。我们的类型是否为空*

我们将进一步应用相同的编程模板,以确定是否将我们选择的表示nullptr_t的类型的对象转换为任何指针(本质上(T)( STDEX_NULL 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) ); }; 

当然,不可能使用volatileconst 修饰符遍历所有可能的和难以想象的指针及其组合,因此,我只限于这9个检查(两个指向类函数的指针,一个指向void的指针,七个指向不同类型的指针),就足够了。

如上所述,某些(* khe-khe * ... Borland Builder 6.0 ... * khe *)编译器无法区分类型的指针和类的成员,因此我们将针对这种情况编写另一种辅助检查,以便我们可以通过该类选择所需的nullptr_t实现如果需要的话。

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

然后剩下的只是检查nullptr_t的不同实现,并为编译器选择适当的编译器。

选择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; }; 


首先,我们检查是否可以将nullptr_t表示为类,但是由于我没有找到独立解决方案的通用编译器,因此我没有找到可以作为编译时间常数的类型对象(顺便说一句,我很乐意接受有关此问题的建议,因为这很可能实现),始终选中此选项( _can_be_ct_constant始终为false )。 接下来,我们切换到通过enum查看带有视图的变量。 如果仍然无法显示(编译器无法通过枚举显示指针或大小不正确),那么我们尝试将其表示为整数类型(其大小将等于void指针的大小)。 好吧,即使这不起作用,也可以通过void *选择nullptr_t类型的实现。

至此,揭示了SFINAE与C ++模板结合使用的大多数功能,因此可以选择必要的实现,而不必依赖于编译器相关的宏,甚至不依赖于宏(与boost不同,在所有这些情况下, #ifdef #else#检查都会塞满这些东西) endif )。

剩下的只是为命名空间 stdex中的nullptr_t定义一个类型别名,以及为nullptr定义(为了遵守不能采用nullptr地址的另一个标准要求,以及使用nullptr作为编译时间常数)。

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

第三章结束。 在第四章中,我终于了解了type_traits以及在开发过程中遇到的编译器中的其他错误。

谢谢您的关注。

Source: https://habr.com/ru/post/zh-CN417295/


All Articles