现代 C++ 随记

序言

现代 C++ 相关的笔记。

inline 和 static

唯一定义原则

https://en.cppreference.com/w/cpp/language/definition

int a;, int a = 1;, extern int a = 1; 都是定义。
extern int a; 是声明。

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;             // c0 的类型是 int,保有 a 的副本
decltype(auto) c1 = a; // c1 的类型是 int,保有 a 的副本
auto c2 = (a); // c2 的类型是 int,保有 a 的副本
decltype(auto) c3 = (a); // c3 的类型是 int&,它是 a 的别名

auto d = {1, 2}; // OK:d 的类型是 std::initializer_list<int>
auto n = {5}; // OK:n 的类型是 std::initializer_list<int>
// auto e{1, 2}; // C++17 起错误,之前是 std::initializer_list<int>
auto m{5}; // OK:DR N3922 起 m 的类型是 int,之前是 initializer_list<int>
// decltype(auto) z = { 1, 2 } // 错误:{1, 2} 不是表达式

顺便一提 C++ 20 中的 auto

在 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); 罢

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;
/*
output :

"test"'test'
`~!@#$%^&*()-_=+[{]}\|;:'",<.>/?

*/

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;
// output : 42
}

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( 字符串字面量 ) (C++20 起):
[[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 {}