不同的函数是否有不同的地址?

60

考虑这两个函数:

void foo() {}
void bar() {}

这个问题的意思是 &foo != &bar 能否得到保证?

同样地,

template<class T> void foo() { }

&foo<int> != &foo<double>是否有保证?


我知道有两种链接器可以将函数定义合并。

MSVC会积极使用COMDAT来折叠函数,所以具有相同实现的两个函数可以变成一个函数。副作用是这两个函数共享同一个地址。我曾经认为这是不合法的,但我找不到标准中规定这是不合法的地方。

Gold链接器也会合并函数,有 safeall 两种设置。 safe 表示如果对函数地址进行了引用,则不会折叠,而 all 即使进行了引用也会折叠。 因此,gold的safe 折叠行为就像函数具有不同地址一样。

虽然折叠可能会出乎意料,而且有些代码依赖于具有不同地址(相同实现)的函数,这样折叠可能会很危险,但在当前的C++标准下(即C++14),它是否真的是不合法的?(自然情况下,safe 折叠是合法的)


5
我确定之前见过这个问题。 - Lightness Races in Orbit
1
@MarcGlisse:赞你找到了这个问题,只有一个观察:他们从未说依赖于任何两个具有不同地址的函数是允许的。引文:"ICF 可能不安全,因为它可能会改变依赖于每个函数具有唯一地址的代码的运行时行为。" - Deduplicator
5
@LightnessRacesinOrbit:是的,你之前见过它:https://dev59.com/7mYq5IYBdhLWcg3wzDlI 虽然那篇文章的赞数、回答和争议都比较少。;-) - Deduplicator
1
@LightnessRacesinOrbit 合并时需要标记吗?根据我的合并经验,这个问题的答案在这里非常合适。 - Shafik Yaghmour
显示剩余14条评论
4个回答

32
看起来defect report 1400: Function pointer equality处理了这个问题,并且我认为它表明这样做是可以的,但是正如注释所指出的那样,人们存在分歧。它说(我强调):
根据5.10 [expr.eq]第2段,“只有指向同一函数的两个函数指针才相等”。然而,作为一种优化,目前的实现将具有相同定义的函数别名化。不清楚标准是否需要显式处理此优化。
回答是:
标准对要求非常明确,实现可以在“as-if”规则的约束下进行优化
该问题涉及两个问题:
  • 这些指针是否可以被视为相等
  • 合并这些函数是否可以
基于评论,我看到了两种解释回答:
这种优化是可以的,标准在“as-if规则”下赋予了实现这种自由。 “as-if规则”在第1.9节中有所涵盖,意味着实现只需模拟与标准要求相关的可观察行为。 这仍然是我对回答的解释。
问题完全被忽略了,该语句仅表示不需要调整标准,因为显然“as-if规则”涵盖了这一点,但是解释留给读者自行理解。 尽管我承认由于回答过于简洁,我无法驳回这个观点,但它最终变成了一个完全没有帮助的回答。 它似乎也与其他NAD问题的回答不一致,因为据我所知,如果存在问题,它们会指出问题。
草案标准的内容
既然我们知道正在处理“as-if规则”,那么我们可以从那里开始,并注意到第1.8节说:
除非一个对象是位域或零大小的基类子对象,否则该对象的地址是它占用的第一个字节的地址。如果一个对象不是位域,则如果它们中的一个是另一个对象的子对象,或者至少一个是零大小的基类子对象且它们是不同类型,则两个对象可能具有相同的地址;否则,它们必须具有不同的地址。(注4:根据“as-if”规则,如果程序无法观察到差异,则允许一个实现在同一机器地址上存储两个对象或不存储任何对象)。
注意4说明:
根据“as-if”规则,如果程序无法观察到差异,则允许一个实现在同一机器地址上存储两个对象或不存储任何对象。
但该部分的一条注释指出:
函数不是对象,无论它是否像对象一样占用存储空间。
虽然这不是规范性的,但段落1中所述对象的要求在函数上下文中毫无意义,因此它与该注释一致。因此,我们明确限制别名对象具有某些例外情况,但不应用于函数。
下面我们有第 5.10等号运算符,其中说(重点是我的):

