最近在看标准库里的type_traits的时候发现了个有趣的地方,几乎所有在标准库里的变量模板都是inline的!

不仅常见的实现上(libstdc++、libc++、ms stl)都是inline的,标准里给的形式定义也是inline的。

比如微软开源的stl实现:https://github.com/microsoft/STL/blob/main/stl/inc/type_traits#L73

_EXPORT_STD template <class _Trait>
_INLINE_VAR constexpr bool negation_v = negation<_Trait>::value; _EXPORT_STD template <class _Ty>
_INLINE_VAR constexpr bool is_void_v = is_same_v<remove_cv_t<_Ty>, void>;

其中_INLINE_VAR这个宏的实现在这里:

// P0607R0 Inline Variables For The STL
#if _HAS_CXX17
#define _INLINE_VAR inline
#else // _HAS_CXX17
#define _INLINE_VAR
#endif // _HAS_CXX17

可以看到如果编译器支持c++17的话这些模板变量就是inline的。

为什么要这样做呢?如果不使用inline又会要什么后果呢?带着这些疑问我们接着往下看。

c++的linkage

首先复习下c++的linkage,国内一般会翻译成“链接性”。因为篇幅有限,所以我们不关注“无链接”、“语言链接”和“模块链接”,只关注内部链接外部链接这两个。

内部链接(internal linkage):符号(粗暴得理解成变量,函数,类等等有名字的东西)仅仅在当前编译单元内部可见,不同编译单元之间可以存在同名的符号,他们是不同实体。

看个例子:

// value.h
static int a = 1; // a.cpp
#include "value.h" void f() {
std::cout << "f() address of a: " << &a << "\n";
} // b.cpp
#include "value.h" void g() {
std::cout << "g() address of a: " << &a << "\n";
} // main.cpp
void f();
void g(); int main() {
f();
g();
}

注意,不要像上面那样写代码,尤其是把具有内部链接的非常量变量写在头文件里。编译并运行:

$ g++ -Wall -Wextra a.cpp b.cpp main.cpp
$ ./a.out f() address of a: 0x564b7892e004
g() address of a: 0x564b7892e01c

可以看到确实有两个不同的实体存在。内部链接最大的好处在于可以实现一定程度上的隔离,但缺点是要付出生成文件体积和运行时内存上的代价,且不如命名空间和模块好使。

这个例子可能看不出,因为只有两个编译单元用了这个模板变量,所以只浪费了一个size_t的内存,在我的机器上是8字节。但项目里往往有成百上千甚至上万个编译单元,而且使用的模板变量不止一个,那么浪费的资源就很可观了。

外部链接(external linkage):符号可以被所以编译单元看见,且只能被定义一次。

例子:

// value.h
// extern int a = 1; 这么写是声明的同时定义了a,在头文件里这么干会导致a重复定义
extern int a; // a.cpp
#include "value.h" int a = 1; // 随便在哪定义都行 void f() {
std::cout << "f() address of a: " << &a << "\n";
} // b.cpp
#include "value.h" void g() {
std::cout << "g() address of a: " << &a << "\n";
} // main.cpp
void f();
void g(); int main() {
f();
g();
}

编译并运行:

$ g++ -Wall -Wextra a.cpp b.cpp main.cpp
$ ./a.out f() address of a: 0x55f5825f8040
g() address of a: 0x55f5825f8040

可以看到这时候就只有一个实体了。

那么什么样的东西会有内部链接,什么又有外部链接呢?

内部链接:所有匿名命名空间里的东西(哪怕声明成extern) + 标记成static的变量、变量模板、函数、函数模板 + 不是模板不是inline没有volatile或extern修饰的常量(const和constexpr)。

外部链接:非static函数、枚举和类天生有外部链接,除非在匿名命名空间里 + 排除内部链接规定的之后剩下的所有模板

说了半天,这和标准库用inline变量有什么关系吗?

还真有,因为内部链接最后一条规则那里的“非模板和非内联”是c++17才加入的,而模板变量c++14就有了,所以一个很麻烦的问题出现了:

