Type traits for cv-qualified and ref types
Handling cv-qualified and ref types in type traits
When you write a type-trait based on specialization, it won’t work correctly for cv-qualified or reference types:
#include <type_traits>
template <int N>
struct foo {};
template <typename T>
struct is_foo : std::false_type {};
template <int N>
struct is_foo<foo<N>> : std::true_type {};
template <typename T>
constexpr bool is_foo_v = is_foo<T>::value;
// Works OK for plain type
static_assert(is_foo_v<foo<1>>);
static_assert(is_foo_v<foo<2>>);
// Doesn't work for const or ref types
static_assert(!is_foo_v<foo<1> const>);
static_assert(!is_foo_v<foo<2>&>);
A tedious solution
One option is to provide further specializations for all the different qualifiers:
template <int N>
struct is_foo<foo<N> const> : std::true_type {};
template <int N>
struct is_foo<foo<N>&> : std::true_type {};
template <int N>
struct is_foo<foo<N> const&> : std::true_type {};
template <int N>
struct is_foo<foo<N>&&> : std::true_type {};
template <int N>
struct is_foo<foo<N> const &&> : std::true_type {};
// ... we haven't even done 'volatile' yet
That gets old very quickly.
A more elegant solution
As is often the case in programming, a more elegant answer lies in exploiting a layer of indirection.
The following technique, borrowed from Baptiste Wicht’s ETL library
defines the basic type trait in a detail namespace, so that callers don’t use it
directly. The library provides a variable template for use by the caller, but
crucially, the variable template applies the trait to std::decay_t<T>
, to
ensure that cv and ref-qualified types are handled transparently:
#include <type_traits>
template <int N>
struct foo {};
namespace detail {
template <typename T>
struct is_foo_impl : std::false_type {};
template <int N>
struct is_foo_impl<foo<N>> : std::true_type {};
} // namespace detail
template <typename T>
constexpr bool is_foo = detail::is_foo_impl<std::decay_t<T>>::value;
// Now handles types with extra qualifiers
static_assert(is_foo<foo<1>>);
static_assert(is_foo<foo<2>>);
static_assert(is_foo<foo<1> const>);
static_assert(is_foo<foo<2>&>);
static_assert(is_foo<foo<42> const volatile&&>);
The standard library
The standard library uses a similar technique. This example is from the implementation of the placeholder types for std::bind
:
template<class _Tp> struct __is_placeholder
: public integral_constant<int, 0> {};
template<class _Tp> struct _LIBCPP_TYPE_VIS_ONLY is_placeholder
: public __is_placeholder<typename remove_cv<_Tp>::type> {};
namespace placeholders
{
template <int _Np> struct __ph {};
_LIBCPP_FUNC_VIS extern __ph<1> _1;
_LIBCPP_FUNC_VIS extern __ph<2> _2;
_LIBCPP_FUNC_VIS extern __ph<3> _3;
_LIBCPP_FUNC_VIS extern __ph<4> _4;
_LIBCPP_FUNC_VIS extern __ph<5> _5;
_LIBCPP_FUNC_VIS extern __ph<6> _6;
_LIBCPP_FUNC_VIS extern __ph<7> _7;
_LIBCPP_FUNC_VIS extern __ph<8> _8;
_LIBCPP_FUNC_VIS extern __ph<9> _9;
_LIBCPP_FUNC_VIS extern __ph<10> _10;
} // placeholders
template<int _Np>
struct __is_placeholder<placeholders::__ph<_Np> >
: public integral_constant<int, _Np> {};
The __is_placeholder
struct is an implementation detail that callers aren’t expected to use directly. The is_placeholder
struct inherits from __is_placeholder
, but removes cv-qualifiers from the type before passing it on.