20

我有一个具有内联成员的类,但后来我决定要从标头中删除实现,因此我将函数的成员主体移到 cpp 文件中。起初我只是将内联签名留在头文件中(我马虎),程序无法正确链接。然后我修复了我的标题,当然一切正常。

但是内联不是完全可选的吗?

在代码中:

第一的:

//Class.h
class MyClass
{
   void inline foo()
   {}
};

接下来更改为(不会链接):

//Class.h
class MyClass
{
   void inline foo();
};

//Class.cpp
void MyClass::foo()
{}

然后到(会正常工作):

//Class.h
class MyClass
{
   void foo();
};

//Class.cpp
void MyClass::foo()
{}

我认为 inline 是可选的,并想象我可能会因为我的草率而受到警告,但没想到会出现链接错误。在这种情况下,编译器应该做的正确/标准的事情是什么,根据标准,我应该得到我的错误吗?

4

4 回答 4

38

事实上,有一个定义规则说,必须在每个使用它的翻译单元中定义一个内联函数。血淋淋的细节如下。首先3.2/3

每个程序都应包含该程序中使用的每个非内联函数或对象的一个​​定义;无需诊断。定义可以显式出现在程序中,可以在标准或用户定义库中找到,或者(在适当时)隐式定义(参见 12.1、12.4 和 12.8)。内联函数应在使用它的每个翻译单元中定义。

当然7.1.2/4

内联函数应在使用它的每个翻译单元中定义,并且在每种情况下都应具有完全相同的定义(3.2)。[注意:在其定义出现在翻译单元之前,可能会遇到对内联函数的调用。] 如果具有外部链接的函数在一个翻译单元中声明为内联,则应在其出现的所有翻译单元中声明为内联;不需要诊断。具有外部链接的内联函数在所有翻译单元中应具有相同的地址。外部内联函数中的静态局部变量始终引用同一个对象。外部内联函数中的字符串文字是不同翻译单元中的相同对象。

但是,如果您在类定义中定义函数,它会被隐式声明为inline函数。这将允许您在程序中多次包含包含该内联函数体的类定义。由于该函数具有external链接,因此它的任何定义都将引用相同的函数(或更血腥 - 指向相同的函数entity)。

关于我的索赔的血腥细节。首先3.5/5

此外,如果类的名称具有外部链接,则成员函数、静态数据成员、类或类范围的枚举具有外部链接。

然后3.5/4

如果具有命名空间范围的名称是 [...] 命名类(第 9 条)的名称,或者是在 typedef 声明中定义的未命名类,其中该类具有用于链接目的的 typedef 名称,则该名称具有外部链接。

这个“用于链接目的的名称”很有趣:

typedef struct { [...] } the_name;

由于现在您的程序中有同一实体的多个定义,ODR 的另一件事恰好限制了您。3.2/5接下来是无聊的东西。

如果每个定义出现在不同的翻译单元中,程序中可以有多个类类型(第 9 条)、枚举类型(7.2)、带有外部链接的内联函数(7.1.2)[...] 的定义,并提供定义满足以下要求。给定这样一个名为 D 的实体在多个翻译单元中定义,则

  • D 的每个定义都应由相同的记号序列组成;和
  • 在 D 的每个定义中,根据 3.4 查找的相应名称应指在 D 的定义中定义的实体,或应指同一实体,在重载决议 (13.3) 和部分模板特化匹配后 (14.8) .3) [...]

我现在切断了一些不重要的东西。以上是关于内联函数需要记住的两个重要内容。如果您多次定义 extern 内联函数,但确实以不同方式定义它,或者如果您定义它并且其中使用的名称解析为不同的实体,那么您正在执行未定义的行为。

函数必须在使用它的每个 TU 中定义的规则很容易记住。并且它是相同的也很容易记住。但是那个名字解析的东西呢?这里有一些例子。考虑一个静态函数assert_it

static void assert_it() { [...] }

现在,既然static会给它内部链接,当你将它包含到多个翻译单元中时,每个定义都会定义一个不同的实体。这意味着您不允许使用assert_it将在程序中多次定义的 extern 内联函数:因为发生的情况是内联函数将引用assert_it一个 TU 中调用的一个实体,但引用相同的另一个实体在另一个 TU 中命名。你会发现这都是无聊的理论,编译器可能不会抱怨,但我发现这个例子特别显示了 ODR 和实体之间的关系。


接下来是再次回到您的特定问题。

以下是相同的事情:

struct A { void f() { } };
struct A { inline void f(); }; void A::f() { } // same TU!

但是这个不同,因为该函数是非内联的。您将违反 ODR,因为您有多个定义f是否多次包含标题

struct A { void f(); }; void A::f() { } // evil!

现在,如果您在类内部inline声明 of f,然后省略在标题中定义它,那么您就违反了3.2/3(并且7.1.2/4说的是同一件事,只是更详细),因为该函数未在该翻译单元中定义!

请注意,在 C (C99) 中,内联具有与 C++ 不同的语义。如果你创建一个外部内联函数,你应该首先阅读一些好论文(最好是标准),因为这些在 C 中真的很棘手(基本上,任何使用的函数内联定义都需要另一个非内联函数定义在另一个TU. C 中的静态内联函数很容易处理。除了具有通常的“内联替换”提示之外,它们的行为与任何其他函数一样inline。C 和 C++ 中的静态仅用作内联替换提示。因为静态已经创建任何时候使用不同的实体(由于内部链接),inline只会添加内联替换提示 - 不多。

于 2009-05-26T13:29:20.590 回答
10

该方法是否实际内联由编译器自行决定。但是 inline 关键字的存在也会影响方法的链接。

C++ 链接不是我的专长,所以我将遵循链接以获得更好的解释。

或者,您可以等待litb在一个小时左右提供血腥细节;)

于 2009-05-26T03:39:23.850 回答
6

注意:当方法被声明为内联时,它的定义必须和它的声明一起。

于 2009-05-26T04:24:08.903 回答
2

关于harshath.jr的回答,如果方法的定义具有“inline”关键字,则不需要将方法声明为内联,并且该定义在同一个标​​头中可用,

class foo
{
  void bar();
};

inline void foo::bar()
{
  ...
}

这对于根据构建是“调试”还是“发布”有条件地内联方法很有用,如下所示:

// Header - foo.h

class foo
{
  void bar();  // Conditionally inlined.
};

#ifndef FOO_DEBUG
# include "foo.inl"
#endif

“内联”文件可能如下所示:

// Inline Functions/Methods - foo.inl
#ifndef FOO_DEBUG
# define FOO_INLINE inline
#else
# define FOO_INLINE
#endif

FOO_INLINE void foo::bar()
{
  ...
}

并且实现可能如下所示:

// Implementation file - foo.cpp
#ifdef FOO_DEBUG
# include "foo.inl"
#endif

...

它并不完全漂亮,但是当激进的内联成为调试难题时,它就派上了用场。

于 2009-05-26T20:28:33.783 回答