Главная » Хабрахабр » Как я стандартную библиотеку C++11 писал или почему boost такой страшный. Глава 2

Как я стандартную библиотеку C++11 писал или почему boost такой страшный. Глава 2

Да - да, вот с этим девизом я и ринулся в бой.

Краткое содержание предыдущих частей

Из-за ограничений на возможность использовать компиляторы C++ 11 и от безальтернативности boost'у возникло желание написать свою реализацию стандартной библиотеки C++ 11 поверх поставляемой с компилятором библиотеки C++ 98 / C++ 03.

Помимо стандартных заголовочных файлов type_traits, thread, mutex, chrono так же были добавлены nullptr.h реализующий std::nullptr_t и core.h куда были вынесены макросы, относящиеся к компиляторозависимому функционалу, а так же расширяющие стандартную библиотеку.

Ссылка на GitHub с результатом на сегодня для нетерпеливых и нечитателей:

Коммиты и конструктивная критика приветствуются

Оглавление

Введение
Глава 1. Viam supervadet vadens
Глава 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif
Глава 3.

Глава 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif

После того как весь код был немного причесан и разделен по «стандартным» заголовкам в отдельный namespace stdex я приступил к наполнению type_traits, nullptr.h и попутно того самого core.h, в котором содержались макросы для определения версии стандарта, используемого компилятором и поддержки им «нативных» nullptr, char16_t, char32_t и static_assert.

