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

是的-是的,我奉行这一座右铭。

先前部分的摘要


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

除了标准头文件type_traits之外 ,还添加 threadMutexchrononullptr.h它们实现了std :: nullptr_tcore.h ,其中添加了与依赖于编译器的功能相关的宏,并扩展了标准库。

链接到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模板库还需要什么
第五章
...

第2章。#ifndef __CPP11_SUPPORT__#定义__COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif


在对所有代码进行了一些梳理并将其按“标准”标头划分为单独的命名空间stdex后,我继续填充type_traitsnullptr.h以及相同的core.h ,其中包含宏来确定编译器使用并支持的标准版本本机nullptrchar16_tchar32_tstatic_assert

从理论上讲,一切都很简单-根据C ++标准(第14.8条), __cplusplus宏必须由编译器定义,并与受支持标准的版本相对应:

C++ pre-C++98: #define __cplusplus 1 C++98: #define __cplusplus 199711L C++98 + TR1: #define __cplusplus 199711L // ??? C++11: #define __cplusplus 201103L C++14: #define __cplusplus 201402L C++17: #define __cplusplus 201703L 

因此,确定支持可用性的代码很简单:

 #if (__cplusplus >= 201103L) //  C++ 11   #define _STDEX_NATIVE_CPP11_SUPPORT //   11  (nullptr, static_assert) #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT //    char16_t, char32_t #endif 

图片 实际上,并非所有事情都那么简单,现在有趣的拐杖开始了。

首先,不是所有的编译器,甚至没有编译器都没有完全,立即实现下一个标准。 例如,在Visual Studio 2013 中,很长一段时间没有constexpr ,而它声称它支持C ++ 11-并警告说实现不完整。 也就是说, auto -please, static_assert-一样容易(即使从早期的MS VS起也一样),但是constexpr 却不容易。 其次,并不是所有的编译器(而且更令人惊讶)都正确地公开了此定义并及时进行了更新。 突然,在同一个编译器中,Visual Studio 并未从编译器的第一个版本中更改__cplusplus 定义的版本,尽管早就宣布了对C ++ 11的完全支持(这也不是真的,对此存在不同的不满之情-一旦对话涉及“新功能”的特定功能时) “ 11位标准开发人员立即说,没有C99预处理器,没有其他”功能”)。 如果标准编译器不完全符合声明的标准,则允许编译器将此定义设置为与上述值不同的情况,这使情况更加恶化。 例如,假设为给定的宏进行这样的定义定义是合理的(随着新功能的引入,增加了隐藏在该定义后面的数量):

 standart C++98: #define __cplusplus 199711L // C++98 standart C++98 + TR1: #define __cplusplus 200311L // C++03 nonstandart C++11: #define __cplusplus 200411L // C++03 + auto and dectype nonstandart C++11: #define __cplusplus 200511L // C++03 + auto, dectype and constexpr(partly) ... standart C++11: #define __cplusplus 201103L // C++11 

但是与此同时,没有一个主要的流行编译器对此功能“磨损”。

