c++的一个优点是它支持只使用头文件库的开发。然而,c++ 17之前,头文件中不需要或不提供全局变量或对象时才有可能成为一个库。c++ 17可以在头文件中定义一个内联的变量/对象,如果这个定义被多个编译单元使用,它们都指向同一个惟一的对象:
class MyClass
{
static inline std::string name = ""; // OK since C++17
...
};
inline MyClass myGlobalObj; // OK even if included/defined by multiple CPP files
1. Inline变量的动机
在c++中,类结构中不允许初始化非const静态成员:
class MyClass
{
static std::string name = ""; // Compile-Time ERROR
...
};
包含在多个CPP文件中的头文件中定义类结构之外的变量也是一个错误,:
class MyClass {
static std::string name; // OK
...
};
MyClass::name = ""; // Link ERROR if included by multiple CPP files
问题在于头文件可能被包含多次,多个包含该头文件的CPP文件中都定义了一份MyClass.name。
同样的原因,如果你在头文件中定义类的对象,你会得到一个链接错误:
class MyClass {
...
};
MyClass myGlobalObject; // Link ERROR if included by multiple CPP files
为了解决是上述问题,有一些变通办法:
a. 可以在类/结构中初始化静态const整数数据成员:
class MyClass
{
static const bool trace = false;
...
};
b. 可以定义一个内联函数返回一个静态局部变量:
inline std::string getName()
{
static std::string name = "initial value";
return name;
}
c. 可以定义一个静态成员函数返回值:
std::string getMyGlobalObject()
{
static std::string myGlobalObject = "initial value";
return myGlobalObject;
}
d. 可以使用变量模板(因为c++ 14):
template
T myGlobalObject = "initial value";
e. 可以从静态成员的基类模板派生:
template
class MyClassStatics
{
static std::string name;
};
template
std::string MyClassStatics::name = "initial value";
class MyClass : public MyClassStatics
{
...
};
但是,所有这些方法都会导致显著的开销、较低的可读性和/或使用全局变量的不同方式。此外,全局变量的初始化可能被推迟到第一次使用时,这将禁用我们希望在程序启动时初始化对象的应用程序(例如在使用对象监视进程时)。
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
执行包含头文件或包含这个定义的第一个编译单元时,将执行初始化。
这里使用line与内联函数具有相同的语义,如果不使用inline则[具体请看下面PS那部分],
a. 可以在多个翻译单元中定义,前提是所有定义都是相同的。
b. 必须在使用它的每个翻译单元中定义。
两者都是通过包含来自相同头文件的定义来给出的。程序的结果行为就好像只有一个变量一样。
PS:[内联函数应该在头文件中定义,这一点不同于其他函数。编译器在调用点内联展开函数的代码时,必须能够找到 inline 函数的定义才能将调用函数替换为函数代码,而对于在头文件中仅有函数声明是不够的。当然内联函数定义也可以放在源文件中,但此时只有定义的那个源文件可以用它,而且必须为每个源文件拷贝一份定义(即每个源文件里的定义必须是完全相同的),当然即使是放在头文件中,也是对每个定义做一份拷贝,只不过是编译器替你完成这种拷贝罢了。但相比于放在源文件中,放在头文件中既能够确保调用函数是定义是相同的,又能够保证在调用点能够找到函数定义从而完成内联(替换)]。
你甚至可以应用这个定义原子类型在头文件中:
inline std::atomic
注意,对于std::atomic,通常在定义值时必须初始化它们。
注意,在初始化类型之前,仍然必须确保类型已经完成。例如,如果结构体或类有自己类型的静态成员,则只能在类型声明之后内联定义该成员:
struct MyValue
{
int value;
MyValue(int i) : value{i}
{
}
// one static object to hold the maximum value of this type:
static const MyValue max; // can only be declared here
...
};
inline const MyValue MyValue::max = 1000;
3. constexpr意味着inline
对于静态数据成员,自从C++17起constexpr意味着内联,因此下面的静态成员变量n为定义了静态数据成员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之前,在没有相应定义的情况下声明静态数据成员时加上const就可以在类内初始化:
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
因此,在c++ 17之前,您必须在一个翻译单元中定义D::n:
constexpr int D:: n; //在C++17之前表达式是定义,C++17中是冗余声明,已被弃用。
4. inline变量和thread_local
通过使用thread_local,可以为每个线程创建一个惟一的内联变量:
struct ThreadData
{
inline static thread_local std::string name; // unique name per thread
...
};
inline thread_local std::vector cache; // one cache per thread
作为一个完整的例子,考虑以下头文件:
inlinethreadlocal.hpp
#include
#include
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()的编译单元中使用:
inlinethreadlocal1.cpp
#include "inlinethreadlocal.hpp"
#include
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()的编译单元中使用inlinethreadlocal.hpp头文件,foo在主线程中被调用:
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:");
}
程序输出如下:
5. 使用内联变量来跟踪::new
下面的程序演示了通过只包含该头文件如何使用内联变量跟踪调用::new:
tracknew.hpp
#ifndef TRACKNEW_HPP
#define TRACKNEW_HPP
#include
#include // for malloc()
#include
class TrackNew
{
private:
static inline int numMalloc = 0; // num malloc calls
static inline long sumSize = 0; // bytes allocated so far
static inline bool doTrace = false; // tracing enabled
static inline bool inNew = false; // don’t track output inside new overloads
public:
// reset new/memory counters
static void reset()
{
numMalloc = 0;
sumSize = 0;
}
// enable print output for each new:
static void trace(bool b)
{
doTrace = b;
}
// print current state:
static void status()
{
std::cerr << numMalloc << " mallocs for " << sumSize << " Bytes" << '\n';
}
// implementation of tracked allocation:
static void* allocate(std::size_t size, const char* call)
{
// trace output might again allocate memory, so handle this the usual way:
if (inNew)
{
return std::malloc(size);
}
inNew = true;
// track and trace the allocation:
++numMalloc;
sumSize += size;
void* p = std::malloc(size);
if (doTrace)
{
std::cerr << "#" << numMalloc << " "
<< call << " (" << size << " Bytes) => "
<< p << " (total: " << sumSize << " Bytes)" << '\n';
}
inNew = false;
return p;
}
};
inline void* operator new (std::size_t size)
{
return TrackNew::allocate(size, "::new");
}
inline void* operator new[] (std::size_t size)
{
return TrackNew::allocate(size, "::new[]");
}
#endif // TRACKNEW_HPP
考虑在下面的头文件中使用这个头文件:
racknewtest.hpp
include "tracknew.hpp"
#include
class MyClass
{
static inline std::string name = "initial name with 26 chars";
};
MyClass myGlobalObj; // OK since C++17 even if included by multiple CPP files#
cpp文件tracknewtest.cpp如下:
#include "tracknew.hpp"
#include "tracknewtest.hpp"
#include
#include
int main()
{
TrackNew::status();
TrackNew::trace(true);
std::string s = "an string value with 29 chars";
TrackNew::status();
}
输出取决于何时初始化跟踪以及初始化执行了多少分配。但结尾应该是这样的:
.......
#33 ::new (27 Bytes) => 0x89dda0 (total: 2977 Bytes)
33 mallocs for 2977 Bytes
#34 ::new (30 Bytes) => 0x89db00 (total: 3007 Bytes)
34 mallocs for 3007 Bytes
初始化MyClass::name需要27个字节,初始化main()中的s需要30个字节。(注意,字符串是由大于15个字符的值初始化的,以避免使用实现小/短字符串优化的库在堆上不分配内存(SSO),它在数据成员中存储最多15个字符的字符串,而不是分配堆内存。)