14. В теории все просто — согласно стандарту C++ (п. 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

image На деле не все так просто и теперь начинаются интересные костыли с граблями.

К примеру в Visual Studio 2013 отсутствовал constexpr очень долгое время, при этом утверждалось что C++11 она поддерживает — с оговорочкой, что реализация не полная. Во-первых не все, а точнее сказать ни один, из компиляторов не реализуют очередной стандарт полностью и сразу. Во-вторых не все компиляторы (и это удивляет еще больше) верно выставляют данный define и своевременно его обновляют. То есть auto — пожалуйста, static_assert — так же легко (еще с более ранних MS VS), а вот constexpr — нет. И ситуация еще усугубляется тем, что по стандарту компиляторам разрешено выставлять данный define в отличные от приведенных выше значений, если они не до конца соответствуют заявленным стандартам. Неожиданно в том же самом компиляторе Visual Studio не изменяли версию дефайна __cplusplus аж с самых первых версий компилятора, хотя давно уже заявлена полная поддержка C++ 11 (что тоже не правда, за что им отдельные лучи недовольства — как только разговор заходит о конкретной функциональности «нового» 11 стандарта разработчики сразу же говорят что нет C99 preprocessor, еще других «фич»). Логично было бы предположить к примеру такое развитие дефайнов для данного макроса (с вводом нового функционала увеличивать и число, скрывающееся за данным define):

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++ и в каком объеме он поддерживает. Так как в моем арсенале поддерживаемых компиляторов есть еще и C++ Borland Builder 6. Во-первых теперь мы добавляем проверку версии для Visual Studio через уникальный для этого компилятора макрос _MSC_VER. Для clang-совместимых компиляторов имеется нестандартный макрос __has_feature(feature_name), который позволяет узнать о наличии поддержки компилятором той или иной функциональности. 0, разработчики которого в свою очередь очень стремились сохранить совместимость с Visual Studio (в том числе и с ее «особенностями» и багами), то там тоже внезапно есть данный макрос. В итоге код раздувается до:

#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 уже реализована поддержка nullptr с версии компилятора _MSC_VER 1600, так же как и встроенных типов char16_t и char32_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, так как для компиляторов без нее не будет существовать некоторых заголовочных файлов стандартной библиотеки, а проверить отсуствие оных мы никак не можем средствами компилятора.

Полный вариант

#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 в которых множество трудолюбивых разработчиков выписывали все эти компиляторозависимые макросы и из них составляли карту того, что поддерживается, а что нет конкретным компилятором конкретной версии, от которых мне лично становится не по себе, хочется никогда на это не смотреть и не трогать больше. Но хорошая новость в том, что на этом можно остановиться. По крайней мере мне этого достаточно для поддержки большинства популярных компиляторов, но если вы нашли неточность или хотите добавить еще один компилятор — я буду только рад принять pull request.

О недостатках данного подхода мы поговорим чуть позже. Большим достижением по сравнению с boost я считаю то, что удалось сдержать расползание компиляторозависимых макросов по коду, что делает код чище и проще для понимания, а так же не нагромождать десятки конфигурационных файлов для каждой из ОС и для каждого компилятора.

На данном этапе мы уже можем начать подключать недостающую функциональность из 11 стандарта, и первое что мы введем это static_assert.

static_assert

Определим структуру StaticAssertion, которая будет принимать шаблонным параметром булевское значение — там будет наше условие, при невыполнении которого (выражение приводится к false) произойдет ошибка компиляции неспециализированного шаблона. И еще одну структуру-пустышку для приема sizeof(StaticAssertion).

namespace stdex
; // 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++ 17.

Разберемся по порядку что же произошло. В результате проверок версий __cplusplus и нестандартных макросов компиляторов мы имеем информацию о поддержке C++ 11 в достаточном нам объеме (а значит и static_assert), выраженную дефайном _STDEX_NATIVE_CPP11_SUPPORT. Следовательно если этот макрос определен мы можем просто использовать стандартный static_assert:

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

Обратите внимание что второй параметр макроса STATIC_ASSERT совсем не string literal и потому с помощью оператора препроцессора # мы преобразуем параметр message в строку для передачи в стандартный static_assert.

Если же поддержки от компилятора у нас нет, то переходим к своей реализации. Для начала объявим вспомогательные макросы для «склеивания» строк (оператор препроцессора ## как раз отвечает за это).

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

Я специально не использовал просто #define CONCATENATE(arg1, arg2 ) arg1##arg2 для того чтобы иметь возможность передавать внутрь макроса как параметр arg1 и arg2 результат того же самого макроса CONCATENATE.

Далее объявляем структуру с красивым именем __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 передадим выражение, которое проверяется в STATIC_ASSERT, приведя его к bool. И в завершение для того чтобы избежать создания локальных переменных и осуществить zero-overhead проверку пользовательского условия объявляется псевдоним для типа StaticAssertionTest<sizeof({имя объявленной выше структуры}) с именем __static_assertion_test_at_line_{№ строки}.

Трюк с sizeof необходим чтобы заставить компилятор инстанцировать шаблонный класс StaticAssertion, находящийся внутри только что объявленной структуры, и таким образом проверить переданное в assert условие. Вся красота с именованиями нужна лишь для того чтобы из ошибки компиляции было понятно что это результат assert, а не просто ошибка, а так же чтобы вывести сообщение об ошибке, которое было заданно для этого assert.

Результаты выдачи STATIC_ASSERT

GCC:
30:103: error: field 'STATIC_ASSERTION_FAILED_AT_LINE_36_WITH__non_x32_platform_is_unsupported' has incomplete type 'stdex::detail::StaticAssertion<false>'
25:36: note: in definition of macro 'CONCATENATE2'
23:36: note: in expansion of macro 'CONCATENATE1'
30:67: note: in expansion of macro 'CONCATENATE'
24:36: note: in expansion of macro 'CONCATENATE2'
23:36: note: in expansion of macro 'CONCATENATE1'
30:79: note: in expansion of macro 'CONCATENATE'
24:36: note: in expansion of macro 'CONCATENATE2'
23:36: note: in expansion of macro 'CONCATENATE1'
30:91: note: in expansion of macro 'CONCATENATE'
36:3: note: in expansion of macro 'STATIC_ASSERT'

Borland C++ Builder:
[C++ Error] stdex_test.cpp(36): E2450 Undefined structure 'stdex::detail::StaticAssertion<0>'
[C++ Error] stdex_test.cpp(36): E2449 Size of 'STATIC_ASSERTION_FAILED_AT_LINE_36_WITH__non_x32_platform_is_unsupported' is unknown or zero
[C++ Error] stdex_test.cpp(36): E2450 Undefined structure 'stdex::detail::StaticAssertion<0>'

Visual Studio:
Error C2079 'main::__static_assertion_at_line_36::STATIC_ASSERTION_FAILED_AT_LINE_36_WITH__non_x32_platform_is_unsupported' uses undefined struct 'stdex::detail::StaticAssertion<__formal>' stdex_test c:\users\user\documents\visual studio 2015\projects\stdex_test\stdex_test\stdex_test.cpp 36

Вторая «фишка», которую хотелось иметь, при этом отсуствующая в стандарте это countof — подсчет количества элементов в массиве. Сишники очень любят данный макрос объявлять через sizeof(arr) / sizeof(arr[0]), но мы пойдем дальше.

countof

#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 здесь снова отличилась наличием собственной реализации _countof в заголовочном файле stdlib.h.

Если присмотреться, то можно понять что она принимает на вход единственным аргументом массив элементов шаблонного типа T и размера N — таким образом в случае передачи других типов элементов (не массивов) мы получим ошибку компиляции, что несомненно радует. Функция COUNTOF_REQUIRES_ARRAY_ARGUMENT выглядит устрашающе и разобраться в том что она делает довольно непросто. Спрашивается зачем нам это все? Присмотревшись еще внимательней можно разобраться (с трудом) что возвращает она массив элементов char размера N. Вызов sizeof(COUNTOF_REQUIRES_ARRAY_ARGUMENT) определяет размер возвращаемого функцией массива элементов char, а так как по стандарту sizeof(char) == 1, то это и есть количество элементов N в исходном массиве. Здесь вступает в дело оператор sizeof и его уникальные возможности работать во время компиляции. Изящно, красиво, и совершенно бесплатно.

forever

Еще один небольшой вспомогательный макрос, который я использую везде где необходим бесконечный цикл это forever. Определен он так:

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

Данный макрос используется исключительно для явного определения бесконечного цикла и включен в библиотеку только из соображений «добавить синтаксического сахара». В дальнейшем предполагаю его заменить на опционально через define подключаемый макрос FOREVER. Что же примечательно в вышеприведенном отрывке кода из библиотеки, так это тот самый макрос WARNING, который генерирует сообщение-предупреждение во всех компиляторах если макрос forever уже был определен пользователем. Он использует уже знакомый стандартный макрос __LINE__ и так же стандартный __FILE__, который преобразуется в строку с именем текущего исходного файла.

stdex_assert

Для реализации 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), которая выполнится, что очевидно, один раз и при этом не внесет «служебного» кода в общий код приложения. Данный прием довольно полезен и применяется еще в нескольких местах библиотеки.

В остальном же реализация очень похожа на стандартный assert — при определенном макросе NDEBUG, который обычно выставляют компиляторы в релизных сборках, assert ничего не делает, в противном случае прерывает выполнение программы с выводом сообщения в стандартный поток ошибок при несоблюдении условия assert'а.

noexcept

Для функций, которые не бросают исключений, в новом стандарте было введено ключевое слово noexcept. Его тоже довольно просто и безболезненно можно реализовать через макрос:

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

однако необходимо понимать что по стандарту noexcept может принимать значение bool, а так же использоваться для определения во время компиляции что переданное ему выражение не бросает исключений. Данный функционал не может быть реализован без поддержки компилятора, и потому в библиотеке есть только «урезанный» stdex_noexcept.

В третьей главе будет рассказано о тонкостях реализации nullptr, о том почему он разный для разных компиляторов, а так же как продвигалась разработка type_traits и на какие еще баги в компиляторах я наткнулся при ее разработке. Конец второй главы.

Благодарю за внимание.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

В школах Кировской области заработала Фабрика программистов

Мы запустили проект по бесплатному обучению школьников основам современной веб-разработки в стеке Node.js / React. Пока проект работает в пилотном режиме в нескольких школах Кировской области, но мы принимаем заявки на подключение из других регионов – https://coderfactory.ru. Предыстория Все началось ...

Китай подтверждает лидерство в азиатской лунной гонке

В нулевых годах в Азии началась вторая «лунная гонка». В отличие от первой, когда в 1960-х соревновались СССР и США, стран-участников оказалось больше, а вот бюджеты меньше, и общие сроки дольше. На старте было три участника — Индия, Китай, Япония. ...