为什么标准库的模板变量都是inline的
最近在看标准库里的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 constexpr:https://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。
这样其实就上一节说到的所有问题:
- c++17新加了通常情况下模板变量和inline变量是外部链接的规定,因此加上inline解决了模板变量常量链接性上的问题
- inline变量允许被多次定义,因此就算ODR规则忘记更新或者重新考虑后改变了规则也没问题(当然现在已经明确模板变量可以多次定义了)
- 比起加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会有什么后果:
- 如果你在用c++20或者更新的版本,那么语法上没有任何问题;否则在语法上也处于灰色地带,在参考资料中的第三个链接里就描述了这个原因引起的符号冲突问题
- 各个编译器处理生成代码的结果不一样且不可控,可能会生成和标准描述的不一致的行为
所以结论显而易见,有条件的话最好始终给模板变量加上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/65521040/global-variables-and-constexpr-inline-or-not
为什么标准库的模板变量都是inline的的更多相关文章
- C++标准库类模板vector
vector是C++标准库STL中的一个重要的类模板,相当于一个更加健壮的,有很多附加能力的数组 使用vector前首先要包含头文件 #include<vector> 1.vector的 ...
- C++标准库类模板(stack)和 队列(queue)
在C++标准库(STL)中有栈和队列的类模板,因此可以直接使用 1.栈(stack):使用栈之前,要先包含头文件 : #include<stack> stack.push(elem); / ...
- std 与标准库
1.命名空间std C++标准中引入命名空间的概念,是为了解决不同模块或者函数库中相同标识符冲突的问题.有了命名空间的概念,标识符就被限制在特定的范围(函数)内,不会引起命名冲突.最典型的例子就是st ...
- Go标准库之html/template
html/template包实现了数据驱动的模板,用于生成可防止代码注入的安全的HTML内容.它提供了和text/template包相同的接口,Go语言中输出HTML的场景都应使用html/templ ...
- C++解析(18):C++标准库与字符串类
0.目录 1.C++标准库 2.字符串类 3.数组操作符的重载 4.小结 1.C++标准库 有趣的重载--操作符 << 的原生意义是按位左移,例:1 << 2;,其意义是将整数 ...
- C++标准库的初探
1,操作符 << 的原生意义是按位左移,例: 1 << 2; 其底层的意义是将整数 1 按位左移 2 位,即: 0000 0001 ==> 0000 0100: 2,重 ...
- 32,初探c++标准库
1. 有趣的重载 (1)操作符<<:原义是按位左移,重载“<<”可将变量或常量左移到对象中 重载左移操作符(仿cout类) #include<stdio.h> co ...
- 16.C++-初探标准库
在别人代码里,经常看到std命名空间,比如使用std命名空间里的标准输入输出流对象cout: #include<iostream> using namespace std; int mai ...
- C++之标准库map
目录 1.成员函数 2.元素访问 3.迭代器Iterators(C++ 11) 4.容量Capacity 5.修改函数(C++ 11和C++ 17) 6.查找表Lookup 7.观察Observers ...
- c运行库、c标准库、windows API的区别和联系
C运行时库函数C运行时库函数是指C语言本身支持的一些基本函数,通常是汇编直接实现的. API函数API函数是操作系统为方便用户设计应用程序而提供的实现特定功能的函数,API函数也是C语言的函数实现的 ...
随机推荐
- [Linux]-screen命令-切换终端
在训练模型时,经常遇到需要采用多个策略同时跑的情况,直接运行的话比较费时,只要CPU和GPU支持,可以通过Linux的screen命令多终端并行,大大提升效率. 创建: screen -S name ...
- java 验证手机号是否合法
一.通用工具类编写 /** * @project * @Description * @Author songwp * @Date 2022/9/15 17:06 * @Version 1.0.0 ** ...
- Django 测试脚本
一.测试脚本 Django 在创建项目时自动在应用下创建了tests.py,这个py文件可以作为测试文件:也可以在应用下手动创建一个py测试文件. 无论哪种方式,都需要提前书写以下代码. from d ...
- 深度剖析Istio共享代理新模式Ambient Mesh
摘要:今年9月份,Istio社区宣布Ambient Mesh开源,由此引发国内外众多开发者的热烈讨论. 本文分享自华为云社区<深度剖析!Istio共享代理新模式Ambient Mesh>, ...
- 车辆稳定性辅助(VSA)系统
当车辆转弯大于或小于预期时,VSA有助于在转弯时稳定车辆. 同时还有助于在湿滑路面上保持牵引力. VSA 打开和关闭 禁用: 请长按按钮直至听到哔的一声.VSA 关闭指示灯点亮. 恢复: 按下按钮直至 ...
- 类和实例,super()函数
class Foo: def __init__(self, name): self.name = name def ord_func(self): """定义实例方法,至 ...
- IC入门课第五课作业:完善 Microblog 前端(1、显示发布者的名字;2、增加新UI、3、关注其他学员的 canister)
上周完成了 IC 入门课程第五课的作业 现将答案贴出,欢迎同学们参考,禁止抄袭作业哦 课程作业 (完善 microblog 前端) 1. 显示消息的发布者名字 a. 给 Message 增加 auth ...
- PAT (Basic Level) Practice 1004 成绩排名 分数 20
读入 n(>0)名学生的姓名.学号.成绩,分别输出成绩最高和成绩最低学生的姓名和学号. 输入格式: 每个测试输入包含 1 个测试用例,格式为 第 1 行:正整数 n 第 2 行:第 1 个学生的 ...
- 请推荐下比较适合中小企业的ERP系统,如odoo,除前期开发和不定期完善,有没有其他固定月费或年费?
odoo的话你自己就可以下载开源的安装使用的啊,如果你要别人帮你开发和完善做技术服务的话一般都还是要年费的,主要是因为要帮你做维护或修bug什么的,自己能搞定的话自然不需要的哦.只是odoo使用的是p ...
- NSIS检测到窗口最小化闪烁提示
#检测到窗口为最小化时闪烁提示 !include nsDialogs.nsh #编写:水晶石 Name "IsIconic Example" OutFile "IsIco ...