使用C库中未通过std命名空间的符号是否有GCC警告?

15

请考虑以下(有bug的)C++代码:

#include <cmath>
#include <cstdlib>

#include <iostream>

int main() {
    if (abs(-0.75) != 0.75) {
        std::cout << "Math is broken!\n";
        return 1;
    } else {
        return 0;
    }
}
这段代码有缺陷,因为它调用了abs(意味着::abs),而不是std::abs。根据实现情况,::abs可能不存在,也可能是C的abs,或者它可能是一个包含double版本的重载集合,就像std::abs一样。
在Linux使用Clang时,至少在我的环境中,结果为第二种情况:C的abs。这会引发两个警告,即使没有明确启用任何警告。
<source>:7:9: warning: using integer absolute value function 'abs' when argument is of floating point type [-Wabsolute-value]
    if (abs(-0.75) != 0.75) {
        ^
<source>:7:9: note: use function 'std::abs' instead
    if (abs(-0.75) != 0.75) {
        ^~~
        std::abs
<source>:7:13: warning: implicit conversion from 'double' to 'int' changes value from -0.75 to 0 [-Wliteral-conversion]
    if (abs(-0.75) != 0.75) {
        ~~~ ^~~~~

在GCC上,我在不同的环境中得到了不同的结果,但我还没有弄清楚环境的哪些细节是相关的。更常见的选项是调用C语言的abs函数。然而,即使使用-Wall -Wextra -pedantic编译参数,它也不会给出任何警告。我可以通过-Wfloat-conversion参数强制发出警告,但是这会导致我在代码库的其他部分获得太多的误报(也许我应该解决这个问题,但那又是另一个问题):

<source>: In function 'int main()':
<source>:7:18: warning: conversion to 'int' alters 'double' constant value [-Wfloat-conversion]
     if (abs(-0.75) != 0.75) {
                  ^

当我通过全局命名空间使用库函数时,是否有一种方法可以在命名空间std中的版本是重载时发出警告?


1
不要垃圾标签。C语言没有std名称空间。 - too honest for this site
@Olaf 你说得对。这只是相关的,因为C++包括了大量的C标准库,但并不是以一种使得C专业知识对回答这个问题有用的方式。我很抱歉。 - Daniel H
在C++代码中使用C函数并不意味着需要加上C标签、标准库或任何其他库。否则,每个程序都需要这个标签,因为甚至PHP或Python也使用这些函数。 - too honest for this site
@Olaf 我的意思是,如果在获取 MCVE 的过程中,生成的代码是有效的 C 代码(并且展示了相同的问题),即使原始代码是 C++,那么我认为使用 C 标签是合适的。如果像我的问题一样,MCVE 取决于 C++ 特性,则该标签不合适。 - Daniel H
@DanielH:差异非常微妙,自C++11以来,这两种语言已经越来越远了。未来还会有更多的变化。只需标记您使用的语言,不要重复标记(除非它确实涵盖了两者,例如不仅仅是ABI,如extern "C"所示)。 - too honest for this site
显示剩余7条评论
3个回答

3
这里有一个解决方案。虽然我不是很满意,但它可能适合你:
namespace DontUseGlobalNameSpace {
// put all std functions here you want to catch
int abs(int x);
}
using namespace DontUseGlobalNameSpace;

现在,如果你不带任何限定地使用abs()函数,你会得到一个“符号不明确”的错误提示。


那样做是可行的,但需要枚举所有相关的方法。浏览cppreference或标准文档可能不会花费超过一个小时,但仍然不是理想的选择,也有可能会遗漏某些内容。 - Daniel H
还需要在所有源文件的顶部包含dontuseglobalnamespace.hpp,我可以使用sed或其他方法来完成,但这肯定不是一个理想的解决方案。特别是在持续进行中;现在修复所有问题比确保随着代码更新它们仍然得到修复更容易。 - Daniel H
是的,这就是为什么我对这个解决方案不满意的原因 :) 编译器通常有一个包含文件的选项。例如,gcc 有 -include 选项,可以将一个文件包含在文件开头一样。 - geza
哦,不错,我不知道那个。这可能足以让我使用这个解决方案,尽管现在我会等待看看是否有更好的答案。 - Daniel H
好的,既然周末没有人提出更好的想法,我可能会尝试这个。如果实践证明有效,我会接受它。 - Daniel H

2
这将是困难的。GCC <cmath> 头文件仅包含 <math.h>#undefs 其宏(以防万一),并将 C++ 函数定义为内联函数,这些函数使用了来自 <math.h> 的标识符。实际上,大多数函数都涉及编译器内置函数:例如,std::abs 使用 __builtin_abs 而不是 ::abs 进行定义。
由于 <cmath> 和你的“错误程序”在同一个翻译单元中,很难看到可见性如何分离:如何允许 <cmath> 中的内联函数使用 <math.h> 的东西,而你的代码则不能。
好吧,有以下方式:<cmath> 必须被重写,以便为其从 <math.h> 中需要的任何内容提供本地范围的声明,并且实际上不包含该头文件。
相反,我们可以准备一个头文件,重新声明我们不想要的函数,使用 __attribute__ ((deprecated)) 标记:
// put the following and lots of others like it in a header:
extern "C" int abs(int) throw () __attribute__ ((deprecated));
#include <cmath>
#include <cstdlib>

#include <iostream>

int main() {
  if (abs(-0.75) != 0.75) {
    std::cout << "Math is broken!\n";
    return 1;
  } else {
    return 0;
  }
}

现在:

$ g++ -Wall  buggy.cc
buggy.cc: In function ‘int main()’:
buggy.cc:9:7: warning: ‘int abs(int)’ is deprecated [-Wdeprecated-declarations]
   if (abs(-0.75) != 0.75) {
       ^~~
In file included from /usr/include/c++/6/cstdlib:75:0,
                 from buggy.cc:4:
/usr/include/stdlib.h:735:12: note: declared here
 extern int abs (int __x) __THROW __attribute__ ((__const__)) __wur;
            ^~~
buggy.cc:9:16: warning: ‘int abs(int)’ is deprecated [-Wdeprecated-declarations]
   if (abs(-0.75) != 0.75) {
                ^
In file included from /usr/include/c++/6/cstdlib:75:0,
                 from buggy.cc:4:
/usr/include/stdlib.h:735:12: note: declared here
 extern int abs (int __x) __THROW __attribute__ ((__const__)) __wur;
            ^~~

一个链接器警告会更简单。我尝试过这样做;问题在于,这个测试程序实际上并没有生成对abs的外部引用(即使在<cmath>中有一个#undef abs)。该调用被内联,因此逃避了链接器警告。
更新:
根据DanielH的评论,我想到了一个更好的方法,允许使用std::abs但阻止使用abs
#include <cmath>
#include <cstdlib>
#include <iostream>

namespace proj {
  // shadowing declaration
  int abs(int) __attribute__ ((deprecated));

  int fun() {
    if (abs(-0.75) != 0.75) {
      std::cout << "Math is broken!\n";
      return 1;
    } else {
      return std::abs(-1); // must be allowed
    }
  }
}

int main() {
  return proj::fun();
}

可以使用简单的命名空间。此外,我们不需要deprecated属性;我们可以将abs声明为不兼容的函数或非函数标识符:

#include <cmath>
#include <cstdlib>
#include <iostream>

namespace proj {
  // shadowing declaration
  class abs;

  int fun() {
    if (abs(-0.75) != 0.75) {
      std::cout << "Math is broken!\n";
      return 1;
    } else {
      return std::abs(-1); // must be allowed
    }
  }
}

int main() {
  return proj::fun();
}

$ g++ -std=c++98 -Wall  buggy.cc -o buggy
buggy.cc: In function ‘int proj::fun()’:
buggy.cc:10:18: error: invalid use of incomplete type ‘class proj::abs’
     if (abs(-0.75) != 0.75) {
                  ^
buggy.cc:7:9: note: forward declaration of ‘class proj::abs’
   class abs;
         ^~~
buggy.cc:16:3: warning: control reaches end of non-void function [-Wreturn-type]
   }
   ^

采用这种方法,我们只需要一个名字列表,并将它们转储到某个提供此功能的标题中:
int abs, fabs, ...; // shadow all of these as non-functions

我在g++命令行中使用了-stdc++98,强调这只是旧版C++namespace语义的运作。


但是GCC实现的cmath将它们放入了std命名空间中,使用using ::abs;等。弃用标记也会继承;如果您将return 1;替换为return std::abs(-1);,它仍然会警告有关弃用的信息。 - Daniel H
@DanielH 我明白了:它是通过 using 将它们引入,然后只是添加一些重载。哎呀!因此,主要的那个继承了警告,只有这些重载是安全的。这相当糟糕。 - Kaz
所以,除非对g++头文件进行了清理,否则只有在使用::absabs时才需要发出警告,但即使std::abs是前两者的别名,也不需要在使用std::abs时发出警告。 - Kaz
1
值得注意的是,abs(int) 函数在 cstdlib 库中,而不是 cmath 库中。 - rici
@rici 在C++17之前是这样的,我的例子有点人为,因为它包含了两个。但在一个真正的程序中,一些C++头文件可能会包含<cstdlib>,即使你不需要显式地这样做。 - Daniel H
显示剩余4条评论

0
此代码可用于检测特定环境中是否存在陷阱:
double (*)(double) = &::abs; // fails if you haven't included math.h, possibly via cmath

但这并不能帮助你发现自己掉进陷阱的地方。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接