C++17之 Inline变量

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 ready{false};

注意,对于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:");
}

程序输出如下:

C++17之 Inline变量_第1张图片

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个字符的字符串,而不是分配堆内存。)

你可能感兴趣的:(C++17,C++)