模块接口中的内联含义

met*_*fox 26 c++ language-lawyer c++20 c++-modules

考虑头文件:

class T
{
private:
  int const ID;

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

  int GetID() const noexcept { return ID; }
};
Run Code Online (Sandbox Code Playgroud)

或者,或者:

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; }
Run Code Online (Sandbox Code Playgroud)

在预模块世界中,这些标头可能会以文本形式包含在多个 TU 中,而不会违反 ODR。此外,由于涉及的成员函数相对较小,编译器可能会“内联”(在使用时避免函数调用)这些函数,甚至优化掉一些实例T

在最近关于 C++20 完成会议的报告中,我可以阅读以下声明:

我们澄清了inline模块接口中的含义:意图是未明确声明的函数体不是inline模块 ABI 的一部分,即使这些函数体出现在模块接口中。为了让模块作者更好地控制他们的 ABI,在模块接口的类体中定义的成员函数不再是隐式的inline

我不确定我没有弄错。这是否意味着,在模块世界中,为了让编译器能够优化掉函数调用,我们必须对它们进行注释,inline即使它们是在类中定义的?

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

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; }
};
Run Code Online (Sandbox Code Playgroud)

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

Nic*_*las 13

这是否意味着,在模块世界中,为了让编译器能够优化掉函数调用,我们必须对它们进行注释,inline即使它们是在类中定义的?

在某种程度上。

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

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

在预模块世界中,在类中定义的类成员定义是inline隐式声明的。为什么?因为定义在类内。在预模块世界中,在 TU 之间共享的类定义通过文本包含共享。因此,在类中定义的成员将在这些 TU 之间共享的标头中定义。因此,如果多个 TU 使用同一个类,那么这些多个 TU 就是通过在标头中包含类定义及其成员的定义来实现的。

也就是说,您无论如何都要包含定义,那么为什么不创建它inline呢?

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

为了在预模块世界中避免这种情况,您可以简单地在 C++ 文件中定义成员,该成员不会包含在其他文件中。你失去了简单的内联,但你获得了编译时间。

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

在模块化世界中,您可能希望在类本身中定义每个成员函数,正如我们在其他语言(如 Java、C#、Python 等)中看到的那样。这使代码局部性保持合理,并防止必须重新键入相同的函数签名,从而满足 DRY 的需求。

但是如果所有成员都在类定义中定义,那么在旧规则下,所有这些成员都是inline. 并且为了让模块允许一个函数inline,二进制模块工件必须包含这些函数的定义。这意味着任何时候只要你改变这样一个函数定义中的一行代码,就必须递归地构建模块,以及依赖它的每个模块。

删除隐式inline模块为用户提供了与文本包含时代相同的权力,而不必将定义移出类。您可以选择哪些函数定义是模块的一部分,哪些不是。


Bar*_*rry 8

这来自P1779,前几天刚刚在布拉格采用。从提案:

本文建议从附加到(命名)模块的类定义中定义的函数中删除隐式内联状态。这允许类从避免冗余声明中受益,保持模块作者在声明有或没有内联的函数时提供的灵活性。此外,它允许注入的类模板的朋友(不能在类定义之外进行一般定义)完全是非内联的。它还解决了 NB 注释US90

该论文(除其他外)删除了这句话:

在类定义中定义的函数是内联函数。

并添加了一句话:

在全局模块中,在类定义中定义的函数是隐式内联的([class.mfct], [class.friend])。


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