本文最后更新于 2024-11-23T03:27:52+08:00
序言
现代 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 2 3 4
| class Class { inline static int member = 42; };
|
对于函数
- 写在头文件里的 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 2 3 4 5
| template<class F, class... Args> decltype(auto) PerfectForward(F fun, Args&&... args) { return fun(std::forward<Args>(args)...); }
|
1 2 3 4 5 6 7 8 9 10
| auto c0 = a; decltype(auto) c1 = a; auto c2 = (a); decltype(auto) c3 = (a); auto d = {1, 2}; auto n = {5};
auto m{5};
|
简写函数模板
在 C++20 中,auto 能推导出作为参数的函数对象,也能推导出可变长参数
上面的 PerfectForward
也可以写成:
1 2 3 4
| decltype(auto) PerfectForward(auto fun, auto &&...args) { return fun(std::forward<decltype(args)>(args)...); }
|
C++ 的最终形态一定是 auto auto = auto(auto); 罢
概念与约束(concept & requires)
模板的报错往往极度缺乏可读性,我们希望在编写模板时就对可能发生的情况做出一定的约束,使得违反约束的调用不要实例化,以避免产生难以阅读的报错信息。
这项技巧叫做 SFINE(Substitution Failure Is Not An Error),自 C++20 起 concept 与 requires 加入了标准,大大简化了 SFINE 的编写与理解难度。
concept
这是一个概念:
1 2
| template<模板形参列表> concept 概念名 = 编译期布尔表达式;
|
ex:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| template<class T> concept C0 = true;
template<class T> concept C1 = std::is_integral_v<T>;
template<class T> concept C2 = std::is_floating_point_v<T> || std::is_signed_v<T>;
template<class T> concept C3 = C1<T> && !std::is_signed_v<T>;
C0<int> C1<int> C2<int> C2<float> C3<uint>
|
这是一个受 concept 约束的函数模板:
1 2 3 4 5 6 7 8 9
| template<class T> concept C = std::is_integral_v<T> && !std::is_signed_v<T>;
template<C T> void Foo(T t){}
Foo(42U); Foo(42.0);
|
这是一个受 concept 约束的简写函数模板:
1 2 3 4 5 6 7 8
| template<class T> concept C = std::is_integral_v<T> && !std::is_signed_v<T>;
void Foo(C auto t){}
Foo(42U); Foo(42.0);
|
标准库已经提供了很多简单且常用的概念:
1 2 3 4 5 6 7
| #include <concepts>
void Foo(std::unsigned_integral auto t){}
Foo(42U); Foo(42.0);
|
requires 表达式
这是一个 requires 表达式:
因为他在编译期返回一个布尔,所以可以接在 concept 后面:
1 2
| template<class T> concept C = requires{ 要求序列 };
|
这是一个简单要求:
1 2 3 4 5 6 7 8 9 10 11 12
| struct Test { static void StaticFoo() {} void Foo() {} }
template<class T> concept C0 = requires{ T::StaticFoo(); }
template<class T> concept C1 = requires(T t){ t.Foo(); }
|
requires 表达式不实际运行大括号内的代码,只检测代码的有效性。
这是一个类型要求,以关键字 typename 开头:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| struct Test { struct Type0{}; using Type1 = int; };
template<class T> struct S{};
template<class T> using Ref = T&;
template<typename T> concept C = requires { typename T::Type0; typename T::type1; typename S<T>; typename Ref<T>; }
|
这是一个复合要求,不但检查代码的有效性,还可以检查返回值类型和 noexcept:
1 2 3 4 5 6 7 8 9 10
| template<class T> concept C = requires(T t) { { t + t } -> std::convertible_to<bool>; { t - t } -> std::same_as<int>; { t.~T() } noexcept; };
|
这是一个嵌套要求,由关键字 requires 开头:
1 2 3 4
| requires { requires 编译期布尔表达式; };
|
ex:
1 2 3 4 5
| template<class T> concept C = requires(T t) { requires std::is_same_v<T, int>; };
|
这种写法完全等价于:
1 2
| template<class T> concept C = std::is_same_v<T, int>;
|
意义在于允许我们在要求序列中加入编译期布尔表达式,并以此影响整个 requires 表达式的结果:
1 2 3 4 5 6
| template<class T> concept C = requires(T t) { { t + t } -> std::convertible_to<bool>; requires std::is_same_v<T, int>; };
|
其实这种写法也等价于:
1 2 3 4 5
| template<class T> concept C = std::is_same_v<T, int> && requires(T t) { { t + t } -> std::convertible_to<bool>; };
|
requires 子句
这是一个 requires 子句,可以在声明模板时直接写在 template<模板形参列表> 后面:
这是一个被 requires 子句约束的函数模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| template<class T> requires std::is_same_v<T, int> void Foo0(T t){}
template<class T> concept C = std::is_same_v<T, int>;
template<class T> requires C<T> void Foo1(T t){}
template<class T> requires requires(T t) { requires std::is_same_v<T, int>; } void Foo2(T t){}
|
以上写法全部等价,你甚至可以:
1 2 3 4 5 6
| template<class T> requires requires(T t) { requires requires{ std::is_same_v<T, int> }; } void Foo2(T t){}
|
R"()"
不使用转义字符打印特殊字符
1 2 3 4 5 6 7 8 9 10 11 12 13
| const char *str = R"( "test"'test' `~!@#$%^&*()-_=+[{]}\|;:'",<.>/? )"; std::cout << str << std::endl;
|
R"()"
会忠实的存储两个小括号之间的所有符号,包括每一个换行和缩进。
offsetof
1 2 3 4 5 6 7 8 9 10 11 12 13
| struct Vertext { int a, b; const int c = 0; };
int main() { Vertext vert; *(int *)((char *)(&vert) + offsetof(Vertext, c)) = 42; std::cout << vert.c << std::endl; }
|
pragma warning
1
| #pragma warning(disable: XXXX)
|
1
| #pragma warning(once: XXXX)
|
1
| #pragma warning(error: XXXX)
|
1
| #pragma warning(default: XXXX)
|
- push, pop 之间所有的更改都会在 pop 之后被放弃
1 2 3
| #pragma warning(push)
#pragma warning(pop)
|
- suppress 仅忽略下一行的警告,然后恢复到 suppress 之前的状态,无法影响多行
1
| #pragma warning(suppress: XXXX)
|
nodiscard
1 2
| [[nodiscard("PURE FUN")]] int strategic_value(int x, int y) { return x ^ y; }
|
- 若该类型作为返回值没有被使用,编译器可以发出警告。
1
| struct [[nodiscard]] error_info { };
|
VA_OPT
当可变参数宏不为空时才展开 __VA_OPT__
内的内容,反之则忽略 __VA_OPT__
内的内容。
例如:
1
| #define ASSERT(x, ...) { if(!(x)) { __VA_OPT__(LOG_FATAL(__VA_ARGS__);) DEBUGBREAK(); } }
|
会将 ASSERT(false, "Hello")
展开为
1 2 3 4 5 6 7
| { if(!(false)) { LOG_FATAL("Hello"); DEBUGBREAK(); } }
|
而会将 ASSERT(false)
展开为
1 2 3 4 5 6
| { if(!(false)) { DEBUGBREAK(); } }
|
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 2 3
| filter { "action:vs*" } buildoptions { "/Zc:preprocessor" } filter {}
|