96
#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;
}

我预计输出将是-5and 5,但输出是-5and -5

我想知道为什么会发生这种情况?

和用途有std什么关系吗?

4

2 回答 2

92

语言规范允许实现通过在全局<cmath>命名空间中声明(和定义)标准函数,然后通过使用声明将它们带入命名空间来实现。未指定是否使用此方法std

20.5.1.2 头文件
4 [...] 然而,在 C++ 标准库中,声明(除了在 C 中定义为宏的名称)在 namespace 的命名空间范围 (6.3.6) 内stdstd未指定这些名称(包括在第 21 到 33 条和附件 D 中添加的任何重载)是否首先在全局名称空间范围内声明,然后通过显式使用声明(10.3.3)注入名称空间。

显然,您正在处理决定遵循这种方法的实现之一(例如 GCC)。即您的实现提供了::abs,而std::abs只是“引用”到::abs.

在这种情况下剩下的一个问题是,为什么除了标准之外::abs您还能够声明自己的::abs,即为什么没有多重定义错误。这可能是由某些实现(例如 GCC)提供的另一个特性引起的:它们将标准函数声明为所谓的弱符号,从而允许您用自己的定义“替换”它们。

这两个因素共同产生了您观察到的效果:弱符号替换::abs也导致替换std::abs. 这与语言标准的一致性是另一回事......无论如何,不​​要依赖这种行为 - 语言不能保证它。

在 GCC 中,可以通过以下简约示例重现此行为。一个源文件

#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和 output的“原始”定义"Hello!"


上述 20.5.1.2/4 给予的许可是为了简化<cmath>. 允许实现简单地包含 C-style <math.h>,然后重新声明函数std并添加一些 C++ 特定的添加和调整。如果上面的解释正确地描述了问题的内部机制,那么它的主要部分取决于C 风格版本的函数的弱符号的可替换性。

请注意,如果我们在上面的程序中简单地全局替换intdouble,代码(在 GCC 下)将表现“如预期” - 它会输出-5 5. 发生这种情况是因为 C 标准库没有abs(double)功能。通过声明我们自己的abs(double),我们不会替换任何东西。

但是,如果在从intwith切换后double我们也从absto切换fabs,那么原来的怪异行为将重新出现在它的全部荣耀中(输出-5 -5)。

这与上面的解释是一致的。

于 2018-06-17T16:40:07.640 回答
13

您的代码会导致未定义的行为。

C++17 [extern.names]/4:

使用外部链接声明的 C 标准库中的每个函数签名都保留给实现,以用作具有 extern "C" 和 extern "C++" 链接的函数签名,或作为全局命名空间中命名空间范围的名称。

因此,您不能创建与标准 C 库函数具有相同原型的函数int abs(int);。无论您实际包含哪些头文件,或者这些头文件是否也将 C 库名称放入全局命名空间。

abs但是,如果您提供不同的参数类型,它将允许重载。

于 2018-06-18T01:35:22.350 回答