9

GCC 4.7.2 编译此代码如下:

  • Obj::getNumber()对in的引用Obj::defaultNumber()是内联的
  • 的正文Obj::getNumber()是单独编译和导出的,并且可以从不同的翻译单元链接到。

VC++2012 在链接步骤失败:

  • Error 1 error LNK2001: unresolved external symbol "public: virtual int __thiscall Obj::getNumber(unsigned int)"main.obj

VC++ 似乎是在符号表中内联调用,Obj::defaultNumber()而不是getNumber()在符号表中导出。

VC++ 可以通过以下方式之一编译它:

  • 从定义中删除inline关键字getNumber(),或
  • 从声明中删除virtual关键字(为什么!?)getNumber()

乍一看,GCC 的行为似乎更有帮助/直观。也许熟悉标准的人可以在这里为我指明正确的方向。

  • 在这种情况下,任何一个编译器都有符合的行为吗?
  • 如果方法是非虚拟的,为什么 VC++ 可以工作?

.

// Obj.h

class Obj
{
public:
    virtual int getNumber(unsigned int i);
    virtual int defaultNumber();
};

// Obj.cpp

static const int int_table[2] = { -1, 1 };

inline int Obj::getNumber(unsigned int i)
{
    return int_table[i];
}

int Obj::defaultNumber()
{
    return getNumber(0);
}

// main.cpp

#include <iostream>
#include "Obj.h"

int main()
{
    Obj obj;
    std::cout << "getNumber(1): " << obj.getNumber(1) << std::endl;
    std::cout << "defaultNumber(): " << obj.defaultNumber() << std::endl;
}
4

3 回答 3

8

我完全改变了一次答案,为什么不是两次?对于任何错别字,已经很晚了。

短的

在这种情况下,任何一个编译器都有符合的行为吗?

他们两个都这样做。这是未定义的行为。

如果方法是非虚拟的,为什么 VC++ 可以工作?

因为您的代码不符合标准,编译器可以自行决定解释您的代码。

为什么编译器是一致的?

该标准表示,如果不是纯的,则每个定义规则都会使用虚拟成员,并且每个翻译单元都需要自己定义 odr 使用的内联函数。

§ 3.2

2) "[...] 一个虚成员函数如果不是纯的,它是 odr-used。[...]"
3) "[...] 一个内联函数应该在它所在的每个翻译单元中定义odr 使用过。[...]"

§ 7.1.2

4) 内联函数应在使用它的每个翻译单元中定义,并且在每种情况下都应具有完全相同的定义(3.2)。[...]

此外,该标准要求函数在每个翻译单元内内联声明,但允许编译器省略任何诊断。

§ 7.1.2

4) [...] 如果具有外部链接的函数在一个翻译单元中声明为内联,则应在其出现的所有翻译单元中声明为内联;不需要诊断。[...]

由于您getNumber()没有在两者中声明为内联,main.cpp并且Obj.cpp您在这里处于未定义行为的土地上。

(编辑)我解释这个(见下面的标准引号)的方式是编译器不需要检查函数是否在它出现的任何地方都被声明为内联。我不知道,但怀疑这个“编译器不必检查”-规则不是指“内联函数应在每个使用它的翻译单元中定义的句子 [...] ”。

即使getNumber在 main 中额外定义了 MSVC(从 main 的角度来看从未被声明为内联),即使 Obj.cpp 实现仍被声明为内联并存在于符号表中,MSVC 也会编译。即使代码不符合标准,编译器的这种行为也是允许的:未定义行为(UB)。(/编辑)

因此,两个编译器都可以自由地接受和编译或拒绝您的代码。

如果方法是非虚拟的,为什么 VC++ 可以工作?

这里的问题是:为什么 VC++ 不实例化一个 virtual 内联函数,但如果 virtual 被忽略了,就这样做呢?

您可能会在 SO 周围读到那些声明,上面写着“UB 可能会炸毁你的房子,杀死你的妈妈,结束世界”等。我不知道为什么如果函数是虚拟的则没有函数实例,但否则有。我想这就是 UB 的意义所在——奇怪的事情。

为什么 GCC 和 VS (没有virtual都提供内联函数的外部链接函数实例?

编译器不需要实际替换内联函数,它们很可能会生成函数的实例。由于默认情况下内联函数具有外部链接,因此getNumber如果创建了实例,则可以从 main 调用。

  • 编译器只会在 main.obj 中添加一个未定义的外部(没问题,这里不是内联的)。
  • 链接器将在 Obj.obj 中找到完全相同的外部符号并愉快地链接。

7.1.2

2) 在调用点执行此内联替换不需要实现;然而,即使省略了这个内联替换,7.1.2 中定义的内联函数的其他规则仍应得到遵守。

4) 具有外部链接的内联函数在所有翻译单元中应具有相同的地址。

§ 9.3

3) 命名空间范围内的类的成员函数具有外部链接。

MSDN 内联、__inline、__forceinline

inline 关键字告诉编译器首选内联扩展。但是,编译器可以创建函数的单独实例(实例化)并创建标准调用链接,而不是插入内联代码。可能发生这种情况的两种情况是:

  • 递归函数。
  • 通过翻译单元中其他地方的指针引用的函数。

这些原因可能会干扰内联,其他原因可能会由编译器自行决定;您不应该依赖 inline 说明符来内联函数。

所以可能有一个“其他”原因使编译器实例化它并创建一个由 main.obj 使用的符号,但我不知道为什么这个原因会在函数是 .obj 时消失virtual

结论:

编译器可能会或可能不会实例化内联函数,并且它可能会或可能不会接受在一个翻译单元中内联和在另一个翻译单元中非内联的相同函数。要定义您的行为,您需要

  • 将函数的每个声明都内联或全部非内联
  • 为每个翻译单元提供单独的内联函数定义(即标题中的内联定义或源文件中的单独定义)
于 2013-07-08T21:40:17.797 回答
5

两个编译器都是正确的。您正在调用未定义的行为,根据 7.1.2 第 2 段不需要诊断:

内联函数应在使用它的每个翻译单元中定义,并且在每种情况下都应具有完全相同的定义(3.2)。...如果具有外部链接的函数在一个翻译单元中声明为内联,则应在其出现的所有翻译单元中声明为内联;不需要诊断。

GCC 可以自由地做它所做的事情,并为您提供一个可能恰好可以工作的程序。VC++ 在拒绝你的代码时同样有效。

于 2013-07-09T20:29:50.293 回答
3

内联函数应该在使用的每个翻译单元中都有一个定义,在你的情况下没有一个main.cpp

内联函数的定义不必在头文件中,但由于内联函数的定义规则是一个,因此函数的相同定义必须存在于每个使用它的翻译单元中。

实现这一点的最简单方法是将定义放在头文件中。

如果要将函数的定义放在单个源文件中,则不应将其声明为内联。未声明内联的函数并不意味着编译器不能内联该函数。

通过@CharlesBailey

1在这种情况下,任何一个编译器都有符合的行为吗?

从这个标准引文中,我们可以说在这种特定情况下 VC 是正确的。

C++ 标准,§3.2 一个定义规则/3:内联函数应在使用 odr 的每个翻译单元中定义

谢谢@Pixelchemist

2如果方法是非虚拟的,为什么 VC++ 可以工作?

根据标准非虚拟功能不是“一个定义规则” - 使用,因此它允许以您想要的方式使用它。

于 2013-07-08T21:25:59.823 回答