Краткое содержание предыдущих частей
Из-за ограничений на возможность использовать компиляторы 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<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;
operator value_type() const
{ // return stored value
return (value);
}
value_type operator()() const
{ // return stored value
return (value);
}
};
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>
{};
// Primary template.
// Define a member typedef @c type to one of two argument types.
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;
};
На основе conditional можно ввести удобные шаблоны для логических операций {«и», «или», «не»} над типами (И все эти операции считаются прямо на этапе компиляции! Здорово, не правда ли?):
namespace detail
{
struct void_type {};
//typedef void void_type;
template<class _B1 = void_type, class _B2 = void_type, class _B3 = void_type, class _B4 = void_type>
struct _or_ :
public conditional<_B1::value, _B1, _or_<_B2, _or_<_B3, _B4> > >::type
{ };
template<>
struct _or_<void_type, void_type, void_type, void_type>;
template<class _B1>
struct _or_<_B1, void_type, void_type, void_type> :
public _B1
{ };
template<class _B1, class _B2>
struct _or_<_B1, _B2, void_type, void_type> :
public conditional<_B1::value, _B1, _B2>::type
{ };
template<class _B1, class _B2, class _B3>
struct _or_<_B1, _B2, _B3, void_type> :
public conditional<_B1::value, _B1, _or_<_B2, _B3> >::type
{ };
template<class _B1 = void_type, class _B2 = void_type, class _B3 = void_type, class _B4 = void_type>
struct _and_;
template<>
struct _and_<void_type, void_type, void_type, void_type>;
template<class _B1>
struct _and_<_B1, void_type, void_type, void_type> :
public _B1
{ };
template<class _B1, class _B2>
struct _and_<_B1, _B2, void_type, void_type> :
public conditional<_B1::value, _B2, _B1>::type
{ };
template<class _B1, class _B2, class _B3>
struct _and_<_B1, _B2, _B3, void_type> :
public conditional<_B1::value, _and_<_B2, _B3>, _B1>::type
{ };
template<class _Pp>
struct _not_
{
static const bool value = !bool(_Pp::value);
typedef const bool value_type;
typedef integral_constant<bool, _not_::value == bool(true)> 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<class _Tp>
struct is_function;
template<class _Tp>
struct remove_const
{ // remove top level const qualifier
typedef _Tp type;
};
template<class _Tp>
struct remove_const<const _Tp>
{ // remove top level const qualifier
typedef _Tp type;
};
template<class _Tp>
struct remove_const<const volatile _Tp>
{ // remove top level const qualifier
typedef volatile _Tp type;
};
// remove_volatile
template<class _Tp>
struct remove_volatile
{ // remove top level volatile qualifier
typedef _Tp type;
};
template<class _Tp>
struct remove_volatile<volatile _Tp>
{ // remove top level volatile qualifier
typedef _Tp type;
};
// remove_cv
template<class _Tp>
struct remove_cv
{ // remove top level const and volatile qualifiers
typedef typename remove_const<typename remove_volatile<_Tp>::type>::type
type;
};
А затем реализуем шаблоны add_… где все уже немного посложнее:
namespace detail
{
template<class _Tp, bool _IsFunction>
struct _add_const_helper
{
typedef _Tp const type;
};
template<class _Tp>
struct _add_const_helper<_Tp, true>
{
typedef _Tp type;
};
template<class _Tp, bool _IsFunction>
struct _add_volatile_helper
{
typedef _Tp volatile type;
};
template<class _Tp>
struct _add_volatile_helper<_Tp, true>
{
typedef _Tp type;
};
template<class _Tp, bool _IsFunction>
struct _add_cv_helper
{
typedef _Tp const volatile type;
};
template<class _Tp>
struct _add_cv_helper<_Tp, true>
{
typedef _Tp type;
};
}
// add_const
template<class _Tp>
struct add_const:
public detail::_add_const_helper<_Tp, is_function<_Tp>::value>
{
};
template<class _Tp>
struct add_const<_Tp&>
{
typedef _Tp & type;
};
// add_volatile
template<class _Tp>
struct add_volatile :
public detail::_add_volatile_helper<_Tp, is_function<_Tp>::value>
{
};
template<class _Tp>
struct add_volatile<_Tp&>
{
typedef _Tp & type;
};
// add_cv
template<class _Tp>
struct add_cv :
public detail::_add_cv_helper<_Tp, is_function<_Tp>::value>
{
};
template<class _Tp>
struct add_cv<_Tp&>
{
typedef _Tp & type;
};
Здесь мы аккуратно отдельно обработаем ссылочные типы чтобы не потерять ссылку. Так же не забудем про типы функций, которые сделать volatile или const впринципе невозможно, потому мы оставим их «как есть». Могу сказать что все это выглядит весьма просто, но это именно тот случай когда «дьявол кроется в деталях», а точнее «баги кроются в деталях реализации».
Конец первой части четвертой главы. Во второй части я расскажу про то как тяжело шаблонное программирование дается компилятору, а так же будет больше крутой шаблонной «магии». Ах, и еще — почему же long long не является integral constant по мнению некоторых компиляторов и по сей день.
Благодарю за внимание.
Автор: oktonion