Микрогайд по type_traits
t.me/thisnotes<type_traits> -- хедер стандартной библиотеки, являющийся частью библиотеки для метапрограммирования.
Я не буду вам пояснять принципы работы тех или иных трейтов. Мы не будем разбираться в том, что такое using, наследование, как выбирается специализация шаблона или работает SFINAE. Я надеюсь, что вы это уже понимаете. Но я вам покажу несколько моих любимых трейтов, которые нравятся мне чисто из-за идеи. Может где-то покажу интересные примеры применения. И попробуем глянуть на то, как часть из них можно реализовать.
Сделаем мы это в форме небольшого путешествия от простого к сложному.
Шаг 1. База
Начнём с самых базовых: std::true_type, std::false_type. Это своего рода true/false в пространстве типов. Реализованы они так:
template <typename T, T v>
struct integral_constant {
static constexpr T value = v;
};
struct true_type : integral_constant<bool, true> {};
struct false_type : integral_constant<bool, false> {};
В следующий раз, когда вы захотите реализовать какой-нибудь трейт, скорее всего они вам пригодятся.
Другой полезный хелпер: std::type_identity:
template <typename T>
struct type_identity { // since C++20
using type = T;
};
Теперь можем реализовать какой-нибудь простенький трейт. Например std::is_same, который говорит, являются ли два типа одинаковыми:
static_assert(is_same_v<int, int>); // true
static_assert(is_same_v<int, std::int32_t>); // usually true if int is 32 bit
// static_assert(is_same_v<int, std::int64_t>); // false
template <typename T, typename U>
struct is_same : std::false_type {};
template <typename T>
struct is_same<T, T> : std::true_type {};
Вы же помните результат дляstd::is_sameи трёх парchar+signed charиchar+unsigned char?
Рядом ещё обычно пишут версию с _v, чтобы не надо было писать это ::value каждый раз
template <typename T, typename U> inline constexpr bool is_same_v = is_same<T, U>::value;
Аналогично с _t over ::type для трейтов, возвращающих типа. Дальше мы это будем опускать.
Мы часто используем std::is_same/std::same_as, когда пишем код вида:
template <typename Object>
void FillObjectData(Object& obj) {
// some common code
if constexpr (std::is_same_v<Object, CustomType>) {
FillCustomTypeData(obj);
} else {
FillCommonData(obj);
}
// some common code
}
Т.е. когда у нас есть похожая логика, которая отличается точечно для разных типов, чтобы не копировать код.
Умея специализировать шаблоны подобным образом, мы можем реализовать и другие трейты:
template <class T> struct is_lvalue_reference : std::false_type {};
template <class T> struct is_lvalue_reference<T&> : std::true_type {};
Аналогично вы можете написать std::is_rvalue_reference, std::is_pointer (помните, что указатели бывают разные), std::is_const/std::is_volatile.
Точно по тем же правилам пишется какой-нибудь std::remove_reference:
template <class T> struct remove_reference : type_identity<T> {};
template <class T> struct remove_reference<T&> : type_identity<T> {};
template <class T> struct remove_reference<T&&> : type_identity<T> {};
Или std::rank (который считает количество измерений вашего массива):
template <class T>
struct rank : std::integral_constant<std::size_t, 0> {};
template <class T>
struct rank<T[]> : std::integral_constant<std::size_t, rank<T>::value + 1> {};
template <class T, std::size_t N>
struct rank<T[N]> : std::integral_constant<std::size_t, rank<T>::value + 1> {};
Шаг 2. Простые идеи
Начнём с std::is_signed. Мне нравится реализация, потому что она пользуется простым очевидным фактом про знаковые числа: -1 < 0!
namespace detail {
template <typename T, bool = std::is_arithmetic<T>::value>
struct is_signed : std::integral_constant<bool, T(-1) < T(0)> {};
template <typename T>
struct is_signed<T, false> : std::false_type {};
} // namespace detail
template <typename T>
struct is_signed : detail::is_signed<T>::type {};
Т.е. мы пытаемся создать из -1 и 0 переданный тип. И если итоговые объекты сохранили отношение порядка, то у нас знаковый тип! Аналогично в std::is_unsigned мы проверим наоборот:
T(-1) > T(0)
Ведь беззнаковый тип переполнится!
Ещё одной полезной утилью является std::conditional. Пишется довольно просто:
template<bool B, class T, class F>
struct conditional : type_indentity<T> {};
template<class T, class F>
struct conditional<false, T, F> : type_indentity<F> {};
Применять такое можно, например, когда вы хотите в зависимости от размера объекта понять, как его лучше хранить:
template<typename T> using OptimizedStorage = std::conditional_t< (sizeof(T) <= sizeof(void*)), T, // Store directly if small T* // Store as pointer if large >;
Или дать пользователю возможность выбрать способ хранение объектов:
template<bool UseArray>
struct Container {
using Storage = std::conditional_t<
UseArray,
std::array<int, 100>,
std::vector<int>
>;
Storage data;
};
Или выбрать нужный тип в зависимости от платформы. Вариантов масса.
Дальше мы можем вспомнить про variadic templates, мы можем сделать общего вида конъюнкцию и дизъюнкцию:
template <class...> struct conjunction : std::true_type {};
template <class B1> struct conjunction<B1> : B1 {};
template <class B1, class... Bn>
struct conjunction<B1, Bn...>
: std::conditional_t<bool(B1::value), conjunction<Bn...>, B1> {};
А дальше можем реализовать обобщение std::is_same:
template <typename T, typename... Args>
struct is_all_same : std::conjunction<std::is_same<T, Args>...> {};
Так всё красиво складывается. Ну ваще.
Ещё одна красивая идея лежит в реализации std::common_type -- трейт, позволяющий найти общий тип для нескольких указанных. Возможно это какой-то родитель в общей иерархии, если таковой существует. Тут мы будем использовать простое свойство тернарного оператора -- обе его ветки должны возвращать один и тот же тип. А для этого ему нужно найти цепочку преобразований указанных типов в какой-то третий, чтобы код скомпилировался. Если конечно такая цепочка существует:
template<typename... T>
struct common_type;
template<typename T>
struct common_type<T> {
using type = T;
};
template<typename T1, typename T2>
struct common_type<T1, T2> {
using type = decltype(true ? std::declval<T1>() : std::declval<T2>());
};
Написать общую версию для N типов оставим упражнением.
Или можно реализовать std::is_base_of через попытку скастовать указатель на Derived к указателю на Base.
Шаг 3. Помоги Даше найти метод
В какой-то момент вы начинаете хотеть проверять свойства ваших объектов. Например, есть ли у вас конкретный метод, using внутри или что угодно другое. Мы можем проверить наличие using'а key_type например вот так:
template <typename Container, typename = void>
struct HasKeyType : std::false_type {};
template <typename Container>
struct HasKeyType<
Container, std::void_t<typename Container::key_type>
> : std::true_type {};
Не будем погружаться в то, почему это работает (вы же шарите в SFINAE? Если не шарите, то и не надо. Сразу используйте концепты). Но оно работает!! Теперь можно написать эффективный EraseIf, если вдруг у вас нет стандартного:
template <typename Container, typename Pred>
auto EraseIf(Container& container, Pred pred) {
if constexpr (HasKeyType<Container>) {
// delete by key
} else {
// std::remove_if + erase
}
}
Таким образом детектить можно всё что угодно. В Boost даже есть специальные хелперы, которые помогут вам не писать это руками:
template<class T>
using clear_t = decltype(boost::declval<T&>().clear());
...
if constexpr (boost::is_detected_v<clear_t, T>) {...}
Но если вам нужно что-то более стандартное, то вы можете выбрать из огромного готового списка [3]: has_plus, has_complement, has_nothrow_copy и т.д.
Я когда-то в своей микролибе писал макрос, позволяющий сгенерить что угодно для подобной задачи:
#define HAS_METHOD(NAME) \
namespace wezen { \
template <class T, class... Args> \
struct has_##NAME { \
private: \
template <class TT, class... Aargs, \
class = decltype(std::declval<TT>().NAME(std::declval<Aargs>()...))> \
static std::true_type f(int); \
template <class...> \
static std::false_type f(...); \
public: \
using type = decltype(f<T, Args...>(0)); \
}; \
template <class T, class... Args> \
constexpr inline bool has_##NAME##_v = std::is_same_v< \
typename has_##NAME<T, Args...>::type, \
std::true_type \
>; \
} // namespace wezen
NAME -- имя метода. Нетрудно догадаться, что после раскрытия макроса для условного NAME=TrololeloTralala сгенерируется переменная has_TrololeloTralala_v и структура has_TrololeloTralala, через которую вы можете проверить возможность вызова метода TrololeloTralala с конкретными аргументами.
Шаг 4. Не всё так просто
Не все трейты можно реализовать с помощью стандартных механизмов языка. Другие можно, но не нужно, потому что C++ это про энергию, про скорость. Хотим всё-таки эффективного чего-то.
Или, например, реализовать has_virtual_destructor без знания метаинформации о типе нельзя. Если мы посмотрим в clang, то такое и найдём:
template <class _Tp>
struct _LIBCPP_NO_SPECIALIZATIONS has_virtual_destructor
: public integral_constant<bool, __has_virtual_destructor(_Tp)> {};
Функция has_virtual_destructor в итоге уходит примерно в эту:
static bool hasVirtualDestructor(QualType T) {
if (const CXXRecordDecl *RD = T->getAsCXXRecordDecl())
if (const CXXDestructorDecl *DD = RD->getDestructor())
return DD->isVirtual();
return false;
}
Функция получается объект T -- тип, для которого мы пытаемся проверить наличие виртуального дтора. Далее мы пытаемся получить объявление класса (первый if). Если получилось, то у нас действительно класс и можем попробовать найти у него дтор (явный или неявный). Если нашли, то возвращаем признак виртуальности. Всё просто!
Пришли
Мне нравится, что для того, чтобы написать хорошую, полную библиотеку type traits, вам нужно знать не только шаблонные приколы. Вам нужно хорошо понимать возможности языка. Глубоко подумать о том, как работают базовые конструкции. И вроде просто шаблончики перекладываешь, а по факту получил кусочек просветления в разных частях этого сложного C++.
Никогда не знаешь, что где пригодится.
1. cppreference.com: https://en.cppreference.com/w/cpp/header/type_traits.html
2. Примеры использования разных трейтов из документации Boost: https://www.boost.org/doc/libs/latest/libs/type_traits/doc/html/boost_typetraits/examples.html
3. Список трейтов в Boost: https://www.boost.org/doc/libs/latest/libs/type_traits/doc/html/index.html