Хабрахабр

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

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

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

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

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

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

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

А теперь, обо всем по порядку.

Вступление

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

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

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

Глава 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 #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, а так же описание своих впечатлений от увиденного и приобретенного опыта с примерами кода. Конец первой главы.

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

Показать больше

Похожие публикации

Добавить комментарий

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

Кнопка «Наверх»