Chapter3 内联变量

出于可移植性和易于整合的目的,在头文件中提供完整的类和库的定义是很重要的。 然而,在C++17之前,只有当这个库既不提供也不需要全局对象的时候才可以这样做。

自从C++17开始,你可以在头文件中以inline的方式 定义 全局变量/对象:

  1. class MyClass {
  2. inline static std::string msg{"OK"}; // OK(自C++17起)
  3. ...
  4. };
  5. inline MyClass myGlobalObj; // 即使被多个CPP文件包含也OK

只要一个编译单元内没有重复的定义即可。此例中的定义即使被多个编译单元使用, 也会指向同一个对象。

3.1 内联变量产生的动机

在C++里不允许在类里初始化非常量静态成员:

  1. class MyClass {
  2. static std::string msg{"OK"}; // 编译期ERROR
  3. ...
  4. };

可以在类定义的外部定义并初始化非常量静态成员,但如果被多个CPP文件同时包含的话又会引发新的错误:

  1. class MyClass {
  2. static std::string msg;
  3. ...
  4. };
  5. std::string MyClass::msg{"OK"}; // 如果被多个CPP文件包含会导致链接ERROR

根据 一次定义原则 (ODR),一个变量或实体的定义只能出现在一个编译单元内—— 除非该变量或实体被定义为inline的。

即使使用预处理来进行保护也没有用:

  1. #ifndef MYHEADER_HPP
  2. #define MYHEADER_HPP
  3. class MyClass {
  4. static std::string msg;
  5. ...
  6. };
  7. std::string MyClass::msg{"OK"}; // 如果被多个CPP文件包含会导致链接ERROR
  8. #endif

问题并不在于头文件是否可能被重复包含多次,而是两个不同的CPP文件都包含了这个头文件, 因而都定义了MyClass::msg

出于同样的原因,如果你在头文件中定义了一个类的实例对象 也会出现相同的链接错误:

  1. class MyClass {
  2. ...
  3. };
  4. MyClass myGlobalObject; // 如果被多个CPP文件包含会导致链接ERROR

解决方法

对于一些场景,这里有一些解决方法:

  • 你可以在一个class/struct的定义中初始化数字或枚举类型的常量静态成员:
    1. class MyClass {
    2. static const bool trace = false; // OK,字面类型
    3. ...
    4. };
    然而,这种方法只能初始化字面类型,例如基本的整数、浮点数、指针类型或者 用常量表达式初始化了所有内部非静态成员的类,并且该类不能有用户自定义的或虚的析构函数。 另外,如果你需要获取这个静态常量成员的地址(例如你想定义一个它的引用)的话 那么你必须在那个编译单元内定义它并且不能在其他编译单元内再次定义。
  • 你可以定义一个返回static的局部变量的内联函数:
    1. inline std::string& getMsg() {
    2. static std::string msg{"OK"};
    3. return msg;
    4. }
  • 你可以定义一个返回该值的static的成员函数:
    1. class MyClass {
    2. static std::string& getMsg() {
    3. static std::string msg{"OK"};
    4. return msg;
    5. }
    6. ...
    7. };
  • 你可以使用变量模板(自C++14起):
    1. template<typename T = std::string>
    2. T myGlobalMsg{"OK"};
  • 你可以为静态成员定义一个模板类: ```cpp template class MyClassStatics { static std::string msg; };

template std::string MyClassStatics::msg{“OK”};

  1. 然后继承它:
  2. ```cpp
  3. class MyClass : public MyClassStatics<>
  4. {
  5. ...
  6. };

然而,所有这些方法都会导致签名重载,可读性也会变差,使用该变量的方式也变得不同。 另外,全局变量的初始化可能会推迟到第一次使用时。 所以那些假设变量一开始就已经初始化的写法是不可行的(例如使用一个对象来监控整个程序的过程)。

3.2 使用内联变量

现在,使用了inline修饰符之后,即使定义所在的头文件被多个CPP文件包含, 也只会有一个全局对象:

  1. class MyClass {
  2. inline static std::string msg{"OK"}; // 自从C++17起OK
  3. ...
  4. };
  5. inline MyClass myGlobalObj; // 即使被多个CPP文件包含也OK

这里使用的inline和函数声明时的inline有相同的语义:

  • 它可以在多个编译单元中定义,只要所有定义都是相同的。
  • 它必须在每个使用它的编译单元中定义

将变量定义在头文件里,然后多个CPP文件再都包含这个头文件,就可以满足上述两个要求。 程序的行为就好像只有一个变量一样。

你甚至可以利用它在头文件中定义原子类型:

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

像通常一样,当你定义std::atomic类型的变量时必须进行初始化。