[...]如果两个指针都为 null,或者都指向同一个函数,或者都表示相同的地址(3.9.2),则它们相等,否则它们不相等。

这告诉我们两个指针相等如果它们是:
  • Null 指针
  • 指向同一个函数
  • 表示相同的地址

或者都表示相同的地址 看起来给予编译器足够的自由度,允许别名两个不同的函数,并且不要求指向不同函数的指针不相等。

观察

Keith Thompson 做了一些很好的观察,我认为值得添加到答案中,因为它们涉及到核心问题,他说:

如果程序打印 &foo == &bar 的结果,那就是可观察的行为;所讨论的优化改变了可观察的行为。

我同意这种观点,如果我们能够证明指针需要是不相等的,那么这确实会违反“as-if rule”,但目前我们无法证明。
另外,考虑一个定义了空函数并将它们的地址用作唯一值的程序(可以想象在 / 中的SIG_DFL、SIG_ERR和SIG_IGN)。将它们赋予相同的地址将会破坏这样的程序。
正如我在评论中提到的,C标准要求这些宏生成具有不同值的常量表达式,这些值与signal函数的第二个参数和返回值兼容,并且它们的值与任何可声明函数的地址不相等。
因此,尽管这种情况已经被覆盖,但也许还有其他情况会使这种优化变得危险。
更新
Jan Hubička是一位gcc开发人员,他写了一篇博客文章 GCC 5中的链接时间和跨程序优化改进,代码折叠是他涉及的许多主题之一。
我问他是否将相同的函数折叠到相同的地址是否符合行为规范,他说这不是符合规范的行为,确实这样的优化会破坏gcc本身:
“将两个函数转换为具有相同地址是不符合规范的,因此MSVC在这方面非常激进。例如,这样做会破坏GCC本身,因为令我惊讶的是,在预编译标头代码中执行地址比较。它适用于许多其他项目,包括Firefox。”
事后看来,经过几个月阅读缺陷报告并思考优化问题,我对委员会的回应持更为保守的态度。获取函数的地址是可观测行为,因此折叠相同的函数将违反“as-if rule”原则。
第二次更新
也请参见此 llvm-dev 讨论:零长度函数指针相等性:

这是一个众所周知的违反符合性的link.exe漏洞;LLVM不应该通过引入类似的漏洞来使事情变得更糟。更聪明的链接器(例如,我认为lld和gold)仅在除一个函数符号以外的所有函数符号仅用作调用的目标(而不是实际观察地址)时才执行相同的函数组合。是的,这种非符合行为(很少)会在实践中导致故障。请参阅此研究论文


4
接近了,但还不够:如果在“as-if”情况下,两个不同的函数共享相同的二进制码是允许的,但是比较两个不同函数的地址是否相等可能会有问题。一种解决方法是要求在函数前面加上“占位符”noopjmp,这样每个函数都会得到一个不同的地址,但是它们的代码体相同(就像某些非MSVC COMDAT折叠等效方法所做的那样)。 - Yakk - Adam Nevraumont
2
@Yakk:这将使计数器圆整到“地址”的通用定义,如果标准中没有不同的定义,则该定义将占主导地位。 - Deduplicator
5
我认为委员会的回应不仅简短,而且晦涩难懂。标准中的一个脚注说:“只要从程序的可观察行为上可以确定符合要求,一个实现可以自由地忽略这个国际标准的任何要求。”如果一个程序打印出&foo == &bar的结果,那就是可观察的行为;所讨论的优化则改变了可观察的行为。我对回应的理解是指指针必须比较不相等,但我怀疑这不是原意。 - Keith Thompson
3
@ShafikYaghmour: 虽然没有明确说明,但我认为这是常识。显然,其他人持不同意见。问题在于两个明确定义的函数是否可以“相同”。一个优化编译器是否可以共享两个函数的代码,使它们在用户代码中“相同”?它能否对不同的对象做同样的事情?实际上,考虑一个定义了空函数并使用它们的地址作为唯一值的程序(例如 <signal.h> / <csignal> 中的 SIG_DFLSIG_ERRSIG_IGN)。给它们分配相同的地址将破坏此类程序。 - Keith Thompson
4
@KeithThompson:看起来你的解释允许将一个函数与一个地址不被获取的独立但相同的函数合并,但是每个地址被获取的函数只有在它们的地址比较不同的情况下才能合并(在指针比硬件地址多的架构上,两个指针可能表示相同的物理地址,但仍然显示为不同)。如果意图是要求地址被获取的函数必须产生不同的地址,我想知道为什么它没有明确说明呢? - supercat
显示剩余16条评论

11

是的。 标准(§5.10/1)中指出:“如果两个相同类型的指针都为 null、都指向相同函数,或者都表示相同地址,则它们相等。”

一旦实例化,foo<int>foo<double> 就成为了两个不同的函数,因此上述规则也适用于它们。


5
这并不妨碍它们表示相同的地址但指向不同的函数。 - Mike Seymour
7
函数在标准中没有地址。 - James Kanze
6
您不会找到一个说函数没有地址的声明。然而,只有对象占据存储空间,并且只有占据存储空间的东西才能有地址。请参见§5.3.1/3; 函数上的一元运算符&返回一个“指针”,而不是它的地址(与对象的情况不同)。 - James Kanze
5
请参考 §1.8/6,该章节定义了对象的地址。至少在我的搜索范围内,并未找到任何有关函数地址的内容。虽然有指向函数的“指针”,但指向函数的指针仅代表指向函数的位置,不像地址那样具有明确含义,实际取决于编译器允许的方式来查找函数。 - James Kanze
7
标准定义了它使用的所有术语。就标准而言,尽管某个章节标题措辞不够谨慎(在其中使用了“地址”这个词的普通意义,而非标准中使用的意义),但函数根本没有地址。请注意,这里的翻译仅供参考,不得用于正式场合。 - James Kanze
显示剩余17条评论

11
因此,有问题的部分显然是短语或两者代表相同的地址(3.9.2)
在我看来,这一部分显然是为了定义对象指针类型的语义。仅仅是为了对象指针类型。
该短语引用第3.9.2节,这意味着我们应该在那里查找。3.9.2讨论了对象指针表示的地址(其中包括其他内容)。它没有讨论函数指针表示的地址。在我看来,这留下了两种可能的解释:
1) 该短语根本不适用于函数指针。这仅留下了两个空指针和两个指向相同函数的指针相等,这可能是我们中的大多数人所预期的。
2) 该短语确实适用。由于它是指3.9.2,而3.9.2并未说明函数指针表示的地址,因此我们可以使任何两个函数指针相等。这非常出乎意料,当然也使得比较函数指针变得完全无用。
因此,尽管理论上可以认为(2)是一个有效的解释,但在我看来,它不是一个有意义的解释,因此应予以忽略。鉴于并不是每个人都同意这一点,我也认为标准需要澄清。

3

5.10 相等运算符 [expr.eq]

1 == (相等) 和 != (不相等) 运算符从左到右结合。操作数必须是算术、枚举、指针或成员指针类型,或类型为 std::nullptr_t。运算符 ==!= 都生成 truefalse 的结果,即类型为 bool。在以下每种情况下,应用了指定的转换后,操作数的类型必须相同
2 如果至少有一个操作数是指针,则对两个操作数执行指针转换(4.10)和资格转换(4.4),将它们带到组合指针类型 (第 5 条)。比较指针的定义如下: 如果两个指针都为空、都指向同一函数或都表示相同的地址(3.9.2),则它们相等;否则它们不相等。

我们逐字地看最后一段:

  1. 两个空指针相等。
    这非常容易理解。
  2. 两个指向同一函数的指针相等。
    其他情况下都将非常令人惊讶。
    这也意味着,除非您希望使函数指针比较变得异常复杂和昂贵,否则任何内联函数的唯一一个分离实现都只能有其地址被取到。
  3. 它们表示相同的地址。
    这就是重点。如果不遵循此规定并省略 if and only if,那么将留下解释的空间。但是这就是 要求使任意两个函数均相同(只要它不以其他方式改变符合规范的程序的可观测行为)的明确规定。

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