Implementing variadic type traits
Asked Answered
T

3

20

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 ?

Timid answered 10/7, 2014 at 21:59 Comment(3)
For the "returning multiple types": You can only return one top-level type. To return a list of types, one usually uses std::tuple< T1, T2, ... >.Bag
Stuff like this makes me want the template equivalent of using -- template<template<class,class>class F>using for_all=template<class...Ts>Univ<F,Ts...>;, used like for_all<std::is_same><int,int,int>, and it would let you easily pass the template<class...> generated to other meta programming without ugliness.Heliotherapy
Univ is a very useful tool. I wonder if you could supply the implementation of ExistDebt
B
25

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... >;
Bag answered 10/7, 2014 at 22:12 Comment(11)
@0x499602D2 Boolean logic: bool_or<B1,B2,B3,...> = !bool_and<!B1,!B2,!B3,!...> - does that answer your question? Otherwise, please be more specific which part of the implementation is causing you trouble.Bag
+1 bool_sequence< ( Bs || true )... > is a most handy snippet.Casablanca
@Casablanca I'd be tempted by (Bs,true).... Regardless, I like that it has constant recursive depth!Heliotherapy
@Yakk (Bs,true)... will lead to warnings on some compilers, that's why I used (Bs||true)....Bag
Funny looks like bool_or<false>::value reports true in VC++ 2013. Works in clangVogel
Isn't this missing some of the bool_sequence implementation for C++11 (pre-C++14)?Snowfall
@Snowfall I don't understand your question. What do you think is missing exactly?Bag
@DanielFrey: It seems like bool_sequence is never anything other than { } the way you defined it (right after // Alternatively, not using C++14).Snowfall
@Snowfall Yes. For the above example that is all that is needed. This is a SO answer, not a complete implementation of an STL.Bag
@DanielFrey: Oh, you mean the different template parameters would be enough to make bool_sequence<false> not the same as bool_sequence<true>?Snowfall
@Snowfall Exactly. It's a template, each set of (different) parameters generates a different type and that is what is needed for std::is_same.Bag
P
1

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 
}
Punctilious answered 8/7, 2016 at 15:47 Comment(0)
M
1

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;
Marchant answered 27/7, 2016 at 14:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.