注意你仍然必须确保在你初始化内联变量之前它们的类型必须是完整的。 例如,如果一个struct或者class有一个自身类型的static成员, 那么这个成员只能在类型声明之后再进行定义:

  1. struct MyType {
  2. int value;
  3. MyType(int i) : value{i} {
  4. }
  5. // 一个存储该类型最大值的静态对象
  6. static MyType max; // 这里只能进行声明
  7. ...
  8. };
  9. inline MyType MyType::max{0};

另一个使用内联变量的例子见追踪所有new调用的头文件。

3.3 constexpr static成员现在隐含inline

对于静态成员,constexpr修饰符现在隐含着inline。 自从C++17起,如下声明 定义 了静态数据成员n

  1. struct D {
  2. static constexpr int n = 5; // C++11/C++14: 声明
  3. // 自从C++17起: 定义
  4. }

和下边的代码等价:

  1. struct D {
  2. inline static constexpr int n = 5;
  3. };

注意在C++17之前,你就可以只有声明没有定义。考虑如下声明:

  1. struct D {
  2. static constexpr int n = 5;
  3. };

如果不需要D::n的定义的话只有上面的声明就够了, 例如当D::n以值传递时:

  1. std::cout << D::n; // OK,ostream::operator<<(int)只需要D::n的值

如果D::n以引用传递到一个非内联函数,并且该函数调用没有被优化掉的话, 该调用将会导致错误。例如:

  1. int twice(const int& i);
  2. std::cout << twice(D::n); // 通常会导致ERROR

这段代码违反了 一次定义原则 (ODR)。如果编译器进行了优化,那么这段代码可能会像预期一样工作 也可能会因为缺少定义导致链接错误。如果不进行优化,那么几乎肯定会因为缺少D::n的定义而 导致错误。 如果创建一个D::n的指针那么更可能因为缺少定义导致链接错误(但在某些编译模式下仍然可能正常编译):

  1. const int* p = &D::n; // 通常会导致ERROR

因此在C++17之前,你必须在一个编译单元内定义D::n:

  1. constexpr int D::n; // C++11/C++14: 定义
  2. // 自从C++17起: 多余的声明(已被废弃)

现在当使用C++17进行构建时,类中的声明本身就成了定义,因此即使没有上边的定义, 上面的所有例子现在也都可以正常工作。上边的定义现在仍然有效但已经成了废弃的多余声明。

3.4 内联变量和thread_local

通过使用thread_local你可以为每个线程创建一个内联变量:

  1. struct ThreadData {
  2. inline static thread_local std::string name; // 每个线程都有自己的name
  3. ...
  4. };
  5. inline thread_local std::vector<std::string> cache; // 每个线程都有一份cache

作为一个完整的例子,考虑如下头文件:

  1. #include <string>
  2. #include <iostream>
  3. struct MyData {
  4. inline static std::string gName = "global"; // 整个程序中只有一个
  5. inline static thread_local std::string tName = "tls"; // 每个线程有一个
  6. std::string lName = "local"; // 每个实例有一个
  7. ...
  8. void print(const std::string& msg) const {
  9. std::cout << msg << '\n';
  10. std::cout << "- gName: " << gName << '\n';
  11. std::cout << "- tName: " << tName << '\n';
  12. std::cout << "- lName: " << lName << '\n';
  13. }
  14. };
  15. inline thread_local MyData myThreadData; // 每个线程一个对象

你可以在包含main()的编译单元内使用它:

  1. #include "inlinethreadlocal.hpp"
  2. #include <thread>
  3. void foo();
  4. int main()
  5. {
  6. myThreadData.print("main() begin:");
  7. myThreadData.gName = "thraed1 name";
  8. myThreadData.tName = "thread1 name";
  9. myThreadData.lName = "thread1 name";
  10. myThreadData.print("main() later:");
  11. std::thread t(foo);
  12. t.join();
  13. myThreadData.print("main() end:");
  14. }

你也可以在另一个定义了foo()函数的编译单元内使用这个头文件, 这个函数会在另一个线程中被调用:

  1. #include "inlinethreadlocal.hpp"
  2. void foo()
  3. {
  4. myThreadData.print("foo() begin:");
  5. myThreadData.gName = "thread2 name";
  6. myThreadData.tName = "thread2 name";
  7. myThreadData.lName = "thread2 name";
  8. myThreadData.print("foo() end:");
  9. }

程序的输出如下:

  1. main() begin:
  2. - gName: global
  3. - tName: tls
  4. - lName: local
  5. main() later:
  6. - gName: thread1 name
  7. - tName: thread1 name
  8. - lName: thread1 name
  9. foo() begin:
  10. - gName: thread1 name
  11. - tName: tls
  12. - lName: local
  13. foo() end:
  14. - gName: thread2 name
  15. - tName: thread2 name
  16. - lName: thread2 name
  17. main() end:
  18. - gName: thread2 name
  19. - tName: thread1 name
  20. - lName: thread1 name