内联如何影响模块接口中的成员函数

How does inline affect member functions in module interfaces?

本文关键字:接口 模块 函数 成员 影响 何影响      更新时间:2023-10-16

考虑头文件:

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; }

在模块之前的世界中,这些标头可能在文本上包含在多个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; }
};

尽管我仍然没有一个支持模块的编译器,但我希望在适当的时候开始使用inline,以最大限度地减少未来的重构。

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

在某种程度上。

内联是一种"好像"优化,如果编译器足够聪明,即使在翻译单元之间也可以进行内联。

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

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

也就是说,您无论如何都包含了的定义,那么为什么不将其设为inline呢?

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

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

但事情是这样的:这是一个使用文本包含作为将类传递到多个位置的手段的工件。

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

但是,如果所有成员都是在类定义中定义的,那么根据旧规则,所有这些成员都将是inline。为了让模块允许函数为inline,二进制模块工件必须包括这些函数的定义。这意味着,每当你在这样的函数定义中更改哪怕一行代码时,都必须递归地构建模块,以及依赖它的每个模块。

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

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

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

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

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

并添加了以下句子:

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


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