I'm looking for a pattern to convert C++ type traits into their variadic counterparts. A methodology to approach the problem would be appreciated and generative programming patterns to automate the task would be ideal.
Take the following :
std::is_same<T, U>::value;
I want to write a trait that works like so :
std::are_same<T1, T2, T3, T4>::value;
It's pretty straightforward to implement the are_same
; Seeking a general solution we can come up with a tool for any variadic trait implementing universal quantification :
template<template<class,class> class F, typename...Ts>
struct Univ;
template<template<class, class> class F, typename T, typename U, typename...Ts>
struct Univ<F, T, U, Ts...>
{
static const int value = F<T, U>::value && Univ<F, U, Ts...>::value;
};
template<template<class, class> class F, typename T>
struct Univ<F, T>
{
static const int value = 1;
};
so that eg are_same
could be written as
Univ<is_same,int, int, int>::value
and this could apply when creating traits like are_classes
, are_scalars
etc
Minor tweaks could give existential quantification out of the previous snippet (replacing &&
with ||
) so that we create traits like exist_same
in the following fashion :
Exist<is_same, int, double, float>::value
The previous cover generalization on type traits related to
How would I generalize for type traits like the following :
enable_if -> enable_if_any // enable if any clause is true
enable_if_all // enalbe if all clauses are true
enable_for // enable only for the type provided
The exist_same
example above is oversimplified. Any ideas for a correct implementation?
There are type_traits that "return" modified types. Any suggestion for scaling those to implementations for arbitrary number of types ?
Are there type_traits which are made not to scale to arbitrary number of type arguments ?
I don't fully understand what exactly you'd like to achieve, but the following helpers might be useful, starting with bool_sequence
:
#include <type_traits>
// Note: std::integer_sequence is C++14,
// but it's easy to use your own version (even stripped down)
// for the following purpose:
template< bool... Bs >
using bool_sequence = std::integer_sequence< bool, Bs... >;
// Alternatively, not using C++14:
template< bool... > struct bool_sequence {};
next, you can check if all or any boolean value or set with these:
template< bool... Bs >
using bool_and = std::is_same< bool_sequence< Bs... >,
bool_sequence< ( Bs || true )... > >;
template< bool... Bs >
using bool_or = std::integral_constant< bool, !bool_and< !Bs... >::value >;
they come in handy as building blocks for more advanced and specialized traits. For example, you could use them like this:
typename< typename R, bool... Bs > // note: R first, no default :(
using enable_if_any = std::enable_if< bool_or< Bs... >::value, R >;
typename< typename R, bool... Bs > // note: R first, no default :(
using enable_if_all = std::enable_if< bool_and< Bs... >::value, R >;
typename< typename T, typename... Ts >
using are_same = bool_and< std::is_same< T, Ts >::value... >;
You can also use std::conditional
in order to achieve enable_if_all
and enable_if_any
:
#include <type_traits>
#include <iostream>
#include <initializer_list>
#include <string>
namespace detail
{
template <typename... Conds>
struct and_ : std::true_type {};
template <typename... Conds>
struct or_ : std::false_type {};
template <typename Cond, typename... Conds>
struct and_<Cond, Conds...>
: std::conditional<Cond::value, detail::and_<Conds...>, std::false_type>::type {};
template <typename Cond, typename... Conds>
struct or_<Cond, Conds...>
: std::conditional<Cond::value, std::true_type, detail::and_<Conds...>>::type {};
}
template <typename... T>
using are_all_pod = detail::and_<std::is_pod<T>...>;
template <typename... T>
using any_is_pod = detail::or_<std::is_pod<T>...>;
template <typename... Args, typename = typename std::enable_if<are_all_pod<Args...>::value>::type>
void f(Args... args)
{
(void)std::initializer_list<int>{(std::cout << args << '\n' , 0)...};
}
template <typename... Args, typename = typename std::enable_if<any_is_pod<Args...>::value>::type>
void g(Args... args)
{
(void)std::initializer_list<int>{(std::cout << args << '\n' , 0)...};
}
int main()
{
std::string s = "hello"; // non pod
//f(1, 1.2, s); // this will fail because not all types are pod
g(1, 1.2, s); // this compiles because there is at least one pod in argument pack
}
Inspired by the excellent idea in Daniel Fray's answer, we can even extend the scope of these variadic traits. Using tuples, we can apply traits on collections of variadic type packs instead of "only" comparing a variadic pack of types to a reference type.
For example, we will be able to see if the types int, int, int, float
are the same types as int, int, int, float
(indeed they are!).
To do so, we need the following constructs:
I compiled a few examples in this live demo.
First we provide a helper to extend a bool sequence one value at a time:
template <bool ... Bs>
struct bool_sequence {};
template <bool b, typename T>
struct prepend_bool_seq;
template <bool b, bool ... bs>
struct prepend_bool_seq<b, bool_sequence<bs...>> {
typedef bool_sequence<b, bs...> type;
};
Now some logic on bool sequences (taken from other answers)
template <typename T>
struct all_of;
template <bool ... Bs>
struct all_of<bool_sequence<Bs...>> :
public std::is_same<bool_sequence<true, Bs...>, bool_sequence<Bs..., true>> {};
template <typename T>
struct any_of;
template <bool ... Bs>
struct any_of<bool_sequence<Bs...>> :
public std::integral_constant<bool, !all_of<bool_sequence<!Bs...>>::value> {};
Then, we define an helper template to access a tuple's tail:
namespace details {
// Sentinel type to detect empty tuple tails
struct null_type {};
template <typename T>
struct tuple_tail;
template <typename T>
struct tuple_tail<std::tuple<T>> {
typedef null_type type;
};
template <typename T, typename ... Ts>
struct tuple_tail<std::tuple<T, Ts...>> {
typedef std::tuple<Ts...> type;
};
}
With these bricks, we can now define an apply_trait
template to apply a given type trait on several type lists:
namespace details {
template <template <typename...> class Trait, typename ... Tuples>
struct apply_trait {
static constexpr bool atomic_value =
Trait<typename std::tuple_element<0u, Tuples>::type...>::value;
typedef typename prepend_bool_seq<atomic_value,
typename apply_trait<Trait,
typename tuple_tail<Tuples>::type...>::type>::type type;
};
template <template <typename...> class Trait, typename ... Tuples>
struct apply_trait<Trait, null_type, Tuples...> {
typedef bool_sequence<> type;
};
}
This template recursively computes the bool sequence given by the trait application in a bottom-up fashion. Now, provided with the resulting bool sequence, we can perform logical operations on the result with the helpers defined above.
Next, some helpers can reproduce the logic of your are_same
example for any binary (or unary) type trait:
// Helper templates for common type traits (unary and binary)
template <template <typename> class UnaryTrait, typename ... Ts>
using apply_unary_trait = details::apply_trait<UnaryTrait, std::tuple<Ts...>>;
template <template <typename, typename> class BinaryTrait, typename Ref, typename ... Ts>
using apply_binary_trait = details::apply_trait<BinaryTrait,
std::tuple<decltype(std::declval<Ts>(), std::declval<Ref>())...>,
std::tuple<Ts...>>;
template <template <typename, typename> class BinaryTrait, typename Ref, typename ... Ts>
using apply_binary_trait_ref_last = details::apply_trait<BinaryTrait,
std::tuple<Ts...>,
std::tuple<decltype(std::declval<Ts>(), std::declval<Ref>())...>>;
For example, we can reproduce the are_same
design you brought up for every binary trait:
template <typename Ref, typename ... Ts>
using are_same = all_of<typename apply_binary_trait<std::is_same, Ref, Ts...>::type>;
We can also apply the traits logic on lists. For example, given two lists of types, we may want to check if a type in the first list is convertible to its matching type in the second list:
// int is convertible to long and char const* is convertible to std::string
std::cout << all_of<details::apply_trait<std::is_convertible,
std::tuple<int, char const*>,
std::tuple<long, std::string>::type>::value;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With