template <typename T>
constexpr bool is_void_t = is_void<T>::value;

在这里is_void_t按照c++14的规则,可以是内部链接的。这样有什么问题?一般来说问题不大,编译器会尽可能把常量全部优化掉,但在这个常量被ODR-used(比如取地址或者绑定给函数的引用参数),这个常量就没法直接优化掉了,编译器只能乖乖地生产两个is_void_t的实例。而且这个is_void_t必须是常量,否则可以任意修改它的值,以及不是编译期常量的话没法在其他的模板里使用。

另一个问题在于,c++14忘记更新ODR原则的定义,漏了变量模板,虽然g++上变量模板和其他模板一样可以存在多次定义,但因为标准里没给出具体说法所以存在很大的风险。

c++社区最喜欢的一句格言是:“Don't pay for what you don't use.”

所以c++17的一个提案在增加了inline变量之后建议标准库里把模板变量和static constexpr都改为inline constexprhttps://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0607r0.html

inline变量

为什么提案里加上inline就能解决问题了呢?这就要了解下inline变量会带来什么了。

inline在c++里的意义比起内联,更确切的是允许某个object(这里不是面向对象那个object)被定义多次。但前提是每个定义都必须是相同的,且在需要这个object的地方必须要能看到它的完整定义,否则是未定义行为

对于这样允许多次定义的东西,链接器最后会选择其中一个定义生成真正的实体变量/类/函数。这就是为什么所以定义都必须一样的原因。

看例子:

// value.h
// 例子,Size返回sizeof的值
template <typename T>
struct Size {
static_assert(!std::is_same_v<T, void>, "can not be void");
static constexpr std::size_t value = sizeof(T);
}; // 注意这里
template <typename T>
inline constexpr std::size_t size_v = Size<T>::value; // a.cpp
#include "value.h" void f() {
std::cout << "f() address of size_v: " << &size_v<int> << "\n";
} // b.cpp
#include "value.h" void g() {
std::cout << "g() address of a: " << &size_v<int> << "\n";
} // main.cpp
void f();
void g(); int main() {
f();
g();
}

编译并运行:

$ g++ -Wall -Wextra a.cpp b.cpp main.cpp
$ ./a.out f() address of a: 0x5615acde601c
g() address of a: 0x5615acde601c

只存在一个实体,看符号表的话也只有一个size_v。

这样其实就上一节说到的所有问题:

  1. c++17新加了通常情况下模板变量和inline变量是外部链接的规定,因此加上inline解决了模板变量常量链接性上的问题
  2. inline变量允许被多次定义,因此就算ODR规则忘记更新或者重新考虑后改变了规则也没问题(当然现在已经明确模板变量可以多次定义了)
  3. 比起加static,使用inline不会生成多余的东西

当然这些只是inline变量带来的附加优点,真正让c++加入这一特性的原因因为篇幅这里就不详细展开了,有兴趣可以深入了解哦。

constexpr不是隐式inline的吗

这话只对了一半。

因为constexpr只对函数静态成员变量产生隐式的inline。

如果你给一个正常的有namespace scope(在文件作用域或者namespace里)变量加上constexpr,它只有const和编译期计算两个效果。

所以只加constexpr是没用的。

我不写inline会有什么问题吗

既然新标准补全了ODR规则,那我可以不再给模板变量加上inline吗?

我们把上上节的例子里的inline去掉:

// value.h
// 例子,Size返回sizeof的值
template <typename T>
struct Size {
static_assert(!std::is_same_v<T, void>, "can not be void");
static constexpr std::size_t value = sizeof(T);
}; // 注意这里
template <typename T>
constexpr std::size_t size_v = Size<T>::value; // a.cpp
#include "value.h" void f() {
std::cout << "f() address of size_v: " << &size_v<int> << "\n";
} // b.cpp
#include "value.h" void g() {
std::cout << "g() address of a: " << &size_v<int> << "\n";
} // main.cpp
void f();
void g(); int main() {
f();
g();
}

