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

в 11:37, , рубрики: boost, c++, c++03, c++11, c++98, loki, sfinae, велосипедостроение, грязные хаки, Компиляторы, кроссплатформенная разработка, макросы, ненормальное программирование, Разработка под Linux, разработка под windows, Совершенный код, стандартная библиотека

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

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

Из-за ограничений на возможность использовать компиляторы 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.

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

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

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

Вся красота с именованиями нужна лишь для того чтобы из ошибки компиляции было понятно что это результат assert, а не просто ошибка, а так же чтобы вывести сообщение об ошибке, которое было заданно для этого assert. Трюк с sizeof необходим чтобы заставить компилятор инстанцировать шаблонный класс StaticAssertion, находящийся внутри только что объявленной структуры, и таким образом проверить переданное в 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:usersuserdocumentsvisual studio 2015projectsstdex_teststdex_teststdex_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.

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

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 и на какие еще баги в компиляторах я наткнулся при ее разработке.

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

Автор: oktonion

Источник

Поделиться

* - обязательные к заполнению поля