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

chm*_*lig 29 c++ linker compilation language-lawyer undefined-function

可能令人惊讶的是,这个C++代码打印出来1.

#include <iostream>

std::string x();

int main() {

    std::cout << "x: " << x << std::endl;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

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++中,这根本不是问题.谁能解释这种行为?

M.M*_*M.M 28

这里发生的是函数指针被隐式转换为bool.这由[conv.bool]以下内容指定:

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

其中"空指针值"包括空函数指针.由于从函数名称的衰减中获得的函数指针不能为空,因此给出了true.您可以通过<< std::boolalpha在输出命令中包含来看到这一点.

以下确实导致g ++中的链接错误: (int)x;


关于是否允许这种行为,C++ 14 [basic.odr.ref]/3说:

如果函数的唯一查找结果或一组重载函数的选定成员[...],则其名称显示为可能已评估的表达式的函数将被使用.

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

每个程序应该只包含该程序中使用的每个非内联函数或变量的一个定义; 无需诊断.

所以程序结构不合理,但不需要诊断,这意味着程序的行为是完全未定义的.

顺便说一句,这条款意味着x();从实施质量角度来看,两者都不需要链接错误; 那会很傻.g++这里选择的课程对我来说似乎是合理的.

  • @Columbo:引用工作草案来证明标准行为并不是很有用,因为它们可以并且确实在标准之间发生了显着变化,同时添加和删除了位.引用_standards_或者如果需要引用编辑成为标准的最后一份工作草案总是更好.这就是马特所做的(是的,N3936是_Effectively_ C++ 14). (2认同)
  • @TC是的,但总的来说,为什么你会推动某人抛弃实际的国际标准措辞而不是草案,而是打算引用一个标准? (2认同)

Fre*_*pin 13

X不需要"绑定"到函数,因为您在代码中声明存在此类函数.因此编译器可以安全地假设该函数的地址不能为NULL.为了实现这一点,你必须声明函数是一个弱符号,而你却没有.链接器没有抗议,因为你从不调用你的函数(你从不使用它的实际地址),所以它没有看到任何问题.

  • 但是`1`不是函数的地址.(如果你添加`x1`,`x2`,`x3`等,它们都会得到`1`) (4认同)
  • @chmullig - 看到编辑过的答案 - 链接器永远不会看到你的`x`符号,因为编译器从不使用它 - 它优化了这个"测试",因为根据语言规则,它总是正确的.如果你有一个`extern`变量并测试它的地址,这将以相同的方式工作. (3认同)

Col*_*mbo 9

[basic.def.odr]/2:

如果它是唯一查找结果或一组重载函数的选定成员(3.4,13.3,13.4),则其名称显示为可能已评估的表达式的函数将使用odr,除非它是纯虚函数及其名称没有明确限定.

因此,严格来说,代码odr-使用函数,因此需要定义.
但是现代编译器会意识到函数的确切地址实际上并不与程序的行为相关,因此会忽略使用而不需要定义.

另请注意[basic.def.odr]/3指定的内容:

每个程序应该只包含该程序中使用的每个非内联函数或变量的一个定义; 无需诊断.

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