从C++17开始,在编写C++代码时就可以在头文件中定义inline变量。且在编译时也不会报错,如果同时有多份代码文件引用了该头文件,编译器也不会报错。不管怎么说,这是一种进步。实际编写时可以如下代码所示:
1 2 3 4
| class MyClass { inline static std::string strValue{"OK"}; }; inline MyClass myGlobalObj;
|
需要注意的是,编写时在同一个代码文件中要保证定义对象的唯一性。
内联变量的缘起
按照一次定义原则,一个变量或者实体只能出现一个编译单元内,除非这个变量或者实体使用了inline进行修饰。如下面的代码。如果在一个类中定义了一个静态成员变量,然后在类的外部进行初始化,本身符合一次定义原则。但是如果在多个CPP文件同时包含了该头文件,在链接时编译器会报错。
1 2 3 4 5 6
| class MyClass { static std::string msg; ... }; std::string MyClass::msg{"OK"};
|
那么如何解决这个问题呢?可能会有些同学说,将类的定义包含在预处理里面。代码如下:
1 2 3 4 5 6 7 8 9
| #ifndef MYCLASS_H #define MYCLASS_H class MyClass { static std::string msg; ... }; std::string MyClass::msg{"OK"}; #endif
|
这样类定义包含在多个代码文件的时候的就不会有链接错误了吧?实际上,错误依旧存在。那么在C++17以前,有哪些解决方法呢?
编程秘籍
实际上,根据不同的使用场景,可以有不同的方案。
1.可以定义一个返回 static 的局部变量的内联函数。
1 2 3 4
| inline std::string& getMsg() { static std::string msg{"OK"}; return msg; }
|
2.可以定义一个返回该值的 static 的成员函数
1 2 3 4 5 6
| class MyClass { static std::string& getMsg() { static std::string msg{"OK"}; return msg; } };
|
3.可以为静态数据成员定义一个模板类,然后继承它
1 2 3 4 5 6 7 8 9 10
| template<typename = void> class MyClassStatics { static std::string msg; }; template<typename T> std::string MyClassStatics<T>::msg{"OK"}; class MyClass : public MyClassStatics<> { };
|
同样,如果有学习过C++14的同学还会想到使用变量模板,如下所示:
1 2
| template<typename T = std::string> T myGlobalMsg{"OK"}
|
从上面可以看到,及时没有C++17在实际编程时也能解决遇到的问题。但是当跳出来再看这些方法的时候,就会注意到在实际使用时会存在一些问题。如上面的方法会导致签名重载、可读性变差、全局变量初始化延迟等一些问题。变量初始化延迟也会和我们固有的认知产生矛盾。因为我们定义一个变量的时候默认就已经被立即初始化了。
内联变量的使用
C++17中内联变量的使用可以帮助我们解决实际编程中的问题而又不失优雅。使用inline后,即使定义的全局对象被多个文件引用也只会有一个全局对象。如下面的代码,就不会出现之前的链接问题。
1 2 3 4 5
| class MyClass { inline static std::string msg{"OK"}; ... }; inline MyClass myGlobalObj;
|
除此之外,需要还需要注意的是,在一个类的内部定义了一个自身类型的静态变量时需要在类的外部进行重新定义。如:
1 2 3 4 5 6 7 8
| struct MyData { int value; MyData(int i) : value{i} { } static MyData max; ... }; inline MyData MyData::max{0};
|
constexpr static和inline
从C++17开始,如果在编程时继续使用constexpr static修饰变量,实际上编译器就会默认是内联变量。如下面定义的代码:
1 2 3
| struct MY_DATA { static constexpr int n = 5; }
|
这段代码实际上和下面的代码是等效的。
1 2 3
| struct MY_DATA { inline static constexpr int n = 5; }
|
内联变量和thread_local
在支持C++17的编译器编程时使用thread_local可以给每一个线程定义一个属于自己的内联变量。如下面的代码:
1 2 3 4
| struct THREAD_NODE{ inline static thread_local std::string strName; }; inline thread_local std::vector<std::string> vCache;
|
如上,通过thread_local修饰的内联变量就给每一个线程对象创建的属于自己的内联变量。
下面,通过一段代码来对此功能进行说明,先介绍下功能,代码主要定义了一个类,类中包含三个成员变量,分别是内联变量、使用了thread_local修饰了的内联变量以及一个本地的成员变量;除此之外定义了一个自身类型的用thread_local修饰的内联变量,以保证不同的线程拥有自己的内联变量。main函数分别对内联变量进行打印和输出,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| #include <string> #include <iostream> #include <thread> 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; void foo() { myThreadData.print("foo() begin:"); myThreadData.gName = "thread2 name"; myThreadData.tName = "thread2 name"; myThreadData.lName = "thread2 name"; myThreadData.print("foo() end:"); } int main() { myThreadData.print("main() begin:"); myThreadData.gName = "thraed1 name"; myThreadData.tName = "thread1 name"; myThreadData.lName = "thread1 name"; myThreadData.print("main() later:"); std::thread t(foo); t.join(); myThreadData.print("main() end:"); }
|
代码执行结果为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| main() begin: - gName: global - tName: tls - lName: local main() later: - gName: thraed1 name - tName: thread1 name - lName: thread1 name foo() begin: - gName: thraed1 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
|
从执行结果可以看出:在代码28-30行对变量赋值后再次打印原来的值已经被修改,但是在接下来的线程执行中,线程函数foo()对内联变量重新进行赋值。最后第34行的代码输出中,只有全量内联变量被线程函数的值覆盖,使用了thread_local修饰的内联变量依旧是main线程中的赋值,这也证明了前面的描述。既:thread_local修饰后,可以保证每个线程独立拥有自己的内联变量。