内联变量如何工作?

How do inline variables work?

在 2016 年 Oulu ISO C++ 标准会议上,一项名为 Inline Variables 的提案被标准委员会投票选入 C++17。

通俗地说,什么是行内变量,它们是如何工作的,它们有什么用?内联变量应该如何声明、定义和使用?

提案第一句:

The ​inline specifier can be applied to variables as well as to functions.

inline 应用于函数的 ¹ 保证效果是允许在多个翻译单元中通过外部链接对函数进行相同的定义。对于 in-practice 表示在 header 中定义函数,可以包含在多个翻译单元中。该提案将这种可能性扩展到变量。

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

直到并包括 C++14,为了支持 class 模板中的 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!

…在 header 文件中。

提案包含措辞

​An inline static data member can be defined in the class definition and may s‌​pecify a ​brace­-or­-equal­-initializer. If the member is declared with the constexpr specifier, it may be redeclared in namespace scope with no initializer (this usage is deprecated; see‌​ D.X). Declarations of other static data members shall not specify a ​brace­-or­-equal­-in‌​itializer

... 这使得上面的内容可以进一步简化为

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

... 正如 T.C 在 a comment 中对此答案所指出的那样。

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


备注:
¹ 对于函数 inline 也有关于优化的暗示效果,编译器应该更愿意用函数机器代码的直接替换来替换对该函数的调用。这个提示可以忽略。

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

内联变量仅可用于在 header 库中定义全局变量。在 C++17 之前,他们不得不使用变通方法(内联函数或模板 hack)。

例如,一种解决方法是将 Meyers’ singleton 与内联函数一起使用:

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

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

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

inline T global;

除了 header 仅库外,还有其他情况需要内联变量提供帮助。 Nir Friedman 在 CppCon 的演讲中谈到了这个话题:What C++ developers should know about globals (and the linker). The part about inline variables and the workarounds starts at 18m9s.

长话短说,如果您需要声明在编译单元之间共享的全局变量,在 header 文件中将它们声明为内联变量很简单,并且避免了 pre-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);
}

notmain.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 upstream.

另请参阅:How do inline variables work?

关于内联变量的 C++ 标准

C++ 标准保证地址相同。 C++17 N4659 standard draft 10.1.6 "The inline specifier":

6 An inline function or variable with external linkage shall have the same address in all translation units.

cppreference 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 nm 表示 u:

"u" The symbol is a unique global symbol. This is a GNU extension to the standard set of ELF symbol bindings. For such a symbol the dynamic linker will make sure that in the entire process there is just one symbol with this name and type in use.

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

Pre-C++ 17: extern const

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

inline 的缺点是:

  • 无法使用此技术创建变量 constexpr,只有 inline 允许:How to declare constexpr extern?
  • 它不太优雅,因为您必须在 header 和 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;
}

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

extern const int notmain_i;

const int* notmain_func();

#endif

GitHub upstream.

Pre-C++17 header 唯一选择

这些不如 extern 解决方案好,但它们有效并且只占用一个内存位置:

一个constexpr函数,因为constexpr implies inline and inline allows (forces) the definition to appear on every translation unit:

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

但是你不能做一些事情,比如获取它的地址,否则它会变成 odr-used,另请参阅:https://en.cppreference.com/w/cpp/language/static "Constant static members" and Defining constexpr static data members

C

在 C 中,情况与 C++ pre C++ 17 相同,我上传了一个示例:What does "static" mean in C?

唯一的区别是,在 C++ 中,const 意味着 static 用于全局变量,但在 C 中不是:C++ semantics of `static const` vs `const`

有什么方法可以完全内联它?

TODO:有没有办法完全内联变量,根本不使用任何内存?

很像预处理器的作用。

这需要某种方式:

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

相关:

在 Ubuntu 18.10、GCC 8.2.0 中测试。