3

I am trying to figure out which is the most idiomatic way implement a function over a variadic type list. For example, computing the maximum size of all the types. I understand there exist several approaches to accomplish such a task, but I would like to know when to choose which strategy.

These are the mechanisms I would consider (there may exist more, please mention if so):

  • Type traits (ideally succinctly with using declarations):

    template <typename Head>
    using max_size = typename std::integral_constant<size_t, sizeof(Head)>::type;
    
    template <typename Head, typename... Tail>
    using max_size = ?;
    
  • constexpr functions:

    template <typename Head>
    constexpr size_t max_size() { return sizeof(Head); }
    
    template <typename Head, typename... Tail>
    constexpr size_t max_size() { ? }
    

My question is twofold:

  1. What features of the computation determine what strategy to choose?

  2. In each case, how would an example implementation for the maximum size example described above look like?

mavam
  • 12,242
  • 10
  • 53
  • 87

3 Answers3

5

I personally prefer functions rather than traits, I find them easier to manipulate and more natural. But that's certainly subjective ;)

#include <iostream>

template <typename Head>
constexpr size_t max_size() { return sizeof(Head); }

template <typename Head, typename Next, typename... Tail>
constexpr size_t max_size() {
    return max_size<Head>() > max_size<Next, Tail...>() ?
           max_size<Head>() : max_size<Next, Tail...>();
}

int main() {
  std::cout << "int: " << max_size<int>() << "\n";
  std::cout << "char, short, int: " << max_size<char, short, int>() << "\n";
  std::cout << "char, double, int: " << max_size<char, double, int>() << "\n";
}

In action at liveworkspace:

int: 4
char, short, int: 4
char, double, int: 8
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • This is the shortest and clearest solution of the available answers. Another plus point is that it does not rely on Boost. – mavam Nov 07 '12 at 02:58
  • @MatthiasVallentin: Yes, I tend to turn to `constexpr` because they are *so* more readable than meta-programming tricks. Less boilerplate, and more habit at reading functions. – Matthieu M. Nov 07 '12 at 07:44
3

I would stay away from strictly using constexpr as they are harder to compose. For instance, I'm not even sure high-order metafunctions are possible with constexpr, if so they use function pointers as template parameters which is ugly.

In general, I start with a metafunction class:

struct max_size {
  template<typename... Ts>
  struct apply : parampack_foldl::apply<boost::mpl::quote2<boost::mpl::max>, typename boost::mpl::sizeof_<Ts>::type...>;
};

Then create an alias to lessen typing:

template<typename... Ts>
using MaxSize = typename max_size::apply<Ts>::type;

Or, create a constexpr function:

template <typename... Ts>
constexpr size_t max_size() { return max_size::apply<Ts...>::type::value; }

The second step is really just a matter of style, what really matters is that you have the first which gives you the most to work with.

Pubby
  • 51,882
  • 13
  • 139
  • 180
  • I like the idea of high-order metafunctions. It would allow to write a single `max` function and pass in another metafunction to compute the size. It actually seems to work fine together with constexpr functions: http://liveworkspace.org/code/5809994d109ad35a1b68d2beb81b3ddd (The link points to Matthieu's example, slightly modified.) – mavam Nov 06 '12 at 17:41
1

For completeness, there is also the technique of inheritance:

#include <cstddef>
using std::size_t;

template<size_t ...S> struct max_size_t;

template<size_t S1, size_t S2, size_t ...Rest>
struct max_size_t<S1, S2, Rest...>
   : max_size_t<(S2 < S1) ? S1 : S2, Rest...> {
};

template<size_t S>
struct max_size_t<S> {
  static const int value = S;
}; 

template<> struct max_size_t<> {
  static const int value = 0;
};

template<typename ...T> struct max_size : max_size_t<sizeof(T)...> {};

// Using the same test-harness as Matthieu M:
#include <iostream>

int main() {
  std::cout << "int: " << max_size<int>::value << "\n";
  std::cout << "char, short, int: " << max_size<char, short, int>::value << "\n";
  std::cout << "char, double, int: " << max_size<char, double, int>::value << "\n";
  return 0;
}

Also at liveworkspace.

Although this is not a way I'd choose to implement max_size, it's very handy when the desired function is one which returns a type (highly contrived example follows):

template<typename T1, typename T2, bool B=(sizeof(T1)>sizeof(T2))> struct selector;
template<typename T1, typename T2> struct selector<T1, T2, true> { using type = T1; };
template<typename T1, typename T2> struct selector<T1, T2, false> { using type = T2; };

template<typename T1, typename ...Rest> struct largest_type;

template<typename T1, typename T2, typename ...Rest>
struct largest_type<T1, T2, Rest...>
   : largest_type<typename selector<T1, T2>::type, Rest...> {};

template<typename T1> struct largest_type<T1> { using type = T1; };

#include <iostream>
int main() {
  static const unsigned long long u = 1ULL << 63;
  std::cout << "int: " << typename largest_type<int>::type(u) << "\n";
  std::cout << "int, double: " << typename largest_type<int, double>::type(u) << "\n";
  std::cout << "short, float, long long: " << typename largest_type<short, float, long long>::type(u) << "\n";
return 0;
}

See it here.

rici
  • 234,347
  • 28
  • 237
  • 341