编译并运行:

$ g++ -Wall -Wextra -std=c++20 a.cpp b.cpp main.cpp
$ ./a.out f() address of a: 0x55fb0cfeb020
g() address of a: 0x55fb0cfeb010

这时候结果很有意思,g++12.2.0在生成的二进制上仍然表现的像是产生了内部链接,而clang14.0.5则和标准描述的一致,产生的结果是正常的:

$ clang++ -Wall -Wextra -std=c++20 a.cpp b.cpp main.cpp
$ ./a.out f() address of a: 0x56184ee30008
g() address of a: 0x56184ee30008

更有意思的在于,如果我把size_v的constexpr去掉,那么size_v就会表现成正常的外部链接:

// 注意这里
template <typename T>
std::size_t size_v = Size<T>::value;
$ g++ -Wall -Wextra -std=c++20 a.cpp b.cpp main.cpp
$ ./a.out f() address of a: 0x5586c90ef038
g() address of a: 0x5586c90ef038

所以看上去g++在判断是否是内部链接的规则上没有遵照c++17标准(还记得老版的标准吗,非inline的constexpr模板变量会被认为具有内部链接),暂时没有进一步去查证,所以没法确定这是g++自己的特性还是单纯只是bug。

如果指定inline,两者的结果是一致的。

所以我不加inline会有什么后果:

  1. 如果你在用c++20或者更新的版本,那么语法上没有任何问题;否则在语法上也处于灰色地带,在参考资料中的第三个链接里就描述了这个原因引起的符号冲突问题
  2. 各个编译器处理生成代码的结果不一样且不可控,可能会生成和标准描述的不一致的行为

所以结论显而易见,有条件的话最好始终给模板变量加上inline。这样不管你在用c++17还是c++20,编译器是GCC还是clang,程序的行为都是符合标准的可预期的。

总结

c++是一门很麻烦的语言,为了弄清楚别人为什么要用某个关键字就得大费周折,还需要许多前置知识作为铺垫。

换回这次的话题,标准库的实现和标准定义里给模板变量加inline最大的原因是因为几个历史遗留问题和标准自己的疏漏,当然加上去之后也没什么坏处。

这也更说明了在c++里真的没有什么银弹,某个特性需不需要用得结合自己的知识、经验还有实际情况来决定,别人的例子最多也只能作为一种参考,也许对于他来说合适的对你来说就是不切实际的,依葫芦画瓢前得三思。

这也算是c++的黑暗面之一吧。。。

参考资料

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0607r0.html

https://stackoverflow.com/questions/70801992/are-variable-templates-declared-in-a-header-an-odr-violation

https://stackoverflow.com/questions/65521040/global-variables-and-constexpr-inline-or-not

为什么标准库的模板变量都是inline的的更多相关文章

  1. C++标准库类模板vector

    vector是C++标准库STL中的一个重要的类模板,相当于一个更加健壮的,有很多附加能力的数组 使用vector前首先要包含头文件 #include<vector>  1.vector的 ...

  2. C++标准库类模板(stack)和 队列(queue)

    在C++标准库(STL)中有栈和队列的类模板,因此可以直接使用 1.栈(stack):使用栈之前,要先包含头文件 : #include<stack> stack.push(elem); / ...

  3. std 与标准库

    1.命名空间std C++标准中引入命名空间的概念,是为了解决不同模块或者函数库中相同标识符冲突的问题.有了命名空间的概念,标识符就被限制在特定的范围(函数)内,不会引起命名冲突.最典型的例子就是st ...

  4. Go标准库之html/template

    html/template包实现了数据驱动的模板,用于生成可防止代码注入的安全的HTML内容.它提供了和text/template包相同的接口,Go语言中输出HTML的场景都应使用html/templ ...

  5. C++解析(18):C++标准库与字符串类

    0.目录 1.C++标准库 2.字符串类 3.数组操作符的重载 4.小结 1.C++标准库 有趣的重载--操作符 << 的原生意义是按位左移,例:1 << 2;,其意义是将整数 ...

  6. C++标准库的初探

    1,操作符 << 的原生意义是按位左移,例: 1 << 2; 其底层的意义是将整数 1 按位左移 2 位,即: 0000 0001  ==> 0000 0100: 2,重 ...

  7. 32,初探c++标准库

    1. 有趣的重载 (1)操作符<<:原义是按位左移,重载“<<”可将变量或常量左移到对象中 重载左移操作符(仿cout类) #include<stdio.h> co ...

  8. 16.C++-初探标准库

    在别人代码里,经常看到std命名空间,比如使用std命名空间里的标准输入输出流对象cout: #include<iostream> using namespace std; int mai ...

  9. C++之标准库map

    目录 1.成员函数 2.元素访问 3.迭代器Iterators(C++ 11) 4.容量Capacity 5.修改函数(C++ 11和C++ 17) 6.查找表Lookup 7.观察Observers ...

  10. c运行库、c标准库、windows API的区别和联系

    C运行时库函数C运行时库函数是指C语言本身支持的一些基本函数,通常是汇编直接实现的.  API函数API函数是操作系统为方便用户设计应用程序而提供的实现特定功能的函数,API函数也是C语言的函数实现的 ...

