从标准库中重新定义一个函数是否违反了一个定义规则?

Does redefining a function from the standard library violate the one-definition rule?

#include <cmath>

double log(double) {return 1.0;}
int main() {
  log(1.0);
}

假设<cmath>中的函数log()是在全局命名空间中声明的(实际上这是未指定的,我们只是做这样的假设),那么它引用与log()我们定义的函数。
那么这段代码是否违反了一个定义规则(参见here,因为不需要诊断,这段代码可能会在某些编译器中编译,我们无法断言它是否正确)?

注意:经过最近的编辑,这不是以下的副本:What exactly is One Definition Rule in C++?

以下内容涉及 OP 的先前修订版。我把它留在这里以防将来的读者来到这里提出类似的查询。

I guess that two names refer to the same entity if and only if they have the same declarative region, where the concept "declarative region" is defined in the standard [...] Is this guess correct? Is there any word in the standard supporting this?

它通俗地称为变量隐藏或阴影。标准几乎逐字逐句地表达了您所说的内容。 §3.3.10 ¶1在当前的C++17标准草案中:

A name can be hidden by an explicit declaration of that same name in a nested declarative region or derived class


So does this code violate the one-definition rule (see here, since no diagnostic required, this code may compile in some compiler and we cannot assert it is correct)?

我不希望如此。该标准要求 all cheader headers(尤其是 cmath)在 std 命名空间中引入它们的符号。也将其注入全局命名空间的实现是符合标准的(因为标准将该位保留为未指定),但我会发现它的形式不佳。你是对的,可能发生。现在,如果您要包含 math.h(违背明智的建议),那肯定会导致违反单一定义规则。

典型场景。

如果 extern "C" double log(double) 最初是在全局命名空间中声明的,那么您已经重新声明它并提供了定义。该实现先前提到的 extern "C" 会延续到您的匹配重新声明中。您的定义适用于属于实现的函数,并且违反了 ODR。

至于UB的表现形式:把log当作一个弱链接符号显然是很常见的。根据 ABI 规则,您的实现会覆盖 libc.so

(如果实现不做extern "C",基本上还是一样。)

其他可能的情况。

如果在namespace std中声明了log,然后又将其带入了全局命名空间,那么你的声明就会与之冲突。 (实际上,using 声明在技术上是一个声明。)诊断出此错误。

Assumption-violating场景。

then it refers to the same function as the log function we defined

实现将 <cmath> 名称放入全局命名空间的一种方法是在 namespace std 中声明 extern "C" 函数,然后执行 using namespace std,并确保当包含任何标准 header 时,这总是作为第一件事发生。由于 using namespace 不是 "sticky"——它只适用于指定名称空间中的前面声明——标准库的其余部分将不可见。 (这不会 声明 全局名称空间中的名称,但标准只说 "placed within the global namespace scope.")

在这样的实现中,您的声明将隐藏标准声明并声明一个具有新的错位名称的新函数(比如 _Z3logd 而不是简单的 log)和一个新的 fully-qualified 名称(::log 而不是 ::std::log)。那么就不会有 ODR 违规(除非某些内联函数在一个 TU 中使用一个 log 而在另一个 TU 中使用另一个)。

当心。 ODR 仅涉及将包含在生成的程序中的定义。这意味着它不涉及库中可能存在的符号,因为(正常)linker 不会加载整个库,而只会加载解析符号所需的部分。例如在这段代码中:

#include <cmath>

double log(double) {return 1.0;}

int main()
{
    log(1.0);
}

没有违反ODR:

  • 要么来自 C 标准库的日志符号只包含在 std 命名空间中,根本没有冲突
  • 或者也包含在全局命名空间

在后一种情况下,声明 double log(double) 与 cmath 中的声明不冲突,因为它们是相同的。并且由于符号 log 已经被定义,它在标准库中的 定义 将不会包含在程序中。因此,程序中只存在 log 函数的一个定义,即 double log(double) {return 1.0;}.

如果您从数学库中提取包含 log 的目标模块,并在您的程序中明确地 link 它,情况就会有所不同。因为目标模块总是包含在生成的程序中,而库中的目标模块仅在解析未定义符号时有条件地包含。


标准参考文献:

C++11 草案 n3337 或 C++14 草案 n4296(或最新修订版 n4618)在第 2.2 段翻译阶段中是明确的[lex.phases]:

§9. All external entity references are resolved. Library components are linked to satisfy external references to entities not defined in the current translation. All such translator output is collected into a program image which contains information needed for execution in its execution environment.

如图所示,代码仅使用一个翻译单元,并且log已在其中定义,因此不会使用库中的定义。