内联变量是如何工作的?

在2016年的 Oulu ISO C + + 标准会议上,标准委员会投票通过了一项名为 内联变量的提案。

用外行人的话说,什么是内联变量,它们是如何工作的,它们有什么用处?如何声明、定义和使用内联变量?

94092 次浏览

建议的第一句话:

inline说明符可以应用于变量和函数。

inline应用于函数时的保证效果是允许在多个翻译单元中以相同的方式定义具有外部连接的函数。在实践中,这意味着在一个标题中定义函数,该函数可以包含在多个翻译单元中。该提案将这种可能性扩大到了变量。

因此,实际上(现在已经被接受的)方案允许您使用 inline关键字在头文件中定义一个外部链接 const名称空间范围变量或任何 static类数据成员,这样,当该头文件包含在多个翻译单元中时产生的多个定义对于链接器来说是可以的——它只是选择其中的

在 C + + 14之前,为了在类模板中支持 static变量,内部机制一直存在,但是没有方便的方法来使用这个机制。人们不得不采取这样的手段

template< class Dummy >
struct Kath_
{
static std::string const hi;
};


template< class Dummy >
std::string const Kath_<Dummy>::hi = "Zzzzz...";


using Kath = Kath_<void>;    // Allows you to write `Kath::hi`.

从 C + + 17开始,我相信一个人可以只写

struct Kath
{
static std::string const hi;
};


inline std::string const Kath::hi = "Zzzzz...";    // Simpler!

在头文件里。

该提案包括措辞

内联静态数据成员可以在类定义中定义,并且可以指定一个大括号或等于初始值设定项。如果使用 constexpr说明符声明成员,则可以在名称空间范围内重新声明它,而不使用初始值设定项(不推荐使用此方法; 请参阅 D.X)。其他静态数据成员的声明不应指定大括号或等号初始化器

这样就可以把上述问题简化为

struct Kath
{
static inline std::string const hi = "Zzzzz...";    // Simplest!
};

正如 T.C 在 评论中对这个问题的回答。

此外,​constexpr说明符暗示 inline用于静态数据成员和函数。


备注: 1对于函数“内联”也有一个关于优化的暗示效果,编译器应该更喜欢用函数的机器代码直接替换这个函数的调用。这个暗示可以忽略。

内联变量与内联函数非常相似。它向链接器发出信号,即使在多个编译单元中看到变量,也只应该存在该变量的一个实例。链接器需要确保不再创建副本。

内联变量可用于仅在标头库中定义全局变量。在 C + + 17之前,他们必须使用变通方法(内联函数或模板技巧)。

例如,一种变通方法是使用带有内联函数的 Meyers 的单身生活:

inline T& instance()
{
static T global;
return global;
}

这种方法有一些缺点,主要是在性能方面。模板解决方案可以避免这种开销,但是很容易出错。

使用内联变量,您可以直接声明它(不会得到多重定义链接器错误) :

inline T global;

除了只有头文件库之外,还有其他内联变量可以提供帮助的情况。尼尔 · 弗里德曼(Nir Friedman)在 CppCon: C + + 开发人员应该了解的关于全局变量(和链接器)的知识的演讲中谈到了这个话题。关于内联变量和解决方案 从18m9开始的部分。

长话短说,如果您需要声明编译单元之间共享的全局变量,那么在头文件中将它们声明为内联变量非常简单,并且避免了 C + + 17之前的变通方法的问题。

(例如,如果你明确希望使用惰性初始模式,仍然可以使用 Meyers 的单例模式。)

最小可运行示例

这个了不起的 C + + 17特性允许我们:

Main.cpp

#include <cassert>


#include "notmain.hpp"


int main() {
// Both files see the same memory address.
assert(&notmain_i == notmain_func());
assert(notmain_i == 42);
}

不是 Main.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP


inline constexpr int notmain_i = 42;


const int* notmain_func();


#endif

不是 main.cpp

#include "notmain.hpp"


