第三章 内联变量

C++的一个优点是它支持header-only(译注:即只有头文件)的库。然而,截止C++17,header-only的库也不能有全局变量或者对象出现。

C++17后,你可以在头文件中使用inline定义变量,如果这个变量被多个翻译单元(translation unit)使用,它们都会指向相同对象:

class MyClass {
  static inline std::string name = ""; // OK since C++17
  ...
};
inline MyClass myGlobalObj; // OK even if included/defined by multiple CPP files

3.1 内联变量的动机

C++不允许在class内部初始化非const静态成员:

class MyClass {
  static std::string name = "";  // Compile-Time ERROR
  ...
};

在class外面定义这个变量定义这个变量,且变量定义是在头文件中,多个CPP文件包含它,仍然会引发错误:

class MyClass {
  static std::string name; // OK
  ...
};
MyClass::name = ""; // Link ERROR if included by multiple CPP files

根据一处定义规则(one definition 入了,ODR),每个翻译单元只能定义变量最多一次。

即便有预处理保护(译注:也叫头文件保护,header guard)也没有用:

#ifndef MYHEADER_HPP
#define MYHEADER_HPP
class MyClass {
  static std::string name; // OK
  ...
};
MyClass.name = ""; // Link ERROR if included by multiple CPP files
#endif

不是因为头文件可能被包含多次,问题是两个不同的CPP如果都包含这个头文件,那么MyClass.name可能定义两次。

同样的原因,如果你在头文件中定义一个变量,你会得到一个链接时错误:

class MyClass {
  ...
};
MyClass myGlobalObject; // Link ERROR if included by multiple CPP files

临时解决方案

这里有一些临时的应对措施:

  • 你可以在class/struct内初始化一个static const整型数据成员:
class MyClass {
  static const bool trace = false;
  ...
};
  • 你可以定义一个返回局部static对象的内联函数:
inline std::string getName() {
  static std::string name = "initial value";
  return name;
}
  • 你可以定义一个static成员函数返回它的值:
std::string getMyGlobalObject() {
  static std::string myGlobalObject = "initial value";
  return myGlobalObject;
}
  • 你可以使用变量模板(C++14及以后):
template<typename T = std::string>
T myGlobalObject = "initial value";
  • 你可以继承一个包含static成员的类模板:
template<typename Dummy>
class MyClassStatics
{
  static std::string name;
};
template<typename Dummy>
std::string MyClassStatics<Dummy>::name = "initial value";
class MyClass : public MyClassStatics<void> {
  ...
};

但是这些方法都有不小的负载,可读性也比较差,想要使用全局变量也比较困难。除此之外,全局变量的初始化可能会推迟到它第一次使用的时候,这使得应用程序不能在启动的时候把对象初始化好。(比如用一个对象监控进程)。

3.2 使用内联变量

现在,有了inline,你可以在头文件中定义一个全局可用的变量,它可以被多个CPP文件包含:

class MyClass {
  static inline std::string name = ""; // OK since C++17
  ...
};
inline MyClass myGlobalObj; // OK even if included/defined by multiple CPP files

初始化发生在第一个包含该头文件的翻译单元。

形式化来说,在变量前使用inline和将函数声明为inline有相同的语义:

  • 如果每个定义都是相同的,那么它可以在多个翻译单元定义
  • 它必须在使用它的每个翻译单元中定义

两者都是通过包含来自同一头文件的定义来实现的。最终程序的行为就像是只有一个变量。

你甚至可以在头文件中定义原子类型的变量:

inline std::atomic<bool> ready{false};

注意,对于std::atomic,通常在定义它的时候你还得初始化它。

这意味着,你仍然必须保证在你初始化它之前类型是完全的(complete)。比如,如果一个struct或者class有一个static成员,类型是自身,那么该成员只能在该类型被声明后才能使用。

struct MyType {
  int value;
  MyType(int i) : value{i} {
  }
// one static object to hold the maximum value of this type:
  static MyType max; // can only be declared here
  ...
};
inline MyType MyType::max{0};

