Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing variadic type traits

Intro

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.

Example

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; 

Current approach

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

Generalizing

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

Question

The previous cover generalization on type traits related to

  • Primary type categories
  • Composite type categories
  • Type properties
  • Supported operations

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 ?

like image 238
Nikos Athanasiou Avatar asked Jul 10 '14 21:07

Nikos Athanasiou


3 Answers

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... >;
like image 139
Daniel Frey Avatar answered Sep 23 '22 15:09

Daniel Frey


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 
}
like image 29
Patryk Avatar answered Sep 21 '22 15:09

Patryk


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:

  • Tuples and a way to produce the tail of a tuple
  • A way to extend bool sequences by appending (or prepending) boolean values to it

TL;DR

I compiled a few examples in this live demo.

Defining the variadic trait facilities

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;
};

}

Combining the constructs

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;
like image 37
Rerito Avatar answered Sep 20 '22 15:09

Rerito