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

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

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


Пожалуй с этой картинки должно начинаться любое повествование о 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. Шаблонная «магия» C++
....4.1 Начинаем с малого
Глава 5.


Вступление


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

Благодарю за внимание.
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 32
    0
    ну под QNX 6-то был 4.8.2, там C++11 был, но не в полном объеме.
    хотя мне первоначально приходилось тащить совместимость кодовой базы из-под четверки с Watcom С++ 11.
    stdatomic пришлось наследовать и переопределять, т.к. в оригинале в методах были не-volatile переменные на стеке, что вводило шестерку при вызове метода из обработчика прерывания в полное изумление.
      0
      Во первых хотел бы выразить благодарность за комментарии и интерес к статье, это чертовски приятно, как оказалось.

      Если по теме: у нас и QNX 6 и QNX 4.25. Причем «одобренного» 4.8.2 под 6-ку нет. Хотя даже если бы был, то я думаю все равно полез бы в эти дебри, потому как в первую очередь есть еще Borland C++ Builder, а так же ведь очень увлекательно это все. Пожалуй увлекательно — в первую очередь даже.

      А с QNX постоянно у всех какие то приключения, вы не первый разработчик от кого узнаю про веселые франкенштейны g++ с Watcom под эту платформу. На atomic пока что не замахивался, но похоже он пишется именно под каждую ОС \компилятор свой уникальный, и боюсь здесь никак это не реализовать не скатываясь в постоянные ifdef компилятор_1 elseif…
        0
        а что значит «одобренного нет»? через Foundry27 он вполне себе раздавался, сейчас уже не могу посмотреть, т.к. год как в том месте не работаю.
        с атомарными операциями в четверке как раз проще все было, там int был атомарный. но ради совместимости API приходилось это прятать под четверкой в макросы, а под шестеркой в шаблоны функций.
        это не считая того, что один из разработчиков говорил «классы зло, хочу C-style», другой «а где мое ООП?», и мне в API приходилось удовлетворять их обоих.
          0
          «Одобренного» значит что у меня был определенный дистрибутив, с определенным компилятором, и другим собирать было «ни-ни». Сейчас да, уже переползаем на что-то поновее. =)

          У вас закрытый проект? Интересно было бы посмотреть на самом деле.
            0
            проект закрытый, к тому же мне пришлось его покинуть по естественным причинам. но было много публикаций в открытых источниках. гуглить по ключевым словам «УВК АДВ», «ЦСПА ОЭС Сибири»
      0
      И по поводу продолжения сразу скажу — руки завтра, думаю, дойдут до Главы 2 с кодом уже и посерьезнее материалом. Надеюсь что возможность редактирования сохраняется для статей и можно будет сделать оглавление в каждой.
        0
        Очень интересно, большая работа, респект. Я делал нечто похожее, реализовывал недостающие части стандартной библиотеки С++11 под Arduino AVR. Но мне было в 100 раз проще, там компилятор поддерживал С++11, только библлиотек и не хватало.

        Я не понимаю, как вы могли реализовать С++11 библиотеку без С++11 компилятора. Например, как сделан auto? Я пытался портировать некоторые новые фичи С++17 в свою библиотеку, но быстро понял, что те вещи, которые требуют поддержки именно со стороны компилятора/языка, я реализовать не могу.
          0
          Сомневаюсь, что автор реализовывал auto, decltype и самое главное — move semantics. Для этих вещей нужна поддержка компилятора.
            0
            Как совершенно верно подметили я, естественно, не реализовывал встроенные в язык фичи компилятора типа auto, так как без доступа к исходникам или внешнего кодогенератора сделать такое не реально. Речь идет именно о реализации стандартной библиотеки по максимуму со следующими ограничениями: без надежды на UB, без реализации через нестандартные макросы компилятора (хотя здесь пришлось сделать исключение, об этом расскажу), с минимальной завязкой на конкретные версии компиляторов (только для определения «степени» поддержки C++). В общем из зарезервированных слов и «языковых» фич компилятора возможно сделать static_assert да nullptr через макросы, и они подключаются в core.h.

            PS: спасибо за добрые слова, и с поддержкой 11 стандарта конечно реализация была бы полной. Очень не хватает decltype в основном для завершения type_traits.
              0
              (оффтоп)
              Никогда не понимал, зачем было сначала (придумывая c++ из c) портить NULL и заодно вводить опасное неявное int->pointer преобразование без варнингов для испорченного NULL'а, а потом придумывать новое слово (nullptr) для обозначения того, что раньше обозначалось как NULL.
                0
                Дык NULL же «улучшили»! Почти во всех реализациях NULL — это "((void*)0)". Но не во всех. И без заголовочных файлов этого и не выяснить.

                Вот Страуструп решил, что обучить компилятор превращать число 0 в NULL будет удобно. Не нужно никаких хедеров, компилятор всё и так знает.

                Несколько лет в начале пытались поддерживать то разделение, которое де-факто имеется в GCC, к примеру: стандартная библиотека отдельно, компилятор — отдельно.

                Но потом оказалось, что отделать стандартную библиотеку C++ от компилятора всё сложнее и сложнее (так что сейчас, де-факто, каждый компилятор работать только со «своей» библиотекой, с некоторыми исключениями, когда компилятор «подвешивается» на чужую библиотеку как ICC) — а вот зато от того, что у NULL'а тип int всем было реально плохо… Так что собрались — и исправили…

                P.S. Забавно тут другое: в C уже был подобный прецедент по подбным же историческим причинам. 'a' имеет тип int. Соотвественно sizeof('a') будет на большинстве современных платформ 4. C++ — это исправил… и тут же породил путаницу с NULL'ом… Может Страуструп был большим поклонником Ломоносова?
                  0
                  Так ведь в C++ запретили implicit cast из void* к любому указателю, отсюда и проблемы пошли с Сишной имплементацией NULL как ((void*)(0)), а так как хотели библиотеку std отвязать от компилятора по максимуму и совместимость с Си сохранить, то вот он новый NULL как 0 — рыцарь в сияющих доспехах. Ну а раз 0 это int, то здравствуй неявное преобразование 0->pointer (заметим что не int->pointer все же).
                    0
                    Ну так сделали ж 0->pointer, могли бы вместо него сделать ((void*)0) -> pointer и зафиксировать в стандарте что NULL = ((void*)0), а остальное не трогать.
                      0
                      Черт его знает, если честно. Может создателям компиляторов на тот момент это было не удобно так реализовывать. А может еще что знал товарищ Страуструп, чего мы не знаем.
                        0
                        Не успел комментарий отредактировать. Есть еще один тонкий момент, ведь как заметил khim, не во всех реализациях NULL был ((void*)(0)). Потому всеравно нужно было что то свое городить.
                0

                Реализация собственной стандартной библиотеки это и правда тяжёлая и весьма скорпулезная работа, требующая глубокого понимания стандарта. Но позвольте задать вопрос несколько выходящий за рамки текста и пересекающийся с одним из комментариев. Скажем, есть компилятор поддерживающий последний стандарт, но стоит он на системе с отсутствием стандартной библиотеки для этого стандарта (свой компилятор на старом линухе, например) — есть ли здесь решения кроме как написание своей библиотеки?

                  0
                  Все зависит от того как реализован компилятор. К примеру тот же GCC имеет добротную открытую стандартную библиотеку, и в основном что требуется от компилятора так это поддерживать все их специфичные built-in'ы. Так же важно понимать на каком железе работает данный компилятор, к примеру вопрос атомарности операций в разных архитектурах будет решаться по разному и здесь нужно учитывать есть ли GCC именно под вашу если заимствовать стандартную библиотеку из него.
                  А так по сути вариантов немного, либо свою, либо часть заимствовать, но близкие к компилятору вещи писать опять свои.
                    +1
                    Можно воспользоваться чужой работой. Вот тут есть интересная штука: GCC 7 со стандартной библиотекой реализованной как «аддон» к библиотеке GCC 4.4+.

                    То есть то, что в GCC 4.4 если в разделяемой библиотеке — используется оттуда, то, чего не хватает — поступает из статической библиотеки. Как результат — можно использовать C++17 и при этом собирать бинарники, которые на каком-нибудь RHEL 6, выпущенном 7 лет назад, запустятся.

                    Конечно чтобы прикрутить это к вашему дистрибутиву — потребуется напильник весьма немаленьких размеров, но… дорогу осилит идущий.
                    –14

                    Место С+++ давно на помойке! Только JSONNET.

                      0
                      Настолько просто и понятно о буднях. Нет, чтобы перевернуть землю, и заставить утилиты собирать boost. Никакого чувства романтики.
                        0
                        Мне нравится простое и понятное, и потому смотря на потроха boost я каждый раз грущу от нагромождения макросов и восхищаюсь красотой костылей. А заставить утилиты собирать boost — это разобрать весь boost по косточкам и добавить своих проверок, переписав местами чужой код. При том возможно и не заведется все равно.
                        Вы правы, никакой романтики.
                          0
                          При том возможно и не заведется все равно.
                          Не, не заведётся. Вот тут замечательная история описана: мы пилили-пилилили, пилилили-пилили, 40 багов исправили за 3 года, упарились… но вот вам ещё заплатки на два десятка оставшихся багов, с ними boost::hana кое-как заводится.

                          И тут ведь шла речь о разработчиках компилятора, которые вольны были выбирать — будут они править баг в компиляторе или заплатку в библиотеку вкручивать!

                          А если у вас компилятор старый и кривой? Вообще без шансов…
                            0
                            А вот это уже романтика и увлекательная история. Спасибо =)
                          +1
                          Извините за неудачный комментарий. Я вообще — то на эзоповском языке хотел сказать, что собирать буст намного лучше, чем писать статьи, как это не делать. В моем скромном понимании. Мне очень жаль, что так вот сложился разговор.
                            +1
                            Да все в порядке с комментарием. На мою статью можно смотреть и под углом «автор не осилил сборку буста, потому решил писать велосипед», и это нормально, хотя не совсем так.

                            Другое дело что если складывается впечатление что я призываю не собирать буст, а пользоваться моим, и уж тем более если вдруг кажется что я советую вообще писать свое, то значит что то не так в моей подаче материала. Я могу с уверенностью сказать что:
                            1. Используйте современные компиляторы и среды разработки с современными стандартными библиотеками.
                            2. Если п.1 не доступен, то пишите на своем стандарте C++ и не связывайтесь с boost без необходимости.
                            3. Если п.2 доставляет много боли и хочется все же приключений, то используйте boost.
                            4. Если п.3 актуален, но boost просто не собирается под вашу задачу, то ищите библиотеки где все велосипеды написаны, и грабли обезврежены для вашей задачи.
                            5. Если п.4 не выполнен, то смиритесь и возвращайтесь к п.2. Если же всеравно хочется приключений, или задача требует их, то милости просим в мою статью.

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

                            PS: Это все в последней части в выводах хотел включить, но раз уж развернулась такая дискуссия…
                          +1
                          Интересная штука, надо будет посмотреть. Когда искал что-то похожее находил вот что: github.com/PollardBanknote/cppbackport может кому будет интересно еще.
                            0
                            Полезная ссылка, спасибо.
                            Очень похожая штука, разве что сильно заточена под GCC и его особенности. И, судя по дефайнам, под Windows многое работать не будет на старых компиляторах.
                            +1
                            Вот, кстати, человек занят похожим занятием:
                            github.com/martinmoene?page=1&tab=repositories

                            Тут, правда, реализация (бакпорт) фишек из новых пропозалов…
                              0
                              Надо посмотреть, спасибо.

                              Вот это нагромождение макросов конечно у него в каждой реализации и снова для определения версий компилятора, поддерживаемых «фич» языка и т.п.
                                0
                                Зато реализации можно использовать независимо, а не целым бандлом. Вот, скажем, нужны мне только expected — взял один .h-ник и ни о чём больше не заморачиваюсь. В общем, есть в этом подходе что-то разумное. Особенно если с каким-нибудь folly сравнить, которая для того же expected чёрта лысого тащит.
                                  +1
                                  Да, этот момент мне тоже нравится, у себя в библиотеке тоже старался по максимуму «отвязать» каждый заголовочный файл от других (например не тащить весь type_traits, когда нужен только is_const). Но при реализации конкретных пропозалов это проще, чем при реализации целых стандартных заголовочных файлов конечно.
                                  0
                                  <дубль предыдущего>

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое