为什么在Visual C++中成员函数指针的行为如此奇怪?

29

我遇到了一个非常奇怪的问题,我已经将其缩小为以下测试用例:

#include <iostream>
#include <map>
#include <string>

struct Test
{
    std::map<std::string, void (Test::*)()> m;
    Test()
    {
        this->m["test1"] = &Test::test1;
        this->m["test2"] = &Test::test2;
    }
    void test1() { }
    void test2() { }
    void dispatch(std::string s)
    {
        if (this->m.at(s) == &Test::test1)
        { std::cout << "test1 will be called..." << std::endl; }
        else if (this->m.at(s) == &Test::test2)
        { std::cout << "test2 will be called..." << std::endl; }
        (this->*this->m.at(s))();
    }
};

int main()
{
    Test t;
    t.dispatch("test1");
    t.dispatch("test2");
}

输出结果为

当启用优化时,将会调用test1...
将会调用test1...

这是非常奇怪的。发生了什么?


7
因为这个问题涉及到C++,测试用例也是用C++编写的,而C++和C是两种不同的语言,有着不同的规则。 - Lightness Races in Orbit
1
@Mehrdad:好的,那我建议改进一下问题的标题。目前它并没有描述问题,只是说事情很“奇怪”。 - Lightness Races in Orbit
4
请保持建设性,"@LightnessRacesinOrbit Please stay constructive. "I don't care about anybody who wants to google this issue" and "change it now but IDK what to change it" sounds selfish." 的意思是自私的。请注意不要改变原意,但需要使翻译更通俗易懂。 - user529758
2
@Mehrdad: 我之所以选择那个短语,不是毫无先例。可以看看《仙女星》(Andromeda)中 Trance 的话,“我不在乎骨头”,这暗示她并没有明确的负面个人和情感联系,而是她的经验和智慧让她不再重视它们,与身边其他人声称这些骨头具有重要价值形成对比。同样,在这里,我的意思只是在我看来,正确标记问题比最大化搜索引擎点击率更重要。否则,我们为了让“每个人”都参与,就把问题标记为“php”、“java”和“mysql”吧!=) - Lightness Races in Orbit
2
@JamesMcNellis: 完成! 如果你注意到他们发布了跟进,而我没有在一天左右的时间内注意到,请随时联系我。 - user541686
显示剩余17条评论
3个回答

27
这是Visual C++中所谓的“Identical COMDAT Folding”(ICF)的副产品,将相同的函数合并为一个实例。您可以通过在链接器命令行中添加以下开关来禁用它:/OPT:NOICF(在Visual Studio界面下,它位于“属性-> 链接器-> 优化-> 启用COMDAT折叠”下)。有关详细信息,请参阅MSDN文章:/OPT (Optimizations)
该开关是链接器级别的开关,这意味着您无法仅针对特定模块或特定代码区域启用它(例如可用于编译器级别优化的__pragma(optimize()))。
然而,通常来说,依赖于函数指针或字面字符串指针(const char*)来测试唯一性是不好的做法。 字符串折叠被几乎所有C / C ++编译器广泛实现。 目前,函数折叠仅适用于Visual C ++,尽管对template<>元编程的广泛使用已增加了将此功能添加到gcc和clang工具链的请求。
编辑:从binutils 2.19开始,据说包括gold链接器也支持ICF,但我无法在我的本地Ubuntu 12.10上验证。

一般来说,依赖于函数指针被认为是不良实践。我同意这一点,也不明白为什么有人需要依赖它。在这里可以看到更广泛的讨论(https://dev59.com/pF8d5IYBdhLWcg3wtz8Q)。 - Shafik Yaghmour

19

事实证明,Visual C++的链接器可以将具有相同定义的函数合并为一个。
根据C++标准是否合法我不清楚;它会影响可观察行为,所以在我看来这似乎是一个bug。然而,其他拥有更多信息的人可能想要发表自己的看法。


7
我看不出5.3.1有任何禁止这样做的规定。&应该会给你一个成员函数指针,但它并没有说明必须是唯一的要求。 - Lightness Races in Orbit
1
@AndreyT:但它们确实是同一个函数,只是有两个名称 :) - Lightness Races in Orbit
2
@Lightness Races in Orbit:这个笑话太容易预测了,所以并不真正有趣 :) - AnT stands with Russia
3
@AndreyT: 我不是在开玩笑! - Lightness Races in Orbit
3
@Lightness Races in Orbit: 我找不到“connect”问题,但这是我同时在MS论坛上发布的帖子链接:http://social.msdn.microsoft.com/Forums/en/vclanguage/thread/dd91dae2-6f9d-4a46-b1dd-753c2b927119。 - K-ballo
显示剩余9条评论

7
C++11 5.3.1描述了&的作用;在这种情况下,它会给你一个指向相关成员函数的指针,并且该段落没有要求此指针必须是唯一的。
然而,5.10/1关于==如下所述:
两个相同类型的指针仅当它们都为null、都指向同一函数或者都表示相同地址时才相等。
那么问题就变成了... test1test2是否是“相同的函数”?
尽管优化器已将它们合并为一个定义,但可以说这两个名称标识了两个函数,因此,这似乎是一个实现错误。
(注意,虽然VS团队不关心它,并认为它“足够有效”以获得优化的好处。那么,他们可能意识不到它是无效的。)
我建议使用字符串作为函数指针的“句柄”。

+1,但我建议您使用int作为“句柄”,因为它们比较快。 - Caesar
3
@Caesar:为什么不用enum class呢?它们既快速又有名称 - Nawaz
3
@jstine:也许您可以澄清一下我误解了标准的什么内容。至于VC ++,它并非没有根据-在我链接的线程中,Passant及其同伴明确指出该功能不是错误,尽管它实际上是。我的这些结论确实依赖于我没有犯错的想法;我当然愿意接受我有可能犯错这一事实,但也许您可以为我找出来,而不是只是大喊“你错了”,然后离开? - Lightness Races in Orbit
@jstine:这是一个棘手的问题。 我最初的解释是,函数在本质上没有“地址”,因此“或者两者表示相同的地址”只能意味着指向相同对象。否则,为什么要使用“指向同一函数”的条款?我坚持这种意图的解释,但我找不到客观证明这就是应该推断出这段话的意思的方法。还在努力中... - Lightness Races in Orbit
@jstine:我已经把它放到https://dev59.com/7mYq5IYBdhLWcg3wzDlI。如果我的想法改变了,我会将结论反馈到这个答案中。 - Lightness Races in Orbit
显示剩余5条评论

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