为什么C++链接器允许未定义的函数?

30

这段C++代码也许令人惊讶地打印出1

#include <iostream>

std::string x();

int main() {

    std::cout << "x: " << x << std::endl;
    return 0;
}

x是一个函数原型,看起来像是一个函数指针,在C++标准第4.12节的布尔转换中说:

4.12 布尔转换 [conv.bool] 1 算术类型、未作用域的枚举类型、指针类型或成员指针类型的prvalue可以转换为bool类型的prvalue。零值、空指针值或空成员指针值转换为false;任何其他值转换为true。对于直接初始化(8.5),std::nullptr_t类型的prvalue可以转换为bool类型的prvalue;结果值为false。

然而,x从未绑定到一个函数。正如我所预期的,C链接器不允许这样做。但是在C++中,这根本不是问题。有人能解释一下这种行为吗?


6
这是一种不需要诊断即可出现 ODR 违规的情况,意味着您的代码存在未定义行为。 - T.C.
4
这是一个不正确的NDR,因此根据[intro.compliance]/2的规定(“如果程序违反了不需要诊断的规则,则本国际标准不对实现该程序的要求做出任何规定”),它基本上是未定义行为([defns.undefined],“本国际标准没有规定的行为”)。 - T.C.
1
@T.C. 嗯,我想是这样的。不过让我想知道为什么他们要在第一次区分“格式错误,无需诊断”和“行为未定义”,虽然我相信肯定有人会问这个问题... - Lightness Races in Orbit
2
@LightnessRacesinOrbit 我认为这是ODR违规的特殊类别。 - M.M
1
@Columbo 那是显然不正确的,大多数类型的未定义行为只有在遇到其语句时才会被触发。(例如:int f() { return 1 / 0; } 只要 f() 从未被调用就可以了)。 - M.M
显示剩余7条评论
3个回答

28
这里发生的情况是函数指针被隐式转换为bool。这是由[conv.bool]规定的:

零值、空指针值或空成员指针值被转换为false;任何其他值都被转换为true

其中,“空指针值”包括空函数指针。由于从函数名的衰减得到的函数指针不能为null,因此结果为true。通过在输出命令中包含<< std::boolalpha可看到这一点。
下面的代码在g++中会导致链接错误:(int)x;
关于是否允许这种行为,C++14 [basic.odr.ref]/3说:

如果函数的名称出现在可能求值的表达式中,则该函数是odr-used, 如果它是唯一的查找结果或一组重载函数中选择的成员[...]。

这确实涵盖了这种情况,因为输出表达式中的x被查找到上面的x的声明,并且那是唯一的结果。然后,在/4中,我们有:

每个程序必须包含该程序中对其进行odr-used的每个非内联函数或变量的正好一个定义;不需要诊断。

因此,程序是有问题的,但不需要进行诊断,这意味着程序的行为完全未定义。
顺便说一下,这个条款意味着x();也不需要链接错误,然而从实现质量的角度来看,那样很愚蠢。在这里,g++选择的方式对我来说似乎是合理的。

1
@Columbo N3936 实际上(据我所知)与 C++14 完全相同。 - M.M
@Columbo,你有下载链接吗?(根据SO C++文档页面的建议,需要从CWG页面进行身份验证) - M.M
2
@Columbo:引用工作草案来证明标准行为并不是很有用,因为它们在标准之间可以显著地改变,同时添加和删除一些内容。最好引用标准或者必要时引用最后一个编辑成为标准的工作草案。这就是Matt所做的(是的,N3936实际上是C++14)。 - Lightness Races in Orbit
1
@LightnessRacesinOrbit N4140 与 N3936 相比仅包含编辑更改,其中一个更改对于引用(编号的项目符号)尤其有用。 - T.C.
2
@T.C. 是的,但一般来说,当意图引用标准时,为什么要推动某人放弃实际的国际标准措辞而选择草案呢? - Lightness Races in Orbit
显示剩余4条评论

14

X不需要与函数"绑定",因为您在代码中声明了该函数的存在。因此编译器可以安全地假定该函数地址不会为NULL。为了实现这一点,您必须将函数声明为弱符号,但是您没有这样做。链接器没有抗议,因为您从未调用过该函数(您从未使用其实际地址),因此它看不到任何问题。


当然,编译器 可以做出这样的假设。但是 链接器 实际上将其链接起来并生成一个可执行文件。 - chmullig
4
不过,1并不是函数的地址。(如果你加入x1x2x3等等,它们都会变成1 - M.M
3
@chmullig - 请看已编辑的答案 - 链接器从未见过您的 x 符号,因为编译器从未使用它 - 它根据语言规则对此“测试”进行了优化,因为它总是为真。如果您有一个 extern 变量并测试其地址,它也会以同样的方式工作。 - Freddie Chopin
@FreddieChopin:什么意思是“你必须声明该函数为弱符号”?我不明白。这个弱符号是什么? - Destructor

9

[basic.def.odr]/2:

如果一个函数名出现在潜在评估的表达式中,那么它是odr-used,前提是它是唯一的查找结果或者是重载函数的选定成员(3.4、13.3、13.4),除非它是一个纯虚函数并且其名称没有明确限定。

因此,严格来说,代码odr使用该函数,因此需要定义。但现代编译器将意识到该函数的确切地址实际上与程序的行为无关,并且因此省略使用并不需要定义。

[basic.def.odr]/3 还规定:

每个程序都应包含每个odr被用于该程序中的非内联函数或变量的一个定义;无需诊断。

实现没有义务停止编译并发出错误消息(=诊断)。它可以做出它认为最好的事情。换句话说,任何操作都是允许的且我们有UB。


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