C++11:变分齐次非POD模板函数参数

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

本文关键字:POD 函数 参数 C++11      更新时间:2023-10-16

如何在C++11中编写一个采用可变数量同质非POD函数参数的模板函数?

例如,假设我们想为定义小于"运算符<"的任何类型编写一个min函数,如下所示:

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

以上是非法的C++11,你会如何合法地写它?

同质只需使用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});

(正如@Jesse所指出的,这相当于标准库中的std::min。)

如果你不喜欢额外的大括号,那么制作一个可变模板,将其转发到初始值设定项列表实现:

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

首先,可变模板不包含"单个类型的可变数量的参数"。当你使用可变模板时,你会得到一个参数包,它是一组零个或多个参数,每个参数都有一个可能唯一的类型:

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

...令牌仅对这些参数包(和vararg函数,但这与重点无关)具有定义的含义。所以你不能将它与非参数包一起使用:

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

其次,一旦有了参数包,就不能像在基于范围的for循环中显示的那样迭代参数。相反,你必须以函数风格编写算法,使用参数包扩展从参数包中"剥离"参数。

// 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)
}

尽管可变模板不直接支持您想要的内容,但您可以使用enable_if并使用"SFINAE"规则来施加此约束。首先,这里有一个没有限制的版本:

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

然后应用enable_if以确保所有类型都相同。

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

根据is_same,只要参数不完全相同,上述修改后的实现将阻止函数被使用。

如果没有必要的话,你最好不要使用这些技巧。按照KennyTM的建议,使用initializer_list可能是一个更好的主意。事实上,如果你真的实现了min和max,那么你就可以省去麻烦了,因为标准库已经包含了使用initializer_list的重载。


is_same<T,typename common_type<Us...>::type>是如何工作的

因为min()有一个单参数版本,所以只有当有两个或多个参数时才选择可变版本。这意味着sizeof...(Us)至少是一个。在它恰好是一个的情况下,common_type<Us...>返回类型single类型,而is_same<T,common_type<Us...>>确保这两个类型相同。

min()的变址实现调用min(us...)。只要这个调用只有在Us...中的所有类型都相同时才有效,我们就知道commont_type<Us...>告诉什么是该类型,而is_same<T,common_type<Us...>>确保T也是同一类型。

因此,我们知道min(a,b)只有当ab是同一类型时才有效。我们知道min(c,a,b)调用min(a,b),所以只有当ab是同一类型时,才能调用min(c,a,b),此外,如果c也是同一类型。min(d,c,a,b)调用min(c,a,b),因此我们知道只有当cab都是同一类型时,才能调用min(d,c,a,b),此外,如果d也是同一类型。等等

这有点复杂,但如果你想的话,这就是你想要的异构自变量:

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

然而,第一个版本要有趣得多。common_type查找所有参数都可以强制使用的类型必要,因为具有异类参数的函数返回他们中的任何一个都需要找到这种类型。然而,这可能是使用boost::variant规避,但对于内置类型,这是这是毫无意义的,因为无论如何都要强迫他们进行比较。

另一种可能性:

template <typename T, typename... T2>
T min(T x1, T2... rest);
template <typename T>
T min(T x)
{
  return x;
}
template <typename T, typename... T2>
T min(T x1, T2... rest)
{
  return std::min (x1, min (rest...));
}

虽然这似乎并不能强制实现一致性,但使用例如intlong进行调用会产生编译错误。不幸的是,我不太擅长用标准语来判断它是否应该是这样。