[Из песочницы] Как я стандартную библиотеку C++11 писал или почему boost такой страшный

Habrahabr 3

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

Вместо предисловия

Пожалуй с этой картинки должно начинаться любое повествование о boost, Loki, самостоятельных, да и так же поставляемых с компиляторами реализациях стандартной библиотеки C++.

Да-да, и если вы думали что разработчики стандартной библиотеки для того же g++, clang, Visual Studio или, прости господи, C++ Builder (бывший Borland, а нынешний Embarcadero) — гуру, что не городят костылей, не ломают стандарт под свой компилятор и не пишут велосипедов, то, скорее всего, вы не так активно используете стандартную библиотеку C++ как вам казалось.

Статья написана как рассказ, и содержит много «воды» и отступлений, но я надеюсь, что мой опыт и получившийся код будет полезен тем, кто столкнулся с похожими проблемами при разработке на C++, особенно на старых компиляторах. Ссылка на GitHub с результатом на сегодня для нетерпеливых и нечитателей:

https://github.com/oktonion/stdex (коммиты и конструктивная критика приветствуются)
А теперь, обо всем по порядку.

Оглавление

Введение Глава 1. Viam supervadet vadens Глава 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif Глава 3. Поиск идеальной реализации nullptr Глава 4. …

Вступление

На дворе был 2017 год, C++ 11 уже давно ворвался свежим потоком во все новые и относительно новые компиляторы, принеся стандартизированную работу с потоками, мьютексами, расширил шаблонное программирование и стандартизировал подходы к нему, появились «большие» типы long long в стандарте, наконец то избавились от повсеместной необходимости выводить за компилятора типы с помощью auto (прощай std::map<type, type>::const_iterator it = ... — ну вы меня понимаете), а связка этой возможности с новым for each стала одной из самых часто используемых реализаций циклов по итераторам. Наконец то мы (разработчики) получили возможность по-человечески сообщить пользователю (разработчику) почему же код не собирается, используя static_assert, а так же enable_if, что выбирал нужные перегрузки теперь как по волшебству.

На дворе был 2017 год! Уже C++ 17 активно вводился в GCC, clang, Visual Studio, везде был decltype (since C++ 11), constexpr (since C++ 11, но существенно доработан), модули уже почти на подходе, хорошее время было. Я же находился на работе и с некоторым неодобрением смотрел на очередной Internal Compiler Error в своем Borland C++ Builder 6.0, а так же на множество ошибок сборки с очередной версией библиотеки boost. Думаю, теперь вы понимаете, откуда взялась эта тяга к велосипедостроению. У нас использовался Borland C++ Builder 6.0 и Visual Studio 2010 под Windows, g++ версии 4.4.2 или ниже под QNX и под некоторые unix системы. От MacOS мы были избавлены, что несомненно было плюсом. Ни о каких других компиляторах (под C++ 11 в том числе) речи даже быть не могло по соображениям, которые мы оставим за пределами данной статьи.

«А что там может быть на столько сложного» — закралась мысль в мой измученный попытками завести boost под старый-добрый builder мозг. «Мне всего то нужно type_traits, thread, mutex, возможно chrono, nullptr было бы еще неплохо.» — рассудил я и принялся за работу.

Глава 1. Viam supervadet vadens

Необходимо было с чего то начать, и начать было с чего — естественно у меня имелось некоторое количество разбросанных по проектам заголовочных файлов и исходников с реализациями похожего или идентичного функционала из стандартной библиотеки C++ 11 моей разработки, а так же честно позаимствованные или переработанные из кодов того же gcc и boost. Объединив все это воедино я получил некоторую кашу из функций, классов, макросов которая должна была превратиться в изящную и стройную стандартную библиотеку. Оценив объем работы я сразу решил отказаться от реализации всего и вся, ограничившись разработкой «надстройки» над поставляемой с компилятором стандартной библиотекой C++ 98.

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

 #define nullptr 0
        
static_assert решался тоже просто:
 #define STATIC_ASSERT(expr) typedef int test##__LINE__##[expr ? 1 : -1];
        
