#include <iostream>
#include <cmath>
/* Intentionally incorrect abs() which seems to override std::abs() */
int abs(int a) {
return a > 0? -a : a;
}
int main() {
int a = abs(-5);
int b = std::abs(-5);
std::cout<< a << std::endl << b << std::endl;
return 0;
}
我希望输出为-5
和5
,但输出为-5
和-5
。
我想知道为什么会发生这种情况?
是否与使用std
或什么有关?
答案 0 :(得分:90)
通过声明(和定义) global 命名空间中的标准函数然后将它们带入命名空间<cmath>
来实现std
的语言规范allows实现使用声明的手段。未指定是否使用此方法
20.5.1.2标题
4 [...]然而,在C ++标准库中,声明(在C中定义为宏的名称除外)在命名空间{{1}的命名空间范围(6.3.6)内}}。这些名称是否未指定(包括任何重载 在第21至33条和附件D)中添加的内容首先在全局命名空间范围内声明,然后通过显式使用声明(10.3.3)注入到命名空间std
中。
显然,您正在处理决定遵循此方法的实现之一(例如GCC)。即您的实施提供std
,而::abs
只是“引用”std::abs
。
在这种情况下仍然存在的一个问题是,为什么除了标准::abs
之外,您还可以声明自己的::abs
,即为什么没有多重定义错误。这可能是由某些实现(例如GCC)提供的另一个功能引起的:它们将标准函数声明为所谓的弱符号,从而允许您用自己的定义“替换”它们。
这两个因素共同创造了您观察到的效果:::abs
的弱符号替换也会导致替换::abs
。这与语言标准的一致性是一个不同的故事......无论如何,不要依赖于这种行为 - 语言无法保证这一点。
在GCC中,可以通过以下简约示例再现此行为。一个源文件
std::abs
另一个源文件
#include <iostream>
void foo() __attribute__((weak));
void foo() { std::cout << "Hello!" << std::endl; }
在这种情况下,您还会发现第二个源文件中#include <iostream>
void foo();
namespace N { using ::foo; }
void foo() { std::cout << "Goodbye!" << std::endl; }
int main()
{
foo();
N::foo();
}
(::foo
)的新定义也会影响"Goodbye!"
的行为。两个调用都将输出N::foo
。如果您从第二个源文件中删除"Goodbye!"
的定义,则两个调用都将调度为::foo
的“原始”定义并输出::foo
。
上述20.5.1.2/4给出的许可是为了简化"Hello!"
的实施。允许实现只包含C风格的<cmath>
,然后重新声明<math.h>
中的函数,并添加一些特定于C ++的添加和调整。如果上面的解释恰当地描述了问题的内在机制,那么它的主要部分取决于函数的 C风格版本的弱符号的可替换性。
请注意,如果我们在上述程序中仅使用std
全局替换int
,则代码(在GCC下)将“按预期”运行 - 它将输出double
。发生这种情况是因为C标准库没有-5 5
函数。通过声明我们自己的abs(double)
,我们不会替换任何内容。
但是,如果从abs(double)
切换到int
后我们也会从double
切换到abs
,原始奇怪的行为将重新出现在其完整的荣耀中(输出{{1 }})。
这与上述说明一致。
答案 1 :(得分:13)
您的代码会导致未定义的行为。
C ++ 17 [extern.names] / 4:
来自使用外部链接声明的C标准库的每个函数签名保留给实现,以用作具有extern“C”和extern“C ++”链接的函数签名,或者作为全局命名空间中的命名空间范围的名称
因此,您无法使用与标准C库函数int abs(int);
相同的原型创建函数。无论您实际包含哪些标头,或者这些标头是否也将C库名称放入全局名称空间。
但是,如果您提供不同的参数类型,则允许重载abs
。