模块接口中的内联含义

inline meaning in module interfaces

考虑 header 文件:

class T
{
private:
  int const ID;

public:
  explicit T(int const ID_) noexcept : ID(ID_) {}

  int GetID() const noexcept { return ID; }
};

或者,或者:

class T
{
private:
  int const ID;

public:
  explicit T(int const ID_) noexcept;

  int GetID() const noexcept;
};

inline T::T(int const ID_) noexcept : ID(ID_) {}

inline int T::GetID() const noexcept { return ID; }

在 pre-modules 世界中,这些 header 可能会以文本形式包含在多个 TU 中,而不会违反 ODR。此外,由于涉及的成员函数相对较小,编译器可能会"inline"(在使用时避免函数调用)这些函数,甚至完全优化掉T的一些实例。

在最近的一次 report 会议上,在 C++20 完成的会议上,我可以读到以下声明:

We clarified the meaning of inline in module interfaces: the intent is that bodies of functions that are not explicitly declared inline are not part of the ABI of a module, even if those function bodies appear in the module interface. In order to give module authors more control over their ABI, member functions defined in class bodies in module interfaces are no longer implicitly inline.

我不确定我没有弄错。这是否意味着,在模块世界中,为了使编译器能够优化函数调用,我们必须将它们注释为 inline,即使它们被定义为 in-class?

如果是这样,下面的模块接口是否等同于上面的 headers?

export module M;

export
class T
{
private:
  int const ID;

public:
  inline explicit T(int const ID_) noexcept : ID(ID_) {}

  inline int GetID() const noexcept { return ID; }
};

尽管我仍然没有支持模块的编译器,但我想在适当的时候像这样开始使用 inline,以尽量减少未来的重构。

这来自P1779,几天前刚在布拉格领养。来自提议:

This paper proposes removing the implicit inline status from functions defined in a class definition attached to a (named) module. This allows classes to benefit from avoiding redundant declarations, maintaining the flexibility offered to module authors in declaring functions with or without inline. Moreover, it allows injected friends of class templates (which cannot be generically defined outside the class definition) to be non-inline at all. It also resolves NB comment US90.

论文(除其他外)删除了句子:

A function defined within a class definition is an inline function.

并添加了句子:

In the global module, a function defined within a class definition is implicitly inline ([class.mfct], [class.friend]).


您使用 export module M 的示例将是初始程序的模块化等价物。请注意,编译器已经执行未注释 inline 的内联函数,只是它们在启发式方法中额外使用了 inline 关键字。

Does that mean that, in a modules world, for the compiler to be able to optimize away function calls we have to annotate them as inline even if they are defined in-class?

在某种程度上。

内联是一种 "as if" 优化,如果编译器足够聪明,内联甚至可以发生在翻译单元之间。

也就是说,在单个翻译单元中工作时,内联是最简单的。因此,为了促进简单的内联,inline 声明的函数必须在使用它的任何翻译单元中提供其定义。这并不意味着编译器肯定会内联它(或者肯定不会内联任何非 inline 限定的函数),但它确实使内联过程变得容易得多,因为内联发生在 TU 中而不是他们之间。

Class 在 class 中定义的成员定义,在 pre-module 世界中,隐式声明为 inline。为什么?因为定义在class内。在 pre-module 世界中,TU 之间共享的 class 定义通过文本包含共享。因此,在 class 中定义的成员将在这些 TU 共享的 header 中定义。因此,如果多个 TU 使用相同的 class,那么这些多个 TU 通过包含 class 定义和在 header.

中声明的其成员的定义来实现。

也就是说,无论如何您都包含了定义 ,那么为什么不加入 inline

当然,这意味着函数的定义现在是 class 文本的一部分。如果您更改在 header 中声明的成员的定义,这将强制重新编译包含该 header 的每个文件,递归。即使 class 的接口本身没有改变,你仍然需要重新编译。所以隐式地创建这样的函数 inline 不会改变这一点,所以你也可以这样做。

要在 pre-module 世界中避免这种情况,您可以简单地在 C++ 文件中定义成员,它不会包含在其他文件中。你失去了简单的内联,但你获得了 compile-time.

但事情是这样的:这是使用文本包含作为将 class 传送到多个地方的手段的产物。

在模块化世界中,您可能希望在 class 本身中定义每个成员函数,正如我们在 Java、C#、Python 和其他语言中看到的那样类似。这使代码局部性保持合理,并且避免了必须 re-type 相同的函数签名,从而满足 DRY 的需要。

但是如果所有成员都在 class 定义中定义,那么在旧规则下,所有这些成员都将是 inline。为了使模块允许函数为 inline,二进制模块工件必须包含这些函数的定义。这意味着任何时候您更改此类函数定义中的一行代码,都必须以递归方式构建模块以及依赖于它的每个模块。

删除模块中的隐式-inline 赋予用户与文本包含时代相同的权力,而无需将定义移出 class。您可以选择哪些函数定义是模块的一部分,哪些不是。