最近在看标准库里的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. 聊聊单点登录(SSO)中的CAS认证

    SSO介绍 背景 随着企业的发展,一个大型系统里可能包含 n 多子系统, 用户在操作不同的系统时,需要多次登录,很麻烦,我们需要一种全新的登录方式来实现多系统应用群的登录,这就是单点登录. web 系 ...

  2. PostgreSQL 模式(SCHEMA)

    PostgreSQL 模式(SCHEMA)可以看着是一个表的集合. 一个模式可以包含视图.索引.据类型.函数和操作符等. 相同的对象名称可以被用于不同的模式中而不会出现冲突,例如 schema1 和 ...

  3. MongoDB 单实例节点主机的用户和权限一般操作步骤

    步骤总结: 1.先正常启动 2.创建用户,创建数据库病授权用户 3.关闭程序,开启安全,然后启动 4.使用账号和密码连接 按未开启认证的方式(配置文件中没开启安全选项并且启动命令中不添加 --auth ...

  4. 制造业数字化转型,本土云ERP系统如何卡位?

    去标准化,主打个性化,方可在制造业数字化转型中大放异彩,本土云ERP要想获得青睐成功卡位必须坚持这个原则.为什么这么说?就连某头部ERP厂商都倡导一个观念"Rise With.......& ...

  5. Educational Codeforces Round 106 (Rated for Div. 2)

    就ac了2题... A题一开始题意模模糊糊的似懂非懂,然后自己按样例推出了题意,简单题很容易ac了.还是自己的英语水平太菜了.... B题根据0和1的位置关系能看出来,因为0不能在1后面, 所以有00 ...

  6. [Android开发学iOS系列] ViewController

    iOS ViewController 写UIKit的代码, ViewController是离不开的. 本文试图讲讲它的基本知识, 不是很深入且有点杂乱, 供初级选手和跨技术栈同学参考. What is ...

  7. VLQ & Base64 VLQ 编码方式的原理及代码实现

    目录 VLQ Base64 VLQ VLQ VLQ (Variable-length quantity)是一种通用的,使用任意位数的二进制来表示一个任意大的数字的一种编码方式. 编码实现: ** 对数 ...

  8. Dytechlab Cup 2022 (A - C)

    Dytechlab Cup 2022 (A - C) A - Ela Sorting Books 分析:贪心,将字符串每一位都存在map里,从前往后尽量让每一个\(n / k\)的段\(mex\)值尽 ...

  9. 基于雪花算法的增强版ID生成器

    sequence 基于雪花算法的增强版ID生成器 解决了时间回拨的问题 无需手动指定workId, 微服务环境自适应 可配置化 快速开始 依赖引入 <dependency> <gro ...

  10. python更改文件后缀名

    path = '1024.png' extension = 'jpg' for i in range(1,len(path)): if (path[-i] == '.'):#找到后缀初始点 new_p ...