fmod(和其他)在c++11下的不同行为,至少在Visual Studio中

Different behaviour of fmod (and others) under c++11, in Visual Studio at least

本文关键字:Studio Visual 其他 c++11 fmod      更新时间:2023-10-16

我有一些示例代码,在Visual c++ 2012下与新的c++ 11头文件的行为不同,而不是在vc++ 2010下。它涉及调用std::fmod函数时发生的情况,该函数在包含cmath时得到,并且传递的参数不是双精度,而是具有隐式转换为双精度操作符的类:

#include <cmath>
class Num {
double d_;
public:
Num(double d) : d_(d) {}
operator double() const { return d_; }
};
int main(int argc, char* argv[]) {
Num n1(3.14159265358979323846264338327950288419716939937510);
Num n2(2.0);
double result1 = fmod((double)n1, (double)n2);
double result2 = fmod((float)n1, (float)n2);
double result3 = fmod(n1, n2);
if (result2==result1) std::cout << "fmod(double, double) returns the same as fmod(float,float)" << std::endl;
if (result3==result1) std::cout << "fmod(Num, Num) returns the same as fmod(double,double)" << std::endl;
if (result3==result2) std::cout << "fmod(Num, Num) returns the same as fmod(float,float)" << std::endl;
}

令我惊讶的是,这调用了fmod的两个浮点数版本,而不是fmod的两个双精度数版本。

所以我的问题是,这是正确的行为给定的c++ 11标准?我能找到的关于行为的唯一信息是在这里的cppreference.com文档中,它说(强调我的):

如果任何实参是整型,则将其强制转换为double类型。如果任何其他实参为long double,则返回类型为long double, 否则返回类型为double

然而,Visual Studio头文件中的实现似乎实现了"否则它是一个浮点数"。

有人知道这是什么意思吗?

通过在线c++11版本的GCC运行这个例子(我没有简单的访问GCC的最新副本),它似乎调用了fmod的"double"版本,这是我天真的期望。

为清楚起见,我使用

Microsoft (R) C/c++ optimization Compiler Version 17.00.51106.1 for x86

也就是

Microsoft Visual Studio Express 2012 for Windows Desktop Version 11.0.51106.01 Update 1

这与我的这个问题有关。原因是,为了提供标准(并在您的问题中引用)所需的额外重载,VS 2012为所有2参数数学函数定义了通用函数模板。所以你实际上不叫fmod(float, float),而是叫fmod<Num>(Num, Num)

此模板化函数优于普通double版本的原因是,因为双版本将需要从Numdouble的用户定义转换,而模板版本是可直接实例化的。

但是调用fmod函数的实际基本类型是由<xtgmath.h>的这个类型特征决定的:

template<class _Ty>
    struct _Promote_to_float
    {   // promote integral to double
    typedef typename conditional<is_integral<_Ty>::value,
        double, _Ty>::type type;
    };
template<class _Ty1,
    class _Ty2>
    struct _Common_float_type
    {   // find type for two-argument math function
    typedef typename _Promote_to_float<_Ty1>::type _Ty1f;
    typedef typename _Promote_to_float<_Ty2>::type _Ty2f;
    typedef typename conditional<is_same<_Ty1f, long double>::value
        || is_same<_Ty2f, long double>::value, long double,
        typename conditional<is_same<_Ty1f, double>::value
            || is_same<_Ty2f, double>::value, double,
            float>::type>::type type;
    };

它的作用是将提升后的类型_Promote_to_float(在您的示例中还是Num,因为它只检查它是否为整型,而Num显然不是整型)检查到所有浮点类型,直到它匹配为止,但它不匹配,从而导致float的else情况。

这种错误行为的原因是,这些额外的数学重载从来没有打算为每个类型提供,而只是为内置的算术类型提供(并且含糊的标准措辞即将被修复,正如我对链接问题的回答所述)。因此,在以上解释的所有类型演绎机制中,VS 2012假设传入类型要么是内置整型,要么是内置浮点型,这当然对Num来说是失败的。所以实际的问题是VS提供了太泛型的数学函数,而它们应该只为内置类型提供重载(就像已经为1参数函数做的那样)。如链接答案中所述,我已经为此提交了一个bug。

EDIT:事实上(正如您所意识到的),即使他们遵循当前含糊不清的标准措辞,并且需要提供泛型函数模板,他们仍然应该将这些泛型参数的实际提升类型定义为double而不是float。但我认为这里的实际问题是,它们完全忽略了在整个类型转换过程中可能存在的非内置类型(因为对于内置类型,它们的逻辑工作得非常好)。

但是根据26.8节中目前模棱两可的标准措辞(尽管已经计划更改)[c]。他们确实正确地将提升的类型推断为float(根据第三种情况):

应该有额外的过载,足以确保:

  1. 如果任何与双形参对应的实参类型为long double,则所有与双形参对应的实参类型均为long double
  2. 否则,如果任何与双精度形参相对应的实参类型为double或整型,则所有与double形参被有效地转换为double。
  3. 否则,所有与双形参对应的实参都被有效地强制转换为float。

正如Christian在他的链接问答中指出的那样,这是严格阅读标准所要求的行为。

然而,你可以很容易地在所有版本中解决这个问题:

Num fmod(const Num a, const Num b)
{
    const double cvt_a = a;
    const double cvt_b = b;
    return Num(fmod(cvt_a, cvt_b));
}