Как я стандартную библиотеку C++11 писал или почему boost такой страшный. Глава 4.1
Краткое содержание предыдущих частей
Из-за ограничений на возможность использовать компиляторы C++ 11 и от безальтернативности boost’у возникло желание написать свою реализацию стандартной библиотеки C++ 11 поверх поставляемой с компилятором библиотеки C++ 98 / C++ 03.
Были реализованы static_assert, noexcept, countof, а так же, после рассмотрения всех нестандартных дефайнов и особенностей компиляторов, появилась информация о функциональности, которая поддерживается текущим компилятором. Включена своя реализация nullptr, которая подбирается на этапе компиляции.
Настало время type_traits и всей этой «особой шаблонной магии».
Ссылка на GitHub с результатом на сегодня для нетерпеливых и нечитателей:
Коммиты и конструктивная критика приветствуются
Погрузимся же в мир «шаблонной магии» C++.
Оглавление
Введение
Глава 1. Viam supervadet vadens
Глава 2. #ifndef __CPP11_SUPPORT__ #define __COMPILER_SPECIFIC_BUILT_IN_AND_MACRO_HELL__ #endif
Глава 3. Поиск идеальной реализации nullptr
Глава 4. Шаблонная «магия» C++
…4.1 Начинаем с малого
Глава 5.
…
Глава 4. Шаблонная «магия» C++
Закончив с ключевыми словами C++ 11 и всеми define-зависимыми «переключениями» между их реализациями я стал наполнять type_traits. По правде говоря у меня уже было довольно много шаблонных классов, аналогичных стандартным, которые уже работали в проектах довольно давно и потому оставалось все это привести в одинаковый вид, а так же дописать недостающую функциональность.
Честно вам скажу что меня вдохновляет шаблонное программирование. Особенно осознание того что все это многообразие вариантов: рассчеты, ветвления кода, условия, проверки на ошибки выполняется в процессе компиляции и ничего не стоит итоговой программе на этапе выполнения. И так как шаблоны в C++ это по сути Тьюринг-полный язык программирования, то я был в предвкушении того как изящно и относительно легко будет реализовывать часть стандарта, связанную с программированием на шаблонах. Но, дабы сразу разрушить все иллюзии, скажу что вся теория о Тьюринг-полноте разбивается о конкретные реализации шаблонов в компиляторах. И эта часть написания библиотеки вместо изящных решений и «трюков» шаблонного программирования превращалась в яростную борьбу с компиляторами, при том что каждый «заваливался» по-своему, и хорошо, если в невнятный internal compiler error, а то и наглухо зависал или вылетал с необработанными исключениями. Лучше всех себя показал GCC (g++), который стоически «пережевывал» все шаблонные конструкции и только ругался (по делу) в местах где не хватало явного typename.
4.1 Начинаем с малого
Начал я с простых шаблонов для std: integral_constant, std: bool_constant и подобных небольших шаблонов.
template
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;
operator value_type() const
{ // return stored value
return (value);
}
value_type operator()() const
{ // return stored value
return (value);
}
};
typedef integral_constant true_type;
typedef integral_constant false_type;
template
struct bool_constant :
public integral_constant
{};
// Primary template.
// Define a member typedef @c type to one of two argument types.
template
struct conditional
{
typedef _Iftrue type;
};
// Partial specialization for false.
template
struct conditional
{
typedef _Iffalse type;
};
На основе conditional можно ввести удобные шаблоны для логических операций {«и», «или», «не»} над типами (И все эти операции считаются прямо на этапе компиляции! Здорово, не правда ли?):
namespace detail
{
struct void_type {};
//typedef void void_type;
template
struct _or_ :
public conditional<_B1::value, _B1, _or_<_B2, _or_<_B3, _B4> > >::type
{ };
template<>
struct _or_;
template
struct _or_<_B1, void_type, void_type, void_type> :
public _B1
{ };
template
struct _or_<_B1, _B2, void_type, void_type> :
public conditional<_B1::value, _B1, _B2>::type
{ };
template
struct _or_<_B1, _B2, _B3, void_type> :
public conditional<_B1::value, _B1, _or_<_B2, _B3> >::type
{ };
template
struct _and_;
template<>
struct _and_;
template
struct _and_<_B1, void_type, void_type, void_type> :
public _B1
{ };
template
struct _and_<_B1, _B2, void_type, void_type> :
public conditional<_B1::value, _B2, _B1>::type
{ };
template
struct _and_<_B1, _B2, _B3, void_type> :
public conditional<_B1::value, _and_<_B2, _B3>, _B1>::type
{ };
template
struct _not_
{
static const bool value = !bool(_Pp::value);
typedef const bool value_type;
typedef integral_constant type;
operator value_type() const
{ // return stored value
return (value);
}
value_type operator()() const
{ // return stored value
return (value);
}
};
}
Здесь внимания заслуживают три момента:
1) Важно везде ставить пробел между угловыми скобками ('<' и '>') у шаблонов, так как до C++ 11 в стандарте небыло уточнения о том как трактовать '>>' и '<<' в коде типа _or_<_B2, _or_<_B3, _B4>>, и потому практически все компиляторы трактовали это как оператор битового сдвига, что приводит к ошибке компиляции.
2) В некоторых компиляторах (Visual Studio 6.0 к примеру) был баг, который заключался в том что нельзя было использовать тип void как шаблонный параметр. Для этих целей в отрывке выше вводится отдельный тип void_type чтобы заменить тип void там, где требуется значение шаблонного параметра по-умолчанию.
3) Очень старые компиляторы (Borland C++ Builder к примеру) имели криво реализованный тип bool, который в некоторых ситуациях «вдруг» превращался в int (true → 1, false → 0), а так же плохо выводили типы константных статических переменных типа bool (да и не только их), если те содержались в шаблонных классах. Из-за всего этого бардака в итоге на совершенно безобидное сравнение в стиле my_template_type: static_bool_value == false компилятор запросто мог выдать фееричное can not cast 'undefined type' to int (0) или что-то подобное. Потому необходимо стараться всегда явно указывать тип значений для сравнения, тем самым помогая компилятору определиться с какими типами он имеет дело.
Добавим еще работу с const и volatile значениями. Сначала тривиально реализуемые remove_… где мы просто специализируем шаблон для определенных модификаторов типа — в случае если в шаблон придет тип с модификатором компилятор обязан, просмотрев все специализации (вспомним принцип SFINAE из предыдущей главы) шаблона, выбрать наиболее подходящую (с явным указанием нужного модификатора):
template
struct is_function;
template
struct remove_const
{ // remove top level const qualifier
typedef _Tp type;
};
template
struct remove_const
{ // remove top level const qualifier
typedef _Tp type;
};
template
struct remove_const
{ // remove top level const qualifier
typedef volatile _Tp type;
};
// remove_volatile
template
struct remove_volatile
{ // remove top level volatile qualifier
typedef _Tp type;
};
template
struct remove_volatile
{ // remove top level volatile qualifier
typedef _Tp type;
};
// remove_cv
template
struct remove_cv
{ // remove top level const and volatile qualifiers
typedef typename remove_const::type>::type
type;
};
А затем реализуем шаблоны add_… где все уже немного посложнее:
namespace detail
{
template
struct _add_const_helper
{
typedef _Tp const type;
};
template
struct _add_const_helper<_Tp, true>
{
typedef _Tp type;
};
template
struct _add_volatile_helper
{
typedef _Tp volatile type;
};
template
struct _add_volatile_helper<_Tp, true>
{
typedef _Tp type;
};
template
struct _add_cv_helper
{
typedef _Tp const volatile type;
};
template
struct _add_cv_helper<_Tp, true>
{
typedef _Tp type;
};
}
// add_const
template
struct add_const:
public detail::_add_const_helper<_Tp, is_function<_Tp>::value>
{
};
template
struct add_const<_Tp&>
{
typedef _Tp & type;
};
// add_volatile
template
struct add_volatile :
public detail::_add_volatile_helper<_Tp, is_function<_Tp>::value>
{
};
template
struct add_volatile<_Tp&>
{
typedef _Tp & type;
};
// add_cv
template
struct add_cv :
public detail::_add_cv_helper<_Tp, is_function<_Tp>::value>
{
};
template
struct add_cv<_Tp&>
{
typedef _Tp & type;
};
Здесь мы аккуратно отдельно обработаем ссылочные типы чтобы не потерять ссылку. Так же не забудем про типы функций, которые сделать volatile или const впринципе невозможно, потому мы оставим их «как есть». Могу сказать что все это выглядит весьма просто, но это именно тот случай когда «дьявол кроется в деталях», а точнее «баги кроются в деталях реализации».
Конец первой части четвертой главы. Во второй части я расскажу про то как тяжело шаблонное программирование дается компилятору, а так же будет больше крутой шаблонной «магии». Ах, и еще — почему же long long не является integral constant по мнению некоторых компиляторов и по сей день.
Благодарю за внимание.