const int* notmain_func() {
return &notmain_i;
}

编译并运行:

g++ -c -o notmain.o -std=c++17 -Wall -Wextra -pedantic notmain.cpp
g++ -c -o main.o -std=c++17 -Wall -Wextra -pedantic main.cpp
g++ -o main -std=c++17 -Wall -Wextra -pedantic main.o notmain.o
./main

GitHub 上游。

参见: 内联变量是如何工作的?

内联变量的 C + + 标准

C + + 标准保证地址是相同的。“ a href =”https://github.com/cplusplus/draft/blob/master/paper/N4659.pdf”rel = “ noReferrer”> C + + 17 N4659标准草案 10.1.6“内联说明符”:

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

https://en.cppreference.com/w/cpp/language/inline解释说,如果 static没有给出,那么它有外部连接。

GCC 内联变量实现

我们可以观察它是如何通过以下方式实施的:

nm main.o notmain.o

其中包括:

main.o:
U _GLOBAL_OFFSET_TABLE_
U _Z12notmain_funcv
0000000000000028 r _ZZ4mainE19__PRETTY_FUNCTION__
U __assert_fail
0000000000000000 T main
0000000000000000 u notmain_i


notmain.o:
0000000000000000 T _Z12notmain_funcv
0000000000000000 u notmain_i

man nmu说:

“ u”符号是一个独特的全球性符号。这是对 ELF 符号绑定标准集的 GNU 扩展。对于这样一个符号,动态链接器将确保在整个过程中 只有一个符号使用这个名称和类型。

所以我们看到有一个专用的 ELF 扩展。

Pre-C + + 17: extern const

在 C + + 17之前,在 C 中,我们可以使用 extern const实现非常类似的效果,这将导致使用单个内存位置。

inline的缺点是:

  • 使用这种技术不可能使变量 constexpr,只有 inline允许: 如何声明 conexpr extern?
  • 它不那么优雅,因为必须在头文件和 cpp 文件中分别声明和定义变量

Main.cpp

#include <cassert>


#include "notmain.hpp"


int main() {
// Both files see the same memory address.
assert(&notmain_i == notmain_func());
assert(notmain_i == 42);
}

不是 main.cpp

#include "notmain.hpp"


const int notmain_i = 42;


const int* notmain_func() {
return &notmain_i;
}

不是 Main.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP


extern const int notmain_i;


const int* notmain_func();


#endif

GitHub 上游。

Pre-C + + 17标头的唯一选择

这些都不如 extern解决方案,但它们可以工作,而且只占用一个内存位置:

一个 constexpr函数,因为 ABC0意味着 inlineinline 允许(强制)定义出现在每个翻译单元上:

constexpr int shared_inline_constexpr() { return 42; }

我打赌任何一个好的编译器都会内联调用。

还可以使用 constconstexpr静态整数变量,如下所示:

#include <iostream>


struct MyClass {
static constexpr int i = 42;
};


int main() {
std::cout << MyClass::i << std::endl;
// undefined reference to `MyClass::i'
//std::cout << &MyClass::i << std::endl;
}

但是你不能这样做,比如获取它的地址,否则它会变得很奇怪,参见: https://en.cppreference.com/w/cpp/language/static“常量静态成员”和 定义 conexpr 静态数据成员

C

在 C 中,情况与 C + + pre C + + 17相同,我在 “ static”在 C 语言中是什么意思?上传了一个示例

唯一的区别是,在 C + + 中,const对于全局变量意味着 static,但是在 C: “静态 const”与“ const”的 C + + 语义中没有

有什么办法能把它完全嵌入吗?

TODO: 有没有什么方法可以完全内联这个变量,而不需要使用任何内存?

就像预处理器一样。

这需要以某种方式:

  • 禁止或检测变量的地址是否被采用
  • 将该信息添加到 ELF 对象文件中,并让 LTO 对其进行优化

相关阅读:

在 Ubuntu 18.10,GCC 8.2.0中测试。