#include <cmath>
double log(double) {return 1.0;}
int main() {
log(1.0);
}
假设<cmath>
中的函数log()
是在全局命名空间中声明的(事实上这是未指定的,我们只是做了这个假设),那么它指的是与我们定义的log()
函数相同的函数
那么,这段代码是否违反了一个定义规则(请参阅此处,因为不需要诊断,所以这段代码可能会在某些编译器中编译,我们无法断言它是否正确)?
注意:经过最近的编辑,这不是:C++中的一个定义规则到底是什么?
典型场景
如果extern "C" double log(double)
最初是在全局命名空间中声明的,那么您已经重新声明了它并提供了一个定义。实现之前提到的extern "C"
将延续到您匹配的重新声明中。您的定义适用于属于实现的函数,这是ODR冲突。
关于UB的表现:将log
视为弱链接符号显然是常见的。根据ABI规则,您的实现将覆盖libc.so
。
(如果实现不执行extern "C"
,那么基本上还是一样的。)
其他可能的情况
如果在namespace std
中声明了log
,然后将其带入全局命名空间,那么您的声明将与之冲突。(实际上,从技术上讲,using
声明就是一个声明。)诊断出了此错误。
违反假设的场景
那么它指的是与我们定义的的
log
函数相同的函数
实现将<cmath>
名称放入全局命名空间的一种方法是在namespace std
内声明extern "C"
函数,然后执行using namespace std
,并确保在包含任何标准标头时,这始终是第一件事。由于using namespace
不是"粘性"的——它只适用于指定名称空间中的先前声明——因此标准库的其余部分将不可见。(这不会在全局命名空间中声明名称,但标准只说"放置在全局命名空间范围内"。)
在这样的实现中,您的声明将隐藏标准名称,并使用新的损坏名称(比如_Z3logd
而不是简单的log
)和新的完全限定名称(::log
而不是::std::log
)声明一个新函数。那么就不会有ODR冲突(除非某个内联函数在一个TU中使用一个log
,而在另一个TU中使用另一个)。
以下内容介绍了OP的前一个版本。我将其保留在此处,以防将来的读者遇到类似的查询
我猜测两个名称引用同一实体,当且仅当它们具有相同的声明性区域,其中"声明性区域"的概念在标准中定义[…]这种猜测正确吗?标准中有任何文字支持这一点吗?
通俗地说,它被称为变量隐藏或阴影。标准几乎一字不差地说出了你所说的话。§3.3.10当前C++17标准草案中的¶1:
可以通过嵌套声明性区域或派生类中相同名称的显式声明来隐藏名称
那么,此代码是否违反了一个定义规则(请参阅此处,由于不需要诊断,因此此代码可能在某些编译器中编译,我们无法断言它是正确的)?
我不希望这样。标准要求所有cheader
标头(尤其是cmath
)在std
命名空间中引入它们的符号。一个也将其注入全局命名空间的实现是符合标准的(因为标准将该位保留为未指定),但我会发现它的形式不好。你说得对,它可能发生。现在,如果您要包含math.h
(反对明智的建议),那么这肯定会导致违反一个定义规则。
小心。ODR只涉及将包含在生成的程序中的定义。这意味着is不涉及库中可能存在的符号,因为(正常)链接器不会加载整个库,而只加载解析符号所需的部分。例如,在此代码中:
#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
的对象模块,并将其明确地链接到程序中,情况就会有所不同。因为对象模块总是包含在生成的程序中,而库中的对象模块只有在解析未定义的符号时才有条件地包含在内。
标准参考:
C++11的草案n3337或C++14的草案n4296(或上一次修订的草案n4618)在第2.2段中明确规定了翻译阶段[lex.Phase]:
§9.所有外部实体引用都已解析链接库组件以满足外部引用到当前翻译中未定义的实体。所有这样的翻译器输出都被收集到一个程序中图像,该图像包含在其执行环境中执行所需的信息。
如图所示,代码只使用一个翻译单元,并且其中已经定义了log
,因此不会使用库中的定义。