由于所有这些(我不怕这个词),现在,对于每个非标准编译器,您都必须编写自己的特定检查,以找出哪种C ++标准及其支持的程度。 好消息是,我们只需要学习一些编译器功能即可正常工作。 首先,我们现在通过_MSC_VER宏添加Visual Studio的版本检查,此编译器是唯一的。 因为在我所支持的编译器库中,还有C ++ Borland Builder 6.0,而其开发人员又非常热衷于与Visual Studio兼容(包括其“功能”和错误),所以突然之间也有了这个宏。 对于兼容clang的编译器,有一个非标准宏__has_feature( feature_name ,您可以通过它查看编译器是否支持该功能。 结果,代码膨胀为:

 #ifndef __has_feature #define __has_feature(x) 0 // Compatibility with non-clang compilers. #endif // Any compiler claiming C++11 supports, Visual C++ 2015 and Clang version supporting constexpr #if ((__cplusplus >= 201103L) || (_MSC_VER >= 1900) || (__has_feature(cxx_constexpr))) // C++ 11 implementation #define _STDEX_NATIVE_CPP11_SUPPORT #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif 

是否想接触更多的编译器? 我们添加了对Codegear C ++ Builder的检查,该检查器是Borland的继承人(处于最坏的表现,但稍后会详细介绍):

 #ifndef __has_feature #define __has_feature(x) 0 // Compatibility with non-clang compilers. #endif // Any compiler claiming C++11 supports, Visual C++ 2015 and Clang version supporting constexpr #if ((__cplusplus >= 201103L) || (_MSC_VER >= 1900) || (__has_feature(cxx_constexpr))) // C++ 11 implementation #define _STDEX_NATIVE_CPP11_SUPPORT #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #if !defined(_STDEX_NATIVE_CPP11_TYPES_SUPPORT) #if ((__cplusplus > 199711L) || defined(__CODEGEARC__)) #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #endif 

还值得注意的是,由于Visual Studio已经支持_MSC_VER 1600编译器版本的nullptr以及内置类型char16_tchar32_t ,因此我们需要正确处理此问题。 添加了更多检查:

 #ifndef __has_feature #define __has_feature(x) 0 // Compatibility with non-clang compilers. #endif // Any compiler claiming C++11 supports, Visual C++ 2015 and Clang version supporting constexpr #if ((__cplusplus >= 201103L) || (_MSC_VER >= 1900) || (__has_feature(cxx_constexpr))) // C++ 11 implementation #define _STDEX_NATIVE_CPP11_SUPPORT #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #if !defined(_STDEX_NATIVE_CPP11_TYPES_SUPPORT) #if ((__cplusplus > 199711L) || defined(__CODEGEARC__)) #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #endif #if ((!defined(_MSC_VER) || _MSC_VER < 1600) && !defined(_STDEX_NATIVE_CPP11_SUPPORT)) #define _STDEX_IMPLEMENTS_NULLPTR_SUPPORT #else #define _STDEX_NATIVE_NULLPTR_SUPPORT #endif #if (_MSC_VER >= 1600) #ifndef _STDEX_NATIVE_CPP11_TYPES_SUPPORT #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #endif 

同时,我们还将检查是否支持C ++ 98,因为对于不带C ++ 98的编译器,标准库中不会包含某些头文件,并且我们无法使用编译器来验证它们的不存在。

全选
 #ifndef __has_feature #define __has_feature(x) 0 // Compatibility with non-clang compilers. #endif // Any compiler claiming C++11 supports, Visual C++ 2015 and Clang version supporting constexpr #if ((__cplusplus >= 201103L) || (_MSC_VER >= 1900) || (__has_feature(cxx_constexpr))) // C++ 11 implementation #define _STDEX_NATIVE_CPP11_SUPPORT #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #if !defined(_STDEX_NATIVE_CPP11_TYPES_SUPPORT) #if ((__cplusplus > 199711L) || defined(__CODEGEARC__)) #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #endif #if ((!defined(_MSC_VER) || _MSC_VER < 1600) && !defined(_STDEX_NATIVE_CPP11_SUPPORT)) #define _STDEX_IMPLEMENTS_NULLPTR_SUPPORT #else #define _STDEX_NATIVE_NULLPTR_SUPPORT #endif #if (_MSC_VER >= 1600) #ifndef _STDEX_NATIVE_CPP11_TYPES_SUPPORT #define _STDEX_NATIVE_CPP11_TYPES_SUPPORT #endif #endif #if _MSC_VER // Visual C++ fallback #define _STDEX_NATIVE_MICROSOFT_COMPILER_EXTENSIONS_SUPPORT #define _STDEX_CDECL __cdecl #if (__cplusplus >= 199711L) #define _STDEX_NATIVE_CPP_98_SUPPORT #endif #endif // C++ 98 check: #if ((__cplusplus >= 199711L) && ((defined(__INTEL_COMPILER) || defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 4)))))) #ifndef _STDEX_NATIVE_CPP_98_SUPPORT #define _STDEX_NATIVE_CPP_98_SUPPORT #endif #endif 


现在,来自boost的大量配置开始出现在我的记忆中,许多努力工作的开发人员编写了所有这些依赖于编译器的宏,并绘制了特定版本的特定编译器所支持和不支持的内容的地图,我个人对此感到不安,我永远不要看或触摸它。 但好消息是您可以在这里停下来。 至少对于我来说,这足以支持大多数流行的编译器,但是如果您发现不准确或想要添加其他编译器,我将非常乐意接受请求请求。

与boost相比,这是一个了不起的成就,我相信可以在代码中保持与编译器相关的宏的分布,这使代码更整洁,更易于理解,并且不会为每个OS和每个编译器堆积数十个配置文件。 稍后我们将讨论这种方法的缺点。

在这一阶段,我们已经可以开始连接11种标准中缺少的功能,而我们首先介绍的是static_assert

static_assert


我们定义了StaticAssertion结构,该结构将布尔值作为模板参数-将存在我们的条件,如果不满足(表达式为false ),则在编译非专用模板时会发生错误。 另一个用于接收sizeof( StaticAssertion )的虚拟结构。

 namespace stdex { namespace detail { template <bool> struct StaticAssertion; template <> struct StaticAssertion<true> { }; // StaticAssertion<true> template<int i> struct StaticAssertionTest { }; // StaticAssertionTest<int> } } 

以及进一步的宏观魔术

 #ifdef _STDEX_NATIVE_CPP11_SUPPORT #define STATIC_ASSERT(expression, message) static_assert((expression), #message) #else // no C++11 support #define CONCATENATE(arg1, arg2) CONCATENATE1(arg1, arg2) #define CONCATENATE1(arg1, arg2) CONCATENATE2(arg1, arg2) #define CONCATENATE2(arg1, arg2) arg1##arg2 #define STATIC_ASSERT(expression, message)\ struct CONCATENATE(__static_assertion_at_line_, __LINE__)\ {\ stdex::detail::StaticAssertion<static_cast<bool>((expression))> CONCATENATE(CONCATENATE(CONCATENATE(STATIC_ASSERTION_FAILED_AT_LINE_, __LINE__), _WITH__), message);\ };\ typedef stdex::detail::StaticAssertionTest<sizeof(CONCATENATE(__static_assertion_at_line_, __LINE__))> CONCATENATE(__static_assertion_test_at_line_, __LINE__) #ifndef _STDEX_NATIVE_NULLPTR_SUPPORT #define static_assert(expression, message) STATIC_ASSERT(expression, ERROR_MESSAGE_STRING) #endif #endif 

用法:

 STATIC_ASSERT(sizeof(void*) == 4, non_x32_platform_is_unsupported); 

我的实现与标准实现之间的重要区别是,在告知用户的情况下,不会重载此关键字。 这是由于以下事实:在C ++中,不可能定义多个具有不同数量的参数但一个名称的定义,并且没有消息的实现比所选选项的用处少得多。 此功能导致以下事实:从本质上讲 ,我的实现中的STATIC_ASSERT是已经在C ++ 11中添加的版本。
让我们看看发生了什么。 通过检查__cplusplus和非标准的编译器宏的版本,我们获得了有关_STDEX_NATIVE_CPP11_SUPPORT define表示的有关C ++ 11支持(以及因此的static_assert )的足够信息。 因此,如果定义了此宏,我们可以简单地使用标准的static_assert

 #ifdef _STDEX_NATIVE_CPP11_SUPPORT #define STATIC_ASSERT(expression, message) static_assert((expression), #message) 

请注意, STATIC_ASSERT宏的第二个参数根本不是字符串文字,因此使用预处理程序运算符我们将把message参数转换为字符串,以传输到标准static_assert
如果我们没有编译器的支持,则继续执行。 首先,我们将声明用于“粘合”字符串的辅助宏(预处理程序运算符##对此负责)。

 #define CONCATENATE(arg1, arg2) CONCATENATE1(arg1, arg2) #define CONCATENATE1(arg1, arg2) CONCATENATE2(arg1, arg2) #define CONCATENATE2(arg1, arg2) arg1##arg2 

为了能够将同一CONCATENATE宏的结果作为参数传递给arg1arg2 ,我特别不是简单地使用#define CONCATENATE( arg1arg2 arg1 ## arg2
接下来,我们以漂亮的名称__static_assertion_at_line_ {行号}声明一个结构(宏__LINE__也由标准定义,并且必须扩展为调用它的行号),然后在该结构内添加一个类型为StaticAssertion的字段,名称为STATIC_ASSERTION_FAILED_AT_LINE_ {行号} _WITH来自调用宏的错误消息}。

 #define STATIC_ASSERT(expression, message)\ struct CONCATENATE(__static_assertion_at_line_, __LINE__)\ {\ stdex::detail::StaticAssertion<static_cast<bool>((expression))> CONCATENATE(CONCATENATE(CONCATENATE(STATIC_ASSERTION_FAILED_AT_LINE_, __LINE__), _WITH__), message);\ };\ typedef stdex::detail::StaticAssertionTest<sizeof(CONCATENATE(__static_assertion_at_line_, __LINE__))> CONCATENATE(__static_assertion_test_at_line_, __LINE__) 

使用StaticAssertion中的 template参数我们传递一个在STATIC_ASSERT中检查的表达式,将其转换为bool 。 最后,为了避免创建局部变量和零开销检查用户条件,声明了类型为StaticAssertionTest <sizeof({上面声明的结构的名称})的别名,名称为__static_assertion_test_at_line_ {行号}。

命名的所有美感仅是为了从编译错误中清楚地表明这是一个肯定的结果,而不仅是一个错误,而且还显示为此断言设置的错误消息。 sizeof技巧是强制编译器实例化StaticAssertion模板类的必要条件,该模板类位于刚刚声明的结构内部,从而检查传递给assert的条件。

STATIC_ASSERT结果
GCC:
30:103:错误:字段'STATIC_ASSERTION_FAILED_AT_LINE_36_WITH__non_x32_platform_is_unsupported'具有不完整的类型'stdex :: detail :: StaticAssertion <false>'
25:36:注意:在宏“ CONCATENATE2”的定义中
23:36:注意:扩展宏“ CONCATENATE1”
30:67:注意:扩展宏“ CONCATENATE”
24:36:注意:扩展宏“ CONCATENATE2”
23:36:注意:扩展宏“ CONCATENATE1”
30:79:注意:扩展宏“ CONCATENATE”
24:36:注意:扩展宏“ CONCATENATE2”
23:36:注意:扩展宏“ CONCATENATE1”
30:91:注意:在宏“ CONCATENATE”的扩展中
36:3:注意:在宏“ STATIC_ASSERT”的扩展中

Borland C ++ Builder:
[C ++错误] stdex_test.cpp(36):E2450未定义结构'stdex :: detail :: StaticAssertion <0>'
[C ++错误] stdex_test.cpp(36):E2449“ STATIC_ASSERTION_FAILED_AT_LINE_36_WITH__non_x32_platform_is_unsupported”的大小未知或为零
[C ++错误] stdex_test.cpp(36):E2450未定义结构'stdex :: detail :: StaticAssertion <0>'

Visual Studio:
错误c2079


我想拥有的第二个“技巧”是countof-计算数组中元素的数量。 Sishers非常喜欢通过sizeof(arr)/ sizeof(arr [0])声明此宏,但是我们将继续。


 #ifdef _STDEX_NATIVE_CPP11_SUPPORT #include <cstddef> namespace stdex { namespace detail { template <class T, std::size_t N> constexpr std::size_t _my_countof(T const (&)[N]) noexcept { return N; } } // namespace detail } #define countof(arr) stdex::detail::_my_countof(arr) #else //no C++11 support #ifdef _STDEX_NATIVE_MICROSOFT_COMPILER_EXTENSIONS_SUPPORT // Visual C++ fallback #include <stdlib.h> #define countof(arr) _countof(arr) #elif defined(_STDEX_NATIVE_CPP_98_SUPPORT)// C++ 98 trick #include <cstddef> template <typename T, std::size_t N> char(&COUNTOF_REQUIRES_ARRAY_ARGUMENT(T(&)[N]))[N]; #define countof(x) sizeof(COUNTOF_REQUIRES_ARRAY_ARGUMENT(x)) #else #define countof(arr) sizeof(arr) / sizeof(arr[0]) #endif 

对于支持constexpr的编译器, 我们将声明此模板的constexpr版本(对于所有标准而言,绝对不需要通过COUNTOF_REQUIRES_ARRAY_ARGUMENT模板实现),其余的则通过模板函数COUNTOF_REQUIRES_ARRAY_ARGUMENT引入 。 Visual Studio在这里再次以stdlib.h头文件中存在_countof自己的实现而著称

COUNTOF_REQUIRES_ARRAY_ARGUMENT函数看起来很吓人, 弄清楚它的作用非常棘手。 如果仔细观察,您会理解它采用模板类型T和大小N的元素数组作为唯一参数-因此,在传输其他类型的元素(不是数组)的情况下,我们会遇到编译错误,这无疑是令人高兴的。 仔细观察一下,您可以发现(很难)返回了大小为Nchar元素数组 问题是,为什么我们需要所有这些? 这是sizeof运算符发挥作用的地方 ,它在编译时具有独特的工作能力。 调用sizeof( COUNTOF_REQUIRES_ARRAY_ARGUMENT 确定函数返回的char元素数组的大小,并且由于标准sizeof(char) == 1,因此这是原始数组中N个元素的数量。 优雅,美丽,完全免费。

永远


永远需要另一个无限循环的小辅助宏,这是永远的 。 定义如下:

 #if !defined(forever) #define forever for(;;) #else #define STRINGIZE_HELPER(x) #x #define STRINGIZE(x) STRINGIZE_HELPER(x) #define WARNING(desc) message(__FILE__ "(" STRINGIZE(__LINE__) ") : warning: " desc) #pragma WARNING("stdex library - macro 'forever' was previously defined by user; ignoring stdex macro definition") #undef STRINGIZE_HELPER #undef STRINGIZE #undef WARNING #endif 

定义显式无限循环的示例语法:

  unsigned int i = 0; forever { ++i; } 

该宏仅用于显式定义无限循环,并且仅出于“添加语法糖”的原因而包含在库中。 将来,我建议可以通过定义FOREVER插件宏来替换它。 在上述库代码片段中,最引人注目的是如果用户已经定义了forever宏,则同一WARNING宏会在所有编译器中生成警告消息。 它使用熟悉的标准__LINE__宏和标准__FILE__宏 ,该将转换为具有当前源文件名称的字符串。

stdex_assert


为了在运行时实现断言 ,将宏stdex_assert引入为:

 #if defined(assert) #ifndef NDEBUG #include <iostream> #define stdex_assert(condition, message) \ do { \ if (! (condition)) { \ std::cerr << "Assertion `" #condition "` failed in " << __FILE__ \ << " line " << __LINE__ << ": " << message << std::endl; \ std::terminate(); \ } \ } while (false) #else #define stdex_assert(condition, message) ((void)0) #endif #endif 

我不会说我为这个实现感到非常自豪(将来会更改),但是在这里使用了一种有趣的技术,我想引起大家的注意。 为了从应用程序代码的范围中隐藏检查,使用了do {} while(false)构造,该构造将被执行,这很明显,并且一次不会在通用应用程序代码中引入“服务”代码。 该技术非常有用,并在库中的其他多个地方使用。

否则,实现与标准断言非常相似-使用某个NDEBUG宏(编译器通常在发行版本中设置该宏),断言什么都不做,否则如果不满足断言条件,则会中断程序的执行并将消息输出到标准错误流。

没有


对于不引发异常的函数,新标准中引入了noexcept关键字。 通过宏实现也是非常简单和轻松的:

 #ifdef _STDEX_NATIVE_CPP11_SUPPORT #define stdex_noexcept noexcept #else #define stdex_noexcept throw() #endif 

但是,有必要了解,按标准, noexcept可以采用值bool ,并且还可以用于确定在编译时传递给它的表达式不会引发异常。 如果没有编译器支持,则无法实现此功能,因此该库中只有“精简”的stdex_noexcept

第二章结束。 第三章将讨论nullptr实现的复杂性,为什么不同的编译器会有所不同,type_traits的开发以及在开发过程中遇到的编译器中的其他错误。

谢谢您的关注。

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


All Articles