Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++11: Variadic Homogeneous Non-POD Template Function Arguments?

Tags:

c++

c++11

How would you write a template function that takes a variable number of homogeneous non-POD function arguments in C++11?

For example suppose we wanted to write a min function for any type that defines the less than "operator<" as follows:

// pseduo-code...

template<class T...>
T min(T x1, T x2, ..., T xn)
{
    T lowest = x1;

    for (T x : {x2,...,xn})
       if (x < lowest)
           lowest = x;

    return lowest;
}

The above is illegal C++11, how would you write it legally?

like image 997
Andrew Tomazos Avatar asked Mar 18 '12 21:03

Andrew Tomazos


3 Answers

Homogeneous? Just use std::initializer_list.

template <typename T>
T min_impl(std::initializer_list<T> values)
{
    return *std::min_element(values.begin(), values.end());
}

...

return min_impl({8, 5, 4, 1, 6});

(As @Jesse noted, this is the equivalent to std::min in the standard library.)

If you don't like the extra braces, make a variadic template that forwards to the initializer list implementation:

template <typename... T>
auto min(T&&... args) -> decltype(min_impl({std::forward<T>(args)...}))
{
    return min_impl({std::forward<T>(args)...});
}

...

return min(8, 5, 1, 4, 6);
like image 133
kennytm Avatar answered Nov 17 '22 23:11

kennytm


First, variadic templates don't include a way to say 'a variable number of arguments of a single type'. When you use variadic templates you get a parameter pack which is a set of zero or more arguments, each with a possibly unique type:

template<typename... Ts> void foo(Ts... ts);

the ... token only has defined meanings for these parameter packs (and vararg functions, but that's beside the point). So you can't use it with non-parameter packs:

template<typename T> void foo(T... t); // error

template<typename T> void foo(T t...); // error

Second, once you have a parameter pack, you can't just iterate over the parameters the way you're showing with the range-based for-loop. Instead you have to write your algorithms in a functional style, using parameter pack expansion to 'peel off' parameters from the parameter pack.

// single argument base case
template<typename T>
void foo(T t) {
    std::cout << t;
}

template<typename T,typename... Us>
void foo(T t,Us... us) {
   foo(t) // handle first argument using single argument base case, foo(T t)
   foo(us...); // 'recurse' with one less argument, until the parameter pack
    // only has one argument, then overload resolution will select foo(T t)
}

Although variadic templates don't directly support what you want, you can use enable_if and use the 'SFINAE' rule to impose this constraint. First here's a version that without the constraint:

#include <type_traits>
#include <utility>

template<class T>
T min(T t) {
    return t;
}

template<class T,class... Us>
typename std::common_type<T,Us...>::type
min(T t,Us... us)
{
    auto lowest = min(us...);
    return t<lowest ? t : lowest;
}

int main() {
    min(1,2,3);
}

And then apply enable_if to ensure that the types are all the same.

template<class T,class... Us>
typename std::enable_if<
    std::is_same<T,typename std::common_type<Us...>::type>::value,
    T>::type
min(T t,Us... us)
{
    auto lowest = min(us...);
    return t<lowest ? t : lowest;
}

The modified implementation above will prevent the function from being used any time the arguments aren't all exactly the same according to is_same.

You're probably better of not using these tricks if you don't have to. Using an initializer_list as KennyTM's suggests is probably a better idea. In fact if you're really implementing min and max then you can save yourself the trouble because the standard library already includes overloads that take an initializer_list.


How does is_same<T,typename common_type<Us...>::type> work?

Because there's a single argument version of min() the variadic version is selected only when there are two or more parameters. This means that sizeof...(Us) is at least one. In the case where it is exactly one, common_type<Us...> returns that type single type, and is_same<T,common_type<Us...>> ensures that the two types are the same.

The variadic implementation of min() calls min(us...). So long as this call only works when all the types in Us... are the same we know that commont_type<Us...> tells up what that type is, and is_same<T,common_type<Us...>> ensures that T is also that same type.

So we know that min(a,b) only works if a and b are the same type. And we know that min(c,a,b) calls min(a,b) so min(c,a,b) can only be called if a and b are the same type and additionally if c is also the same type. min(d,c,a,b) calls min(c,a,b) so we know that min(d,c,a,b) can only be called if c, a, and b are all the same type, and additionally if d is also the same type. Etc.

like image 4
bames53 Avatar answered Nov 18 '22 00:11

bames53


This is a little convoluted but that's what you get if you want heterogeneous arguments:

#include <iostream>
#include <type_traits>

template<typename F, typename T, typename Arg>
auto fold(F f, T&& t, Arg&& a) 
  -> decltype(f(std::forward<T>(t), std::forward<Arg>(a)))
{ return f(std::forward<T>(t), std::forward<Arg>(a)); }

template<typename F, typename T, typename Head, typename... Args>
auto fold(F f, T&& init, Head&& h, Args&&... args) 
  -> decltype(f(std::forward<T>(init), std::forward<Head>(h)))
{ 
  return fold(f, f(std::forward<T>(init), std::forward<Head>(h)), 
              std::forward<Args>(args)...); 
}

// polymorphic less
struct p_less {
  template<typename T, typename U>
  typename std::common_type<T, U>::type 
  operator()(T&& t, U&& u) const {
    return t < u ? t : u;
  }
};

// heterogeneous arguments possible
template<typename Head, typename... Args>
auto min(Head&& h, Args&&... args) -> typename std::common_type<Head, Args...>::type
{
  return fold(p_less(), std::forward<Head>(h), 
              std::forward<Args>(args)...);
}


// only considers homogeneous arguments
template<typename Head, typename... Args>
auto hmin(Head&& h, Args&&... args) -> Head
{
  return fold([](Head x, Head y) -> Head { return x < y ? x : y; }, 
              std::forward<Head>(h), std::forward<Args>(args)...);
}

int main()
{

  double x = 2.0, x2 = 3.0;
  int y = 2;

  auto d1 = min(3, 4.0, 2.f, 6UL);
  auto d2 = min(x, y);
  auto d3 = hmin(x, x2);
  auto b = hmin(3, 2, 7, 10);

  std::cout << d1 << std::endl;
  std::cout << d2 << std::endl;
  std::cout << d3 << std::endl;

  std::cout << b << std::endl;
  return 0;
}

The first version is a lot more interesting however. common_type looks for the type that all arguments can be coerced to. This is necessary because a function with heterogeneous arguments returning either of them needs to find this type. However, this could be circumvented using boost::variant but for built-in types this is rather pointless as they have to be coerced to be compared anyway.

like image 1
pmr Avatar answered Nov 18 '22 01:11

pmr