现代 C++ 随记
序言
现代 C++ 相关的笔记。
inline 和 static
唯一定义原则
https://en.cppreference.com/w/cpp/language/definition
extern int a;
是声明。
int a;
, int a = 1;
, extern int a = 1;
都是定义。
void foo();
是声明。
void foo() {}
是定义。
static
- 将定义的可见性限制到当前翻译单元,使得不同翻译单元之间同名定义不冲突。
- 实际上根据不同的用法 static 可以控制可见性,也可以控制生命周期,这里我们只讨论可见性相关的功能。
inline
- 可以在多个翻译单元内存在同名 inline 定义,编译器选择其中一份作为唯一的定义,具体选择哪份定义则是未定义行为。
- 既有 static 的优点,可以写在头文件内被不同翻译单元包含,又有 extern 的优点,可以全局共享。
对于成员变量
inline static
的成员变量可以直接在头文件内声明与定义,免去了再在 cpp 文件里写一份成员变量定义的繁琐。
1 |
|
对于函数
- 写在头文件里的 static 函数定义自然不会产生冲突,但是 static 也导致不同翻译单元之间各包含一份函数定义,导致最终生成的目标体积增大。inline 很好地解决了这个问题,因为连接器最终只会将 inline 函数链接到同一份定义,指向同一个函数地址。
- include 同一份头文件更不会产生连接器链接到不同的定义(函数体)的问题。
- 同时,整个定义都在 class / struct / union 内的函数,隐式的是 inline 的。
- 顺便,直接写定义的模板函数是也默认 inline 的。
对于类
- 可惜不能将 class 声明为 static,但是要解决这个问题也很简单,将类的定义放在一个匿名命名空间即可将该类的可见范围限制在当前翻译单元。
- 匿名命名空间下的函数定义和 static 的函数定义效果也是一样的。
至于内联
- 自 C++17 之后 inline 的意义产生了巨大的变化,对于函数的含义已经变为“容许重定义”而不是“建议内联”。
- 比如对于 MSVC,可以使用
__forceinline
放宽编译器对内联函数的判断,不过编译器依旧不一定会听你的。
前向声明与 std::unique_ptr
假设我们前向声明了一个 T
。
当一个类持有 std::unique_ptr<T>
类型的成员变量时,该类的析构函数必须也只能在 cpp 内有定义(当然一般来说这时 cpp 内是有 T 的完整定义的)。
理由很简单,该类析构时需要调用 unique_ptr 所包裹对象的析构函数,但是默认生成的析构函数是头文件中的内联函数,而在头文件内自然是不知道 T 该如何析构的。
将类的析构函数的定义移至 cpp 相当于向其告知了 T 的析构方式。
static std::unique_ptr
前向声明不可用于 inline static std::unique_ptr<T>
类型的成员变量。
静态成员变量的生命周期已经脱离了类实例的生命周期,此时 unique_ptr 需要在该静态成员变量的定义处得知 T 的完整定义。
inline static
成员变量的定义处自然是头文件,故此时会编译失败。将静态成员变量的定义分离至 cpp(总之是有类完整定义的地方)内才能编译。
decltype(auto)
在完美转发返回值时尤为有用
1 |
|
1 |
|
简写函数模板
在 C++20 中,auto 能推导出作为参数的函数对象,也能推导出可变长参数
上面的 PerfectForward
也可以写成:
1 |
|
C++ 的最终形态一定是 auto auto = auto(auto); 罢
概念与约束(concept & requires)
模板的报错往往极度缺乏可读性,我们希望在编写模板时就对可能发生的情况做出一定的约束,使得违反约束的调用不要实例化,以避免产生难以阅读的报错信息。
这项技巧叫做 SFINE(Substitution Failure Is Not An Error),自 C++20 起 concept 与 requires 加入了标准,大大简化了 SFINE 的编写与理解难度。
concept
这是一个概念:
1 |
|
ex:
1 |
|
这是一个受 concept 约束的函数模板:
1 |
|
这是一个受 concept 约束的简写函数模板:
1 |
|
标准库已经提供了很多简单且常用的概念:
1 |
|
requires 表达式
这是一个 requires 表达式:
1 |
|
因为他在编译期返回一个布尔,所以可以接在 concept 后面:
1 |
|
这是一个简单要求:
1 |
|
requires 表达式不实际运行大括号内的代码,只检测代码的有效性。
这是一个类型要求,以关键字 typename 开头:
1 |
|
这是一个复合要求,不但检查代码的有效性,还可以检查返回值类型和 noexcept:
1 |
|
这是一个嵌套要求,由关键字 requires 开头:
1 |
|
ex:
1 |
|
这种写法完全等价于:
1 |
|
意义在于允许我们在要求序列中加入编译期布尔表达式,并以此影响整个 requires 表达式的结果:
1 |
|
其实这种写法也等价于:
1 |
|
requires 子句
这是一个 requires 子句,可以在声明模板时直接写在 template<模板形参列表> 后面:
1 |
|
这是一个被 requires 子句约束的函数模板:
1 |
|
以上写法全部等价,你甚至可以:
1 |
|
R"()"
不使用转义字符打印特殊字符
1 |
|
R"()"
会忠实的存储两个小括号之间的所有符号,包括每一个换行和缩进。
offsetof
1 |
|
实现
最近看到 MSVC offsetof 的实现:
1 |
|
很神奇,将一个空指针转为该类型的指针然后解引用该成员再取地址,有点像是在用成员地址减去实例地址,不过这里减去的是 0。
顺便一说还有一些情况能让空指针的解引用不报错:
1 |
|
pragma warning
- 完全忽略指定警告
1 |
|
- 指定警告只显示一次
1 |
|
- 指定警告为错误
1 |
|
- 重置警告,可能会影响更早的警告设置
1 |
|
- push, pop 之间所有的更改都会在 pop 之后被放弃
1 |
|
- suppress 仅忽略下一行的警告,然后恢复到 suppress 之前的状态,无法影响多行
1 |
|
nodiscard
- 若没有使用函数的返回值,编译器可以发出警告。
1 |
|
- 若该类型作为返回值没有被使用,编译器可以发出警告。
1 |
|
VA_OPT
当可变参数宏不为空时才展开 __VA_OPT__
内的内容,反之则忽略 __VA_OPT__
内的内容。
例如:
1 |
|
会将 ASSERT(false, "Hello")
展开为
1 |
|
而会将 ASSERT(false)
展开为
1 |
|
VS
在 VS2022 中启用 __VA_OPT__
等较新的预处理器需要开启编译器开关 /Zc:preprocessor
https://learn.microsoft.com/en-us/cpp/preprocessor/preprocessor-experimental-overview?view=msvc-170
Premake
按理来说在 Premake 中启用该 VS 开关对应的是 usestandardpreprocessor
https://premake.github.io/docs/usestandardpreprocessor/
但是实测不生效,可能是一个 bug
现在而言的话可以写:
1 |
|
更新:Premake 最新版本已经解决了这个 bug
std::weak_ptr
lock()
,返回一个std::shared_ptr
,如果对象已被释放,则返回空的std::shared_ptr
expired()
,检查对象是否已被释放use_count()
返回关联的std::shared_ptr
的引用计数
编译器自动生成函数的规则
- 如果你没有显式定义任何拷贝、移动、析构函数,编译器会自动生成以下函数:
- 默认构造函数(如果没有其他构造函数)
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数(C++11 及以后)
- 移动赋值运算符(C++11 及以后)
- 析构函数
-
显式定义带参数的构造函数:
编译器不会自动生成默认构造函数 -
显式定义拷贝构造函数或拷贝赋值运算符:
编译器不会自动生成移动构造函数和移动赋值运算符 -
显式定义移动构造函数或移动赋值运算符:
编译器不会自动生成拷贝构造函数和拷贝赋值运算符 -
显式定义析构函数:
编译器不会自动生成移动构造函数和移动赋值运算符
自定义类型与 std::format
如何用 std::format 格式化自定义类型呢,提供对应的 std::formatter 特化即可
最简单的方式,根据你的类型继承不同的 std::formatter 特化:
1 |
|
https://www.modernescpp.com/index.php/formatting-user-defined-types-in-c20/
std::span
类似 std::string_view,只持有一个指针 + 一个长度。
- 不持有数据
- 易于 copy
- 可读写
1 |
|
- Iterators
- front(), back(), data(), operator[]
- size(), size_bytes(), empty()
- first(count)
- last(count)
- subspan(offset, count)