随机推荐

  1. 部署Netlify站点博客

    Netlify站点部署静态博客 今天尝试把站点部署在Netlify上,因为部署在GitHub Pages上,国内访问速度太慢了,所以就尝试一下别的站点,部署成功之后发现速度还是不太行,后边继续找找原因 ...

  2. 2021年3月-第03阶段-前端基础-JavaScript基础语法-JavaScript基础第01天

    1 - 编程语言 1.1 编程 编程: 就是让计算机为解决某个问题而使用某种程序设计语言编写程序代码,并最终得到结果的过程. 计算机程序: 就是计算机所执行的一系列的指令集合,而程序全部都是用我们所掌 ...

  3. Mysqldump 的 的 6 大使用场景的导出命令

    Mysqldump 选项解析 场景描述 1. 导出 db1.db2 两个数据库的所有数据. mysqldump -uroot -p -P8635 -h192.168.0.199 --hex-blob ...

  4. 延申三大问题中的第二个问题处理---收集查看k8s中pod的控制台日志

    1.不使用logstash 2.步骤: 2.1 先获取一个文件的日志 2.2 再获取多个文件的日志 2.3 批量获取文件日志 pod日志文件路径 [root@worker hkd-eureka]# p ...

  5. 手把手教你使用LabVIEW OpenCV DNN实现手写数字识别(含源码)

    @ 目录 前言 一.OpenCV DNN模块 1.OpenCV DNN简介 2.LabVIEW中DNN模块函数 二.TensorFlow pb文件的生成和调用 1.TensorFlow2 Keras模 ...

  6. 国产电脑可较为流畅运行的Windows系统

    系统是Windows2003,内置了WPS和IE8,使用QEMU TCG运行,速度慢,凑合能用. 使用前先sudo apt install qemu-system-x86,把压缩包中的2003.qco ...

  7. 20220925 - CSP-S 模拟赛 #2

    20220925 - CSP-S 模拟赛 #2 时间记录 \(8:00-8:20\) 浏览题面 \(8:20-8:45\) T1 想到了分块计算,但是在手推样例的过程中,发现样例的数据并不能真正构成一 ...

  8. 齐博x1模块安装文件讲解

    频道模块存放的目录是/application/频道目录/ 插件存放的目录是/plugins/插件目录/ 他的安装目录都是/install/ 推荐参考默认的/application/cms/instal ...

  9. C语言整人关机程序

    #include <stdio.h> #include <stdlib.h> #include <string.h> int main() { char input ...

  10. SQL中事务以及全局变量的使用

    事务的定义 简单的说,事务处理可以用来维护数据库的完整性,保证一批SQL语句要么全执行,要么全部不执行 事务的特性 原子性  一致性  持久性  隔离性        注:一元九个 事务的使用 sel ...