为什么这段代码在使用int而不是使用double时失败(可能的编译器错误)?



给定以下GCC 7.3.1中非常简单的程序:

//exponentTest.cc
#include <complex> 
#include <iostream>        
int main(int argc, char **argv)
{
std::complex<double> iCpx = std::complex<double>(0,1); 
double baseline[3] = {-0.1, 0, 0};

double theta = atan2(baseline[1], baseline[0]); 
std::cout << "Theta: " << theta << std::endl;
for(size_t m =1; m < 10; m++)
{
std::complex<double> thetExp = exp(-1 * m * theta * iCpx); 
std::cout << "m(" << m << "): " << thetExp << std::endl;
}
return 0; 
}

当θ为PI时,这段代码给出了一个不正确的结果:

[stix@localhost ~]$ gcc exponentTest.cc -o expTest -lm -lstdc++
[stix@localhost ~]$ ./expTest
Theta: 3.14159
m(1): (-0.963907,0.26624)
m(2): (-0.963907,0.26624)
m(3): (-0.963907,0.26624)
m(4): (-0.963907,0.26624)
m(5): (-0.963907,0.26624)
m(6): (-0.963907,0.26624)
m(7): (-0.963907,0.26624)
m(8): (-0.963907,0.26624)
m(9): (-0.963907,0.26624)

正确答案应该是+-1交替。

在一阵哀号和咬牙切齿之后,我把问题归结为在for()循环中使用了size_t。我用一个无符号int替换了它,单元测试工作了,但是我更广泛的代码库中使用了它。沮丧的是,我最终显式地将std::exp的输入强制转换为double:

//Improved exponentTest.cc
#include <complex> 
#include <iostream> 

int main(int argc, char **argv)
{
std::complex<double> iCpx = std::complex<double>(0,1); 
double baseline[3] = {-0.1, 0, 0};

double theta = atan2(baseline[1], baseline[0]); 
std::cout << "Theta: " << theta << std::endl;
for(size_t m = 1; m < 10; m++)
{
std::complex<double> thetExp = exp(-1 * (double)m * theta * iCpx); 
std::cout << "m(" << m << "): " << thetExp << std::endl;
}
return 0; 
}

这个版本始终给出正确的结果:

[stix@localhost ~]$ gcc exponentTest.cc -o expTest -lm -lstdc++
[stix@localhost ~]$ ./expTest
Theta: 3.14159
m(1): (-1,-1.22465e-16)
m(2): (1,2.44929e-16)
m(3): (-1,-3.67394e-16)
m(4): (1,4.89859e-16)
m(5): (-1,-6.12323e-16)
m(6): (1,7.34788e-16)
m(7): (-1,-8.57253e-16)
m(8): (1,9.79717e-16)
m(9): (-1,-1.10218e-15)

问题解决了。但是,我不知道为什么会出现这个问题,而且它有点像编译器错误。最糟糕的是,如果没有在exp()函数中显式强制转换m,问题就不一致;有时它能提供正确的结果,有时却不能。

我的理解是c++标准要求编译器自动将所有int型转换为double型,例如:

double = int * double * int * double;

但是编译器显然没有这样做。

这是一个编译器的错误,或者有一些gotcha我没有考虑在混合双精度和整型?

编辑:每个注释中的指数行应该抛出一个警告,因为unsigned类型被乘以-1,然而,情况并非如此:

[stix@localhost ~]$ gcc exponentTest.cc -o expTest -lm -lstdc++ -Wall -Wextra -pedantic-errors
exponentTest.cc: In function ‘int main(int, char**)’:
exponentTest.cc:6:14: warning: unused parameter ‘argc’ [-Wunused-parameter]
int main(int argc, char **argv)
^~~~
exponentTest.cc:6:27: warning: unused parameter ‘argv’ [-Wunused-parameter]
int main(int argc, char **argv)
^~~~

相关软件版本(GCC 7使用devtoolset-7):

[stix@localhost ~]$ gcc --version
gcc (GCC) 7.3.1 20180303 (Red Hat 7.3.1-5)
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
[stix@localhost ~]$ cat /etc/redhat-release
CentOS Linux release 7.9.2009 (Core)
[stix@localhost ~]$ rpm -qa | grep devtoolset
devtoolset-7-gcc-plugin-devel-7.3.1-5.16.el7.x86_64
devtoolset-7-binutils-2.28-11.el7.x86_64
devtoolset-7-gcc-7.3.1-5.16.el7.x86_64
devtoolset-7-gcc-gfortran-7.3.1-5.16.el7.x86_64
devtoolset-7-runtime-7.1-4.el7.x86_64
devtoolset-7-libquadmath-devel-7.3.1-5.16.el7.x86_64
devtoolset-7-gcc-gdb-plugin-7.3.1-5.16.el7.x86_64
devtoolset-7-libstdc++-devel-7.3.1-5.16.el7.x86_64
devtoolset-7-gcc-c++-7.3.1-5.16.el7.x86_64

最简单的修复方法是将1更改为1.0。这就迫使计算必须使用doubles:

std::complex<double> thetExp = exp(-1.0 * m * theta * iCpx);

为什么?让我们看一下失败的表达式:

-1 * m * theta * iCpx

这里的类型是:

int * size_t * double * std::complex<double>

c++不会查看所有类型并选择"最高的"。要促进一切。相反,它从左到右逐个查看二进制操作,就好像有括号将它们分组,如:

(((int * size_t) * double) * std::complex<double>)

您会遇到麻烦,因为int * size_t首先执行。整型提升规则适用,int被转换为size_t,因为在您的平台上size_t更大。这意味着一个32位带符号整数被转换为一个64位无符号整数。

试试这个,你可以看到问题:

std::cout << (size_t) -1 << "n";

它打印:

18446744073709551615

当您将-1 * m更改为-1 * (double) m时,消除了有符号/无符号问题。-1被提升为double,即-1.0

乘法运算符(*)具有从左到右的结合性。因此,在你的参数表达式中:

exp(-1 * m * theta * iCpx)

-1 * m首先执行,并使用size_t类型完成。这将导致very(由于-1被解释为无符号常量),当转换/强制转换为double时,几乎肯定会失去精度。

在你的循环中添加一些诊断可以揭示问题的本质:

for (size_t m = 1; m < 10; m++) {
std::cout << (-1 * m) << " " << (-1 * (double)m) << std::endl; // Diagnostic!
std::complex<double> thetExp = exp(-1 * m * theta * iCpx);
std::cout << "m(" << m << "): " << thetExp << std::endl;
}

输出:

Theta: 3.14159
18446744073709551615 -1
m(1): (-0.963907,0.26624)
18446744073709551614 -2
m(2): (-0.963907,0.26624)
18446744073709551613 -3
m(3): (-0.963907,0.26624)
18446744073709551612 -4
m(4): (-0.963907,0.26624)
18446744073709551611 -5
m(5): (-0.963907,0.26624)
18446744073709551610 -6
m(6): (-0.963907,0.26624)
18446744073709551609 -7
m(7): (-0.963907,0.26624)
18446744073709551608 -8
m(8): (-0.963907,0.26624)
18446744073709551607 -9
m(9): (-0.963907,0.26624)

显式地将m转换为double强制将初始乘法(正确地)执行为双精度算术。(注意:将m转换为(带符号的)int也可以。)


注意clang-cl会发出警告:

警告:隐式转换将符号:'int'更改为'unsigned '[-Wsign-conversion]

相关内容