参见另一个使用内联变量的例子,它会使用头文件跟踪所有new调用

3.3 constexpr隐式包含inline

对于static数据成员,constexpr现在隐式包含inline的语义,所以下面的声明在C++17后会定义static数据成员n:

struct D {
  static constexpr int n = 5; // C++11/C++14: declaration
                              // since C++17: definition
};

换句话说,它与下面的代码一样:

struct D {
  inline static constexpr int n = 5;
};

在C++17之前,有时候你也可以只声明不定义。考虑下面的声明:

struct D {
  static constexpr int n = 5;
};

如果不需要D::n的定义,这就足够了,例如,D::n只通过值传递的话:

std::cout << D::n; // OK (ostream::operator<<(int) gets D::n by value)

如果D::n是传引用到非内联函数,并且/或者函数调用没有优化,那么就是无效的。比如:

int inc(const int& i);
std::cout << inc(D::n); // usually an ERROR

这段代码违背了一处定义规则(ODR)。当使用带优化的编译器构建时,它可能正常工作,或者抛出链接时错误指出缺少定义。当使用不带优化的编译器时,几乎可以确定这段代码会由于缺少D::n的定义而拒绝编译:

因此,在C++17前,你不得不在相同的翻译单元定义D::n

constexpr int D::n; // C++11/C++14: definition
                    // since C++17: redundant declaration (deprecated)

当使用C++17构建,在class中的声明本身就是一个定义,所以这段代码就算没有前面的定义也是有效的。前面的定义也是可以的,但是已经废弃。

3.4 内联变量和thread_local

使用thread_local你可以让每个线程拥有一个内联变量:

struct ThreadData {
  inline static thread_local std::string name; // unique name per thread
...
};

inline thread_local std::vector<std::string> cache; // one cache per thread

为了演示一个完整的例子,考虑下面的头文件:

// lang/inlinethreadlocal.hpp
#include <string>
#include <iostream>

struct MyData {
  inline static std::string gName = "global"; // unique in program
  inline static thread_local std::string tName = "tls"; // unique per thread
  std::string lName = "local"; // for each object
  ...
  void print(const std::string& msg) const {
    std::cout << msg << '\n';
    std::cout << "- gName: " << gName << '\n';
    std::cout << "- tName: " << tName << '\n';
    std::cout << "- lName: " << lName << '\n'; }
};

inline thread_local MyData myThreadData; // one object per thread

你可以在有main()的翻译单元使用它:

// lang/inlinethreadlocal1.cpp
#include "inlinethreadlocal.hpp" 
#include <thread>
void foo();

int main()
{
  myThreadData.print("main() begin:");
  myThreadData.gName = "thread1 name";
  myThreadData.tName = "thread1 name";
  myThreadData.lName = "thread1 name";
  myThreadData.print("main() later:");
  std::thread t(foo);
  t.join();
  myThreadData.print("main() end:");
}

你可以在另一个定义foo()的翻译单元使用头文件,其中foo()被不同的线程调用:

// lang/inlinethreadlocal2.cpp
#include "inlinethreadlocal.hpp"

void foo()
{
  myThreadData.print("foo() begin:");
  myThreadData.gName = "thread2 name";
  myThreadData.tName = "thread2 name";
  myThreadData.lName = "thread2 name";
  myThreadData.print("foo() end:");
}

程序的输出如下:

main() begin:
- gName: global
- tName: tls
- lName: local
main() later:
- gName: thread1 name
- tName: thread1 name
- lName: thread1 name
foo() begin:
- gName: thread1 name
- tName: tls
- lName: local
foo() end:
- gName: thread2 name
- tName: thread2 name
- lName: thread2 name
main() end:
- gName: thread2 name
- tName: thread1 name
- lName: thread1 name

3.5 后记

David Krauss的文档https://wg21.link/n4147是内联变量产生的动机。内联变量最初由Hal Finkel和Richard Smith在https://wg21.link/n4424中提出。最后这个特性的公认措辞是由Hal Finkel和Richard Smith在 https://wg21.link/p0386r2中给出。