C++有个强大之处是支持header-only(仅头文件)的库。然而直到C++17,只有该库不用到全局变量/对象,才能做到header-only。
从C++17开始,可以在头文件中将变量/对象定义为 inline
,如果此定义被多个转译单元使用,它们也全都指向同一个唯一的对象:
-
class MyClass {
-
inline static std::string msg{"OK"}; // C++17没问题
-
...
-
};
-
inline MyClass myGlobalObj; // 即使被多个CPP文件include也没问题
引入 inline
变量的动机
在C++中,不允许类/结构体内初始化非 const
静态成员变量:
-
class MyClass {
-
static std::string msg{"OK"}; // 编译错误
-
...
-
};
在类/结构体外定义变量,如果这个定义是在头文件中,而这头文件被多个cpp文件包含,也是不行的:
-
class MyClass {
-
static std::string msg;
-
...
-
};
-
std::string MyClass::msg{"OK"}; //如果被多个cpp文件包含,则链接错误
根据一次定义规则(one definition rule,ODR),一个(没有 inline
)的变量/对象必须在且仅在一个转译单元(一个cpp文件)中定义一次,即使加了包含保护宏也没用:
-
#ifndef MYHEADER_HPP
-
#define MYHEADER_HPP
-
class MyClass {
-
static std::string msg;
-
...
-
};
-
std::string MyClass::msg{"OK"}; // 如果被多个cpp文件包含,则链接错误
-
#endif
问题不在于头文件被包含了多次,而是有2个不同的cpp文件都包含了该头文件导致都定义了 MyClass::msg
。
基于同样的原因,如果在头文件里定义了你自己的类的实例,也会链接错误:
-
class MyClass {
-
...
-
};
-
MyClass myGlobalObject;// 如果被多个cpp文件包含,则链接错误
变通方法
对于有些情况,是有变通方法的:
-
可以在类/结构体中初始化字面量类型的静态
const
成员变量:
-
class MyClass {
-
static const bool trace = false; // 可以,是字面量类型
-
...
-
};
然后,这仅限于字面量类型,比如基础的整形,浮点数,或指针类型,如果是类,则仅限于有常量表达式初始化的非静态静态成员变量,不能有用户自定义类型或虚析构函数。
-
可以定义
inline
函数,返回一个static
的局部变量:
-
inline std::string& getMsg() {
-
static std::string msg{"OK"};
-
return msg;
-
}
-
可以定义
static
成员函数返回该值:
-
class MyClass {
-
static std::string& getMsg() {
-
static std::string msg{"OK"};
-
return msg;
-
}
-
...
-
};
-
可以使用变量模板(C++14引入):
-
template<typename T = std::string>
-
T myGlobalMsg{"OK"};
-
可以为静态成员变量定义类模板:
-
template<typename = void>
-
class MyClassStatics
-
{
-
static std::string msg;
-
};
-
template<typename T>
-
std::string MyClassStatics<T>::msg{"OK"};
还可以派生:
-
class MyClass : public MyClassStatics<>
-
{
-
...
-
};
但是所有这些办法都导致了巨大的开销,更差的可读性以及绕弯的方式使用全局变量。另外,全局变量的初始化可能被推迟到它第一次被使用,而有时我们想在程序启动时就初始化全局对象(比如想用个对象监控整个进程),这就不行了。
使用 inline
变量
现在,有了 inline
,你只需要在某个头文件中定义一次,就可以用上单一的全局有效的对象,还可以在多个cpp文件中包含:
-
class MyClass {
-
inline static std::string msg{"OK"}; // 从 C++17开始就没问题
-
...
-
};
-
inline MyClass myGlobalObj; // 即使在多个cpp文件中包含也没问题
初始化是在进入到包含该头文件或这些定义的第一个转译单元(一个cpp文件编译成.o)时进行的。
形式上, inline
关键字用在这里跟函数声明为 inline
有相同的语义:
-
可以在多个转义单元内定义,所有定义都是同一个。
-
必须在每个用到的转译单元内定义。
2个cpp文件包含了相同的头文件,因为有了相同的定义。程序的行为就跟只有一个变量一样。
你可以试一下把atomic
类型定义到头文件:
-
inline std::atomic<bool> ready{false};
一般用 std::atomic
,必须在定义的时候就初始化。
注意,仍然要保证初始化时类型是完整的。例如,如果一个 struct
或 class
有一个自定义类型的静态成员变量,则该成员变量只能在该类型声明后被定义为 inline
:
-
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};
constexpr
隐含了 inline
对于静态成员变量,现在 constexpr
隐含了 inline
,因此从C++17开始,下面的声明也就定义了静态成员变量 n
:
-
struct D {
-
static constexpr int n = 5; // C++11/C++14: 声明
-
// since C++17: 定义
-
};
跟下面的代码相同:
-
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; //没问题,D::n以值传递给ostream::operator<<(int)
如果 D::n
以引用传递给非inline的函数,或是函数调用没被优化掉,这就不合法了。例如:
-
int twice(const int& i);
-
std::cout << twice(D::n); //一般会报错
此代码没有优化,遵循一次定义规则(ODR)。当被最优化编译器编译时,它可能会如愿正常工作,也可能会报个链接错误说缺少定义。当被无优化编译时,它几乎肯定会被报错说缺少 D::n
的定义。创建一个指针指向静态成员变量则更会因为缺少定义而被报链接错误(但可能有些编译器模式仍然能工作):
-
const int* p = &D::n; //通常会报错
同理,C++17之前,要在某个转译单元中明确定义 D::n
:
-
constexpr int D::n; // C++11/C++14: 定义
-
// C++17: 重复声明 (已弃用)
现在,按C++17编译,类内的声明被它自己定义,所以前面所有示例不需要以前那种定义也能用。以前那种定义仍然有效,但已被弃用。
inline
变量和 thread_local
使用 thread_local
可以使得 inline
变量为每个线程生成一份变量:
-
struct ThreadData {
-
inline static thread_local std::string name; //一个线程一个name
-
...
-
};
-
inline thread_local std::vector<std::string> cache; //一个线程一个cache
完整的例子如下:
lang/inlinethreadlocal.hpp
-
#include <string>
-
#include <iostream>
-
struct MyData {
-
inline static std::string gName = "global"; // 程序中唯一
-
inline static thread_local std::string tName = "tls"; // 每个线程中唯一
-
std::string lName = "local"; // 每个对象
-
...
-
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; // 一个线程一个对象
然后可以在有 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()
,并在其他线程中使用,然后使用前面的头文件:
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