std::to_string был реализован через std::stringstream, который подменялся на std::strstream в реализациях без заголовочного файла sstream, причем все это пихалось сразу в namespace std:
        #ifndef NO_STD_SSTREAM_HEADER
        #include <sstream>
        #else
        #include <strstream>
        namespace std {typedef std::strstream stringstream;}
        #endif
        namespace std
        {
                template<class T>
                string to_string(const T &t)
                {
                        stringstream ss;
                        ss << t;
                        return ss.str();
                }
        }
        
Так же были и «трюки» не вошедшие в стандарт, но тем не менее полезные в повседневной работе, такие как макросы forever или countof:
 #define forever for(;;) // бесконечный цикл объявленный явно
        #define countof(arr) sizeof(arr) / sizeof(arr[0]) // подсчет количества элементов в массиве в стиле C
        
countof затем трансформировался в более C++ вариант:
         template <typename T, std::size_t N>
                char(&COUNTOF_REQUIRES_ARRAY_ARGUMENT(T(&)[N]))[N];
                
                // подсчет количества элементов в массиве в стиле C++ (с проверкой аргумента на то что он массив):      
                #define countof(x) sizeof(COUNTOF_REQUIRES_ARRAY_ARGUMENT(x))
        
Работа с потоками (заголовочный файл thread из std) была реализована через какую то из Tiny библиотек, переписанную с учетом особенностей всего зоопарка компиляторов и ОС. И разве что type_traits в какой то мере был уже похож на то, что требовал стандарт C++ 11. Был std::enable_if, std::integral_constant, std::is_const и тому подобные шаблоны, которые уже применялись в разработке.
 namespace std
        {
                template<bool Cond, class Iftrue, class Iffalse>
                struct conditional
                {
                        typedef Iftrue type;
                };
        
                // Partial specialization for false.
                template<class Iftrue, class Iffalse>
                struct conditional<false, Iftrue, Iffalse>
                {
                        typedef Iffalse type;
                };

                template <bool, class T = void>
                struct enable_if
                { };
        
                template <class T>
                struct enable_if<true, T>
                {
                        typedef T type;
                };

                template<class Tp, Tp Val>
                struct integral_constant
                {       // convenient template for integral constant types
                        static const Tp value = Val;
        
                        typedef const Tp value_type;
                        typedef integral_constant<Tp, Val> type;
                };
        
                typedef integral_constant<bool, true> true_type;
                typedef integral_constant<bool, false> false_type;
        
                template<bool Val>
                struct bool_constant :
                        public integral_constant<bool, Val>
                { };

                template<class, class>
                struct is_same :
                        public false_type
                { };
        
                template<class Tp>
                struct is_same<Tp, Tp> :
                        public true_type // specialization
                { };
        }


        // ... и еще несколько шаблонов
        
Было принято решение выделить все нестандартные и «компиляторные» макросы, функции, типы в отдельный заголовочный файл core.h. И, вопреки практике boost, где повсеместно используются «переключения» реализаций с помощью макросов, максимально отказаться от макросов связанных с компиляторозависимыми вещами во всех файлах библиотеки, кроме core.h. Так же тот функционал, что не поддается реализации без использования «хаков» (нарушения стандарта, полагаясь что Undefined Behaviour будет Somewhat Defined), или реализуется для каждого компилятора индивидуально (через его build-in макросы к примеру) было решено не добавлять в библиотеку, дабы не наплодить еще один монструозный (но прекрасный) boost. В итоге главное и практически единственное для чего используется core.h так это для определения наличия поддержки встроенного nullptr (т.к. компиляторы ругаются если переопределять зарезервированные слова), поддержки встроенного static_assert (опять же для избежания пересечения зарезервированного слова) и поддержки встроенных типов C++ 11 char16_t и char32_t.

Забегая вперед могу сказать что идея почти удалась, т.к. большинство из того что в boost определяется в зависимости от конкретного компилятора жесткими макросами, в данной реализации определяется самим компилятором на этапе компиляции.

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

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