类型检测:使用可变参数来正确实现计算平均值的函数

时间:2017-06-07 06:01:59

标签: c++ c++11 templates variadic-templates template-meta-programming

我试图了解如何正确实现一个函数来计算数学样本均值,具有两个初始必需特征:

1)使用可变参数。

2)不使用两个函数来完成这项工作,即不使用调用函数,而是使用实际执行计算的第二个函数。

3)功能应尽可能通用

我非常清楚已经提出了一个非常类似的问题:Calculate the average of several values using a variadic-template function然而,虽然这个问题的公认答案似乎已经教会了OP怎么做他不知道的小部分,它提供了一个实际上错误且无法编译的代码。

所以,我自己的第一次尝试就是这样:

template <class... Args>
double mean(const Args & ... args)
{
    auto total = 0;
    for (auto value : { ...args })
    {
        total += value;
    }
    return (double)total / sizeof...(args);
}

这里的问题是,在行auto total = 0;中,编译自然无法自动识别对象total应具有的类型。

然后,我的第二次尝试:

template <class T, class... Args>
T mean(const T &t, const Args & ... args)
{
    T total = 0;
    for (auto value : { args... })
    {
        total += value;
    }
    return (T)(total / sizeof...(args));
}

该版本存在以下问题。如果调用者使用混合类型的参数调用函数,它不起作用,例如在mean(1, 2.5)中,其中第一个参数被自动检测为int,第二个参数被检测为double

我能够通过以下方式解决这个问题:

template <class T, class... Args>
T mean(const T &t, const Args & ... args)
{
    size_t argsSize = sizeof...(args);
    T total = t;
    T arg_array[] = { args... };
    for (size_t i = 0; i< argsSize; i++)
    {
        total += (T)arg_array[i];
    }
    return (T)(total / argsSize) ;
}

即使传递的参数属于不同类型(当然,只要这些类型可以转换为T),这个就可以工作。然而,现在的问题是该函数仅适用于至少两个参数。如果在mean(3.14)中调用它,它应该返回3.14,它实际上会引发错误,因为T arg_array[] = { args... }无法编译,因为不可能使用size {创建静态数组{1}}。当然,我可以用它替换动态数组,但是这会让我每次调用函数时都要做一次内存分配和一次内存释放 - 这是一种不可接受的浪费。

那么,实现这样一个避免上述问题并且遵循我的两个初始条件的函数的正确方法是什么?

3 个答案:

答案 0 :(得分:4)

使用std::common_type_t

template<class... Args> constexpr auto mean(Args... args) {
    std::common_type_t<Args...> total(0);
    for(auto value : {std::common_type_t<Args...>(args)...}) total += value;
    return total/sizeof...(args);
}

答案 1 :(得分:2)

在C ++ 17中使用fold expressions

template<typename... Args>
constexpr decltype(auto) mean(Args&&... args)
{
    return (... + std::forward<Args>(args)) / sizeof...(Args);
}

std::forward适用于支持移动的一些bignum类型。

在C ++ 17之前,需要手动折叠,但这需要为基础案例重载mean,这似乎不是你想要的。

答案 2 :(得分:0)

为了解决&#34;该功能仅适用于至少两个参数&#34;你可以创建一个简单的类型特征来提取第一个类型

template <typename T0, typename ...>
struct firstType
 { using type = T0; };

或者,如果您愿意(并且,恕我直言,更好),请遵循Diego91b的std::common_type_t建议(+1)。

您可以在C ++ 11中编写mean(),如下所示

template <typename ... Args>
typename firstType<Args...>::type mean (Args const & ... args)
// or typename std::common_type<Args...>::type mean (Args const & ... args)
 {
   using unused = int[];

   typename firstType<Args...>::type  total { 0 };
   // or typename std::common_type<Args...>::type  total { 0 };

   (void)unused { (total += args, 0)... };

   return total / sizeof...(args);
 }

在C ++中,14可以简化(并在constexpr中转换)如下

template <typename ... Args>
constexpr auto mean (Args const & ... args)
 {
   using unused = int[];

   //typename firstType<Args...>::type  total { 0 };
   std::common_type_t<Args...>  total { 0 };

   (void)unused { (total += args, 0)... };

   return total / sizeof...(args);
 }

在C ++ 17中,您可以使用折叠表达式(如Passer By所建议的那样)并且变得非常简单

template <typename ... Args>
constexpr typename firstType<Args...>::type mean (Args const & ... args)
// or constexpr auto mean (Args const & ... args)
{
    return (... + args) / sizeof...(Args);
}

以下是完全可编译的C ++ 11示例

#include <iostream>
#include <type_traits>

template <typename T0, typename ...>
struct firstType
 { using type = T0; };

template <typename ... Args>
typename firstType<Args...>::type mean (Args const & ... args)
// or typename std::common_type<Args...>::type mean (Args const & ... args)
 {
   using unused = int[];

   typename firstType<Args...>::type  total { 0 };
   // or typename std::common_type<Args...>::type  total { 0 };

   (void)unused { (total += args, 0)... };

   return total / sizeof...(args);
 }

int main()
 {
   std::cout << mean(2, 5.5) << std::endl; // print 3 (3.75 with common_type)
   std::cout << mean(5.5, 2) << std::endl; // print 3.75
   std::cout << mean(2) << std::endl;      // print 2
   // std::cout << mean() << std::endl; compilation error
 }

- 编辑 -

OP问

  

你知道为什么你的解决方案给出了3,而使用common_type在调用mean(2,5.5)时得到3.75?

不确定

使用mean(2, 5.5)致电firstType时,第一个参数的类型是2的类型,即int。因此,total被声明为int,当5.5被添加到总计时,会转换为int,因此变为5,广告被添加到2 }。 total变为7并除以2,因此(整数除法)变为3

在计票上,如果您致电mean(5.5, 2),则firstType的{​​{1}}类型为5.5。因此double成为total,添加double2并获得5.5。最后一个部门(7.5)是7.5 / 2部门,提供double

使用3.75致电mean(2, 5.5)(或mean(5.5, 2))时,std::common_type_tint之间的常见类型为double;因此double定义为total,函数返回double 7.5 / 2

- 编辑2 -

OP问

  

为什么使用未使用的必须专门分配3.75是否有特殊原因?我尝试将其更改为int[],这似乎会使函数更通用,但它不会编译。此外,它不适用于typename std::common_type<Args...>::type

double[]变量是......未使用的。 接受声明的计数只允许在其初始化时添加unused total的值,将其解包。

诀窍是逗号:观察

args

逗号丢弃左值(计算,因此(total += args, 0)... 被修改,但已丢弃)并返回正确的值。那是零。整数零。因此totalint[]

最终结果是unused初始化为{ 0, 0, 0, ... }

没有必要用不同类型的东西来改造它;这只是使用args包的技巧。

如果你愿意,你可以这样做(例如)float;如下

using unused = float[];

(void)unused { (total += args, 0.0f)... };

(观察0.0f)但是......为什么我们应该这样做?