C++17之 Inline变量

c++的一个优点是它支持只使用头文件库的开发。然而,c++ 17之前,头文件中不需要或不提供全局变量或对象时才有可能成为一个库。c++ 17可以在头文件中定义一个内联的变量/对象,如果这个定义被多个编译单元使用,它们都指向同一个惟一的对象:
 

//hpp
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文件中的头文件中定义类结构之外的变量也是一个错误,:

//hpp
class MyClass
{
	static std::string name; // OK
	//...
};
MyClass::name = ""; // Link ERROR if included by multiple CPP files

问题在于头文件可能被包含多次,多个包含该头文件的CPP文件中都定义了一份MyClass.name。

同样的原因,如果你在头文件中定义类的对象,你会得到一个链接错误:

//hpp
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<typename T = std::string>
T myGlobalObject = "initial value";

e. 可以从静态成员的基类模板派生:

template<typename Dummy>
class MyClassStatics
{
    static std::string name;
};

template<typename Dummy>
std::string MyClassStatics<Dummy>::name = "initial value";

class MyClass : public MyClassStatics<void>
{
...
};

但是,所有这些方法都会导致显著的开销、较低的可读性和/或使用全局变量的不同方式。此外,全局变量的初始化可能被推迟到第一次使用时,这将禁用我们希望在程序启动时初始化对象的应用程序(例如在使用对象监视进程时)。

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<bool> 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;

        C++程序通常都有多个C++源文件组成(其以 .cpp 或 .cc 结尾)。这些文件会单独编
译成模块/二进制文件(通常以 .o 结尾)。链接所有模块/二进制文件形成一个单独的
可执行文件,或是动态库/静态库则是编译的最后一步。
当链接器发现一个特定的符号,被定义了多次时就会报错。举个栗子,现在我们有
一个函数声明 int foo(); ,当我们在两个模块中定义了同一个函数,那么哪一个才
是正确的呢?链接器自己不能做主。这样没错,但是这也可能不是开发者想看到的。
       为了能提供全局可以使用的方法,通常会在头文件中定义函数,这可以让C++的所
有模块都调用头文件中函数的实现(C++中,头文件中实现的函数,编译器会隐式的
使用inline来进行修饰,从而避免符号重复定义的问题)。这样就可以将函数的定义
单独的放入模块中。之后,就可以安全的将这些模块文件链接在一起了。这种方式
也被称为定义与单一定义规则(ODR,One Definition Rule)。看了下图或许能更好
的理解这个规则: 

    如果这是唯一的方法,就不需要只有头文件的库了。只有头文件的库非常方便,因为只需要使用 #include 语句将对应的头文件包含入C++源文件/头文件中后,就可以使用这个库了。当提供普库时,开发者需要编写相应的编译脚本,以便连接器将库模块链接在一起,形成对应的可执行文件。这种方式对于很小的库来说是不必要的。
        对于这样例子, inline 关键字就能解决不同的模块中使用同一符号采用不同实现的方式。当连接器找到多个具有相同签名的符号时,这些函数定义使用 inline 进行声明,链接器就会选择首先找到的那个实现,然后认为其他符号使用的是相同的定义。所有使用 inline 定义的符号都是完全相同的,对于开发者来说这应该是常识。我们的例子中,连接器将会在每个模块中找到 process_monitor::standard_string 符号,因为这些模块包含了 foo_lib.hpp 。如果没有了 inline 关键字,连接器将不知道选择哪个实现,所以其会将编译过程中断并报错。同样的原理也适用于 global_process_monitor 符号。使用 inline 声明所有符号之后,连接器只会接受其找到的第一个符号,而将后续该符号的不同实现丢弃。
        C++17之前,解决的方法是通过额外的C++模块文件提供相应的符号,这将迫使我
们的库用户强制在链接阶段包含该文件。 

       传统的 inline 关键字还有另外一种功能。其会告诉编译器,可以通过实现直接放在
调用它的地方来消除函数调用的过程。这样的话,代码中的函数调用会减少,这样
我们会认为程序会运行的更快。如果函数非常短,那么生成的程序段也会很短(假
设函数调用也需要若干个指令,保护现场等操作,其耗时会高于实际工作的代
码)。当内联函数非常长,那么二进制文件的大小就会变得很大,有时并无法让代
码运行的更快。因此,编译器会将 inline 关键字作为一个提示,可能会对内联函数
消除函数调用。当然,编译器也会将一些函数进行内联,尽管开发者没有使
用 inline 进行提示。 

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;
};

但是,只有在不需要获取static成员变量D::n地址的情况下才可以不用定义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); // ld: error: undefined symbol: D::n

因此,在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<std::string> cache; // one cache per thread

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

inlinethreadlocal.hpp

#include <string>
#include <iostream>
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 <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()的编译单元中使用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 <new>
#include <cstdlib> // for malloc()
#include <iostream>
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 <string>
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 <iostream>
#include <string>
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个字符的字符串,而不是分配堆内存。)

  • 22
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值