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;

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 programinline static thread_local std::string tName = "tls"; // unique per threadstd::string lName = "local"; // for each objectvoid 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 callsstatic inline long sumSize = 0; // bytes allocated so farstatic inline bool doTrace = false; // tracing enabledstatic inline bool inNew = false; // don’t track output inside new overloadspublic:// reset new/memory countersstatic 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个字符的字符串,而不是分配堆内存。)

C++17之 Inline变量相关推荐

  1. 《MySQL 入门教程》第 17 篇 MySQL 变量

    文章目录 17.1 系统变量 17.1.1 查看系统变量 17.1.2 设置系统变量 17.2 用户变量 17.2.1 定义用户变量 17.2.2 访问用户变量 变量是一个拥有名字的对象,可以用于存储 ...

  2. Linux系统编程---17(条件变量及其函数,生产者消费者条件变量模型,生产者与消费者模型(线程安全队列),条件变量优点,信号量及其主要函数,信号量与条件变量的区别,)

    条件变量 条件变量本身不是锁!但它也可以造成线程阻塞.通常与互斥锁配合使用.给多线程提供一个会合的场所. 主要应用函数: pthread_cond_init 函数 pthread_cond_destr ...

  3. jenkins学习17 - 添加环境变量(Environment Injector)

    前言 jenkins在构建job的时候,可以自己添加一些环境变量,在后续的构建步骤中引用环境变量,比如在测试报告中引用 Environment Injector 插件可以在在构建步骤中添加环境变量 P ...

  4. 17. 二元离散型随机变量边际分布律与条件分布律

    文章目录 二元离散型随机变量边际分布律与条件分布律 边际分布 条件分布 二元离散型随机变量边际分布律与条件分布律 边际分布 对于离散型随机变量 (X,Y)(X,Y)(X,Y),分布律为 P(X=xi, ...

  5. Go的变量到底在堆还是栈中分配

    最近试着优化掉一些小对象分配,发现一个很诡异的问题:这段代码会在堆上分配对象! package mainimport ("fmt" )func main() {var a [1]i ...

  6. C++11\14\17\20 新特性整理

    文章目录 C++11 新特性 01 auto 与 decltype 02 defaulted 与 deleted 函数 03 final 与 override 04 尾置返回类型 05 右值引⽤ 06 ...

  7. C++17新特性总结

    参考链接:C++17 一.语言特性 1.1 折叠表达式 C++17中引入了折叠表达式,主要是方便模板编程,分为左右折叠,下图为其解包形式: template <typename... Args& ...

  8. C++11\14\17\20 特性介绍

    C++11 新特性 #01 auto 与 decltype auto: 对于变量,指定要从其初始化器⾃动推导出其类型.⽰例: auto a = 10; // 自动推导 a 为 int auto b = ...

  9. 【C语言】20-static和extern关键字2-对变量的作用

    说明:这个C语言专题,是学习iOS开发的前奏.也为了让有面向对象语言开发经验的程序员,能够快速上手C语言.如果你还没有编程经验,或者对C语言.iOS开发不感兴趣,请忽略 上一讲介绍了static和ex ...

最新文章

  1. 2020年十大机器学习框架
  2. mysql数据库sql语句大全
  3. python求立方尾不变_蓝桥杯:单词分析——————Python
  4. Python爬虫入门(5):URLError异常处理
  5. 【公司金融课堂学习笔记】1、企业融资—风险投资
  6. Detectron:Pytorch-Caffe2-Detectron的一些跟进
  7. WireSkark(六)
  8. php windows共享内存,关于php的共享内存的使用和研究之由起
  9. leetcode--983.最低票价
  10. SenchaTouch2.3.1 中使用listpaging以及pullrefresh插件 做的分页示例
  11. 【深度学习】基于Numpy实现的神经网络进行手写数字识别
  12. 学习Javascript闭包(Closure)(转载+理解心得)
  13. 2022年词达人竞赛笔记
  14. pythonrender函数_Render函数
  15. C++系列(关键字static)
  16. 医院选址c语言课程设计,通信学院2012届本科毕业设计选题结果(学生)2.xls
  17. 机器学习数学基础知识
  18. Jasperreport_6.18的吐血记录五之柱形图
  19. 公开资料整理网是什么_语文老师最常用的5个备课网站推荐,第1个资料质量很高...
  20. iOS学习——获取iOS设备的各种信息

热门文章

  1. 拜关帝爷等于是崇拜小集体主义?
  2. LinuxSir.Org
  3. lammps 案例:CuZr合金建模以及融化固溶结晶过程模拟
  4. 统计检验分析 (本文在chatGPT辅助下完成)
  5. 最近工作中遇到的8个问题
  6. 一个做电商的表哥,一年轻松赚五十万,也没有见他发货,是如何做到的?
  7. Android11 状态栏icon图标的显示流程
  8. Linux log工具:交叉编译log4c及使用示例
  9. Node 文件操作,缓存区 Buffer 理解 ?
  10. 数据结构 线性表试题