【问题标题】:Why does this code fail when an int is used but not when a double is used (possible compiler bug)?为什么使用 int 时此代码会失败,而使用 double 时则不会(可能的编译器错误)?
【发布时间】:2021-09-17 22:05:37
【问题描述】:

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

当 theta 为 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 的使用。我用 unsigned int 替换它,并且单元测试有效,但我使用它的更广泛的代码库没有。沮丧的是,我最终明确地将 std::exp 的输入转换为双精度:

//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++ 标准要求编译器自动将所有整数转换为双精度值,如下所示:

double = int * double * int * double;

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

这是编译器错误还是在混合双精度和整数时我没有考虑到一些问题?

编辑:对于其中一个 cmets,指数行应引发警告,因为无符号类型正在乘以 -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

【问题讨论】:

  • msize_t,这是一个无符号类型。乘以 -1 可能会给你一个警告。我建议调高你的警告级别。
  • @ChrisMM 公平点,但是由于那里有一个双精度数,编译器不应该将整个事情转换为双精度数吗?这不是 C 标准吗?
  • @KamilCuk 好的,克里斯说它至少应该发出警告,甚至在 -Wall -Wextra -pedantic-errors 上都没有
  • 您正在寻找的警告选项是-Wsign-conversion。我通过在 Clang 中使用 -Weverything 找到了这一点,收到了相关警告,注意到消息中提供的警告选项,并在 GCC 中尝试,因为 Clang 与 GCC 非常兼容。
  • 只是关于术语的注释:该表达式中不涉及演员表。有隐式转换。强制转换是您在源代码中编写的内容,用于告诉编译器进行转换。

标签: c++


【解决方案1】:

最简单的解决方法是将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

【讨论】:

  • 正如其他人指出的那样,代码不应该至少抛出一个警告,因为有符号类型被乘以无符号?
  • 我同意应该这样做。不幸的是-Wall -Wextra -pedantic-errors 没有产生警告。我不怪你错过了这个问题。
  • @stix 是 - clang-cl(在 Visual Studio 中)确实对此发出警告(请参阅我的答案的编辑)。
  • clang++ on Linux 也错过了这一点。
  • 即使该类型与int, IIRC 的大小相同,也会转换为size_t。 (关于没有负整数文字这样的东西?)
【解决方案2】:

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

exp(-1 * m * theta * iCpx)

-1 * m 被执行首先并使用size_t 类型完成。这将导致一个 非常 大的数字(由于 -1 被解释为 unsigned 常量),当转换/转换为 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 long long' [-Wsign-conversion]

【讨论】:

  • 请注意,OP 尝试使用(大概是 32 位)unsigned int for m似乎起作用。 -1 * m仍然作为 unsigned int 操作执行,但是结果值(大约 4294967295)可以准确表示为 double。但是,我很惊讶exp 函数似乎可以处理这些问题(我在我的 MSVC 上使用带有size_t 的 32 位版本进行了测试)。
猜你喜欢
  • 1970-01-01
  • 2016-02-27
  • 1970-01-01
  • 2012-01-03
  • 2016-06-28
  • 2023-02-10
  • 1970-01-01
  • 2018-04-08
  • 2011-01-02
相关资源
最近更新 更多