CMake

g++14

支持C++23需要g++14版本,可以安装

1
sudo apt install gcc-14 g++-14

Cmake

新建 CMakeLists.txt 里面写

1
2
3
4
5
6
7
8
9
cmake_minimum_required(VERSION 3.28)


set(CMAKE_CXX_COMPILER g++-14)
set(CMAKE_CXX_STANDARD 23)

project(exampleproject LANGUAGES CXX)

add_executable(exampleproject main.cpp)

构建

优点: - 明确指定源代码和构建目录 - 避免在源代码目录中生成构建文件 - 支持out-of-source构建(推荐做法)

1
cmake -S /mnt/d/Fyind/Master_Semester7/cpp -B /mnt/d/Fyind/Master_Semester7/cpp/build

C++23

print

1
2
3
4
5
6
7
8
#include <print>

int main() {
std::println("Hello world");
std::string name = "Fyind";
int age = 25;
std::println("Name: {}, Age: {}", name, age);
}

noexcept

noexcept 是 C++11 引入的异常规范关键字,用于声明函数不会抛出异常。

作用: 1. 编译器优化 - 编译器可以进行更激进的优化 2. 移动语义 - 标准库容器会优先使用noexcept的移动构造函数 3. 文档化 - 明确告知调用者函数不会抛异常

1
2
3
4
5
6
//基本语法:
// 声明函数不抛出异常
void func() noexcept;

// 条件性noexcept
void func() noexcept(condition);

内存管理

内存管理的重要性

计算机内存

虚拟地址空间,内存页

image-20250717203926581
  • 当前主流的虚拟内存实现方式是:将虚拟地址空间划分为固定大小的块,称为“内存页(page)”

  • 当一个进程访问某个虚拟地址时,操作系统会检查该虚拟地址所在的页是否已加载到物理内存中(即是否映射到了物理页框 page frame)。如果没有,就会发生 页错误(page fault)

    • 页错误并不是程序错误,而是一个正常的、受控的硬件中断,目的是从磁盘加载数据到内存
  • 如果物理内存没有空闲页框了,系统就必须从内存中移除一个已有的页(称为“页置换”)

  • 如果被移除的页是“脏页(dirty page)”,即该页自从从磁盘加载进内存之后有过修改,就必须先将其写回磁盘,以防数据丢失。

  • 如果是“干净页(clean page)”,即未被修改,直接丢弃即可,无需写回磁盘。

这整个过程被称为 paging(分页置换)

  • iOS不支持脏页写回,内存不足时直接终止进程。

抖动(Thrashing)

  • 系统物理内存不足时,频繁进行页面置换。
  • 导致系统性能急剧下降。
  • 通过监控page fault频率判断是否发生抖动。

进程内存

栈内存

  • 栈是一块连续的内存区域。
  • 每个线程有独立的栈。
  • 栈的大小固定,超出会栈溢出。
  • 栈内存分配和释放非常快,不会产生碎片。
  • 栈增长方向通常向下。
  • 示例:递归函数可能导致栈溢出。
  • 默认栈大小约为8MB(在Mac系统上)。
  • 每个线程都有自己独立的栈 → ✅ 线程安全

堆内存

  • 堆是全局共享的内存区域。
  • 用于动态内存分配(new/malloc)和释放(delete/free)。
  • 堆内存分配模式不固定,容易产生内存碎片。
  • 示例:频繁分配和释放不同大小内存可能导致碎片。
  • 因为堆是共享资源 → ❌ 不是线程安全,需要配合互斥锁等机制。

内存中的对象

而堆内存就很“混乱”:你可以随时在任意位置 newdelete

这就可能造成 内存碎片(fragmentation)

创建与删除对象

new 与 delete 的工作原理

  • new 操作包含两个步骤:
    • 分配内存(调用 operator new)。
    • 构造对象(调用构造函数)。
  • delete 操作也包含两个步骤:
    • 析构对象(调用析构函数)。
    • 释放内存(调用 operator delete)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User {
std::string name;
public:
User(std::string name) : name(name) {}

void print_name();
};

void User::print_name() {
std::println("Name: {}", this->name);
}
// main
auto user = new User{"John"};
user->print_name();
delete user;

定置 new(Placement new)

  • 允许分离内存分配与对象构造。就是 placement new,意思是“在这块内存上构造对象”。

    它不会分配内存,只是调用构造函数。

  • 使用示例:

    1
    2
    3
    4
    5
    auto memory = std::malloc(sizeof(User));
    auto user = new (memory) User("John");
    user->print_name();
    user->~User();
    std::free(memory);
  • ⚠️ 如果你用了 placement new,就必须手动调用析构函数

  • C++17 提供了

    • std::uninitialized_fill_n 再内存构造一个对象
    • std::destroy_at 调用析构函数。
    1
    2
    3
    4
    5
    6
    auto memory = std::malloc(sizeof(User));
    auto user_ptr = reinterpret_cast<User*>(memory);
    std::uninitialized_fill_n(user_ptr, 1, User{"John"});
    user_ptr->print_name();
    std::destroy_at(user_ptr);
    std::free(memory);

new 和 delete 操作符

  • 可以全局或类内重载 operator new 和 operator delete。

  • 示例重载:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    auto operator new(size_t size) -> void* {
    void* p = std::malloc(size);
    std::println("Allocated: {}", size);
    return p;
    }

    auto operator delete(void* p) noexcept -> void {
    std::println("Deleting");
    return std::free(p);
    }
  • 数组操作符:

    • operator new[]operator delete[]
  • 类内重载 new/delete 可用于特定类的内存管理。

  • 如果要访问类外的

    1
    2
    auto* p = ::new Document(); // 调用全局 operator new
    ::delete p; // 调用全局 operator delete
  • 为什么new[]new 实现一样

只重载了内存分配函数本身operator new[]operator new

构造函数和数组元信息管理是编译器负责的,不是 operator new 负责的 编译器会自动在分配的内存中“隐藏元信息”(如元素数量),让 delete[] 知道需要调用多少次析构函数

内存对齐

内存对齐是指:不同数据类型的变量必须存储在符合其“对齐要求”的地址上,以提高 CPU 访问效率,甚至在某些平台上是必须遵守的规则。

对齐基础

  • CPU 以字为单位读取内存(如64位架构为8字节)。

  • 每种类型都有对齐要求:

    • 使用 alignof 查看类型对齐要求。

      1
      std::cout << alignof(int) << '\n';   // 输出可能是 4
  • 不对齐的内存访问可能导致性能下降或程序崩溃。

image-20250717212604845

内存分配对齐保证

  • 使用 new 或 malloc 分配的内存满足最大对齐要求。

  • std::max_align_t 表示最大对齐类型。也就是说,任何基本类型(比如 intdoublelong doublechar 等)的对齐要求都不会超过 std::max_align_t 的对齐要求。它的作用是为内存分配和布局提供一个“最大对齐保证”,确保分配的内存地址满足任何类型的对齐需求。

    1
    auto max_alignment = alignof(std::max_align_t);
  • 即使分配单个 char,也按最大对齐方式对齐。

填充(Padding)

  • 编译器会在类成员之间插入填充字节以满足对齐要求。
  • 示例:
    • class Document { bool; double; int; } 会因填充导致大小为24字节。
  • 优化方法:将对齐要求大的成员放在前面。
  • 优化后的示例:
    • class Document { double; int; bool; } 大小为16字节。
  • 对齐与缓存友好性:
    • 可以将对象对齐到缓存行边界以提高性能。
    • 将频繁使用的成员放在一起以减少缓存行切换。

内存所有权

所有权(ownership)表示某个变量、对象或代码块对资源(如内存、文件、数据库连接等)的控制权。

拥有某个资源就意味着负责它的释放和清理。

处理资源隐式

  • 使用自动变量处理动态内存的分配/释放。
  • 通过析构函数释放动态内存,避免内存泄漏。
  • RAII(资源获取即初始化)技术用于管理资源生命周期。
  • 使用RAIIConnection类自动管理连接资源,确保连接在使用后关闭。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class RAIIConnection {
public:
RAIIConnection(const std::string& url)
: connection_{open_connection(url)} {}

~RAIIConnection() {
try {
close(connection_);
} catch (const std::exception&) {
// 不应从析构函数中抛出异常
}
}

auto& get() { return connection_; }

private:
Connection connection_;
};

容器

  • 使用标准容器自动管理动态内存。
  • 容器负责其存储对象的内存所有权。
  • 减少代码中显式使用new和delete的情况。

智能指针

独占指针
  • std::unique_ptr 表示独占所有权。
  • 独占所有权不可复制,但可转移。
1
2
auto owner = std::make_unique<User>("John");
auto new_owner = std::move(owner); // Transfer ownership
共享指针
  • std::shared_ptr表示共享所有权。
  • 使用引用计数跟踪对象的所有者数量。
  • 当最后一个所有者释放时,对象自动删除。
1
auto i = std::make_shared<double>(42.0);
弱指针

示例应用场景:

场景 解决方案
GUI 中父子窗口 父窗口持有子窗口(shared),子窗口持有父窗口(weak)
树形结构节点 父节点持有子节点(shared),子节点回指父节点(weak)
观察者模式 被观察者持有 weak_ptr 指向观察者
  • std::weak_ptr 表示弱所有权,不延长对象生命周期。是一种不会增加引用计数的指针。
  • 用于打破共享指针之间的循环引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <memory>
#include <iostream>

struct B;

struct A {
std::shared_ptr<B> b;
~A() { std::cout << "A destroyed\n"; }
};

struct B {
std::weak_ptr<A> a; // 改为 weak_ptr,打破循环
~B() { std::cout << "B destroyed\n"; }
};

int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();

a->b = b; // A 持有 B(shared)
b->a = a; // B 持有 A(weak)

return 0; // 程序结束后 A 和 B 都被正确销毁
}
  • 使用lock()方法将weak指针转换为shared指针。
1
2
3
4
5
if (auto shared_i = weak_i.lock()) {
std::cout << *shared_i << '\n'; // 安全访问
} else {
std::cout << "对象已被销毁,无法访问\n";
}
关键词 含义
weak_i 一个 std::weak_ptr<int>,指向某个 std::shared_ptr<int> 管理的对象(可能已释放)
weak_i.lock() 尝试从 weak_ptr 获取一个临时的 shared_ptr,如果对象还存在,返回有效的 shared_ptr,否则返回空指针
if (auto shared_i = ...) 如果成功获取到了有效的 shared_ptr,则进入 if 分支;否则说明原对象已经销毁,进入 else

小型优化

动态内存分配开销大:普通的容器如 std::vectorstd::string 在存储数据时,通常会在堆上分配内存。当存储的数据量很小,比如只有几个字符时,分配和释放堆内存的开销反而会影响性能。

  • 对于短字符串或小容器,使用栈内存代替堆内存以提升性能。
  • 标准库 std::string 通常使用小字符串优化(SSO)。
  • 实际使用union实现短模式和长模式的内存布局切换。
  • 示例:std::string在24字节栈内存中可存储22字符。
image-20250717221907713

自定义内存管理

构建一个内存池(Arena)

  • Arena是一个连续内存块,用于高效分配和回收内存。
  • 支持固定大小分配、单线程优化、有限生命周期等策略。单线程:无需锁,速度快
  • 示例:使用Howard Hinnant的short_alloc实现栈分配器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
template <size_t N>
class Arena {
static constexpr size_t alignment = alignof(std::max_align_t); // 最大对齐需求
public:
Arena() noexcept : ptr_(buffer_) {} // 指针初始指向buffer头部
Arena(const Arena&) = delete; // 禁止拷贝构造和赋值
Arena& operator=(const Arena&) = delete;

auto reset() noexcept { ptr_ = buffer_; } // 重置指针,回收所有内存
static constexpr auto size() noexcept { return N; }
auto used() const noexcept { return static_cast<size_t>(ptr_ - buffer_); } // 已用内存大小

auto allocate(size_t n) -> char*; // 分配函数声明
auto deallocate(char* p, size_t n) noexcept -> void; // 释放函数声明

private:
// 对齐函数:将n向上取整到alignment的倍数
static auto align_up(size_t n) noexcept -> size_t {
return (n + (alignment-1)) & ~(alignment-1);
}

// 检查指针是否在buffer范围内
auto pointer_in_buffer(const char* p) const noexcept -> bool {
return buffer_ <= p && p <= buffer_ + N;
}

alignas(alignment) char buffer_[N]; // 内存缓冲区,大小N
char* ptr_{}; // 当前分配指针
};

  • Arena类模板支持对齐内存分配。
  • allocate和deallocate方法用于分配和回收内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template<size_t N>
auto Arena<N>::allocate(size_t n) -> char* {
const auto aligned_n = align_up(n); // 调整大小到对齐边界
const auto available_bytes = static_cast<decltype(aligned_n)>(buffer_ + N - ptr_); // 剩余空间大小
if (available_bytes >= aligned_n) {
char* r = ptr_; // 返回当前指针
ptr_ += aligned_n; // 移动指针,表示已使用
return r;
}
return static_cast<char*>(::operator new(n)); // 缓冲区不够大时,调用全局new分配
}
template<size_t N>
auto Arena<N>::deallocate(char* p, size_t n) noexcept -> void {
if (pointer_in_buffer(p)) { // 如果指针属于 buffer_
n = align_up(n);
if (p + n == ptr_) { // 只有释放的是最后分配的内存块,才回收指针
ptr_ = p;
}
// 否则忽略释放请求(不能释放中间内存)
} else {
::operator delete(p); // 非buffer内存交给全局delete处理
}
}

  • 示例:为User类重载new和delete操作符,使用Arena分配内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
auto user_arena = Arena<1024>{};  // 创建一个1024字节的arena

class User {
public:
auto operator new(size_t size) -> void* {
return user_arena.allocate(size);
}
auto operator delete(void* p) -> void {
user_arena.deallocate(static_cast<char*>(p), sizeof(User));
}
// 支持数组new/delete
auto operator new[](size_t size) -> void* {
return user_arena.allocate(size);
}
auto operator delete[](void* p, size_t size) -> void {
user_arena.deallocate(static_cast<char*>(p), size);
}

private:
int id_{};
};

自定义内存分配器

  • 为什么类特定的 operator new 没有被调用?
    • 因为 std::make_shared 需要一次性分配足够空间给对象和引用计数控制块。它使用的是一次内存分配 + placement new 构造对象,而不是单纯调用 new User()
    • std::vector<User> users; users.reserve(10); reserve 只分配内存,不构造元素。这内存分配调用的是 vector 默认的分配器(std::allocator),不会调用 User 的 operator new。
  • 自定义分配器可用于标准容器和智能指针。
  • C++11中自定义分配器的最小接口包括allocate和deallocate方法。
  • 示例:实现Mallocator使用malloc/free进行内存管理。
  • 实现ShortAlloc分配器,绑定Arena实例进行栈内存分配。
  • 示例:使用栈内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
template <class T, size_t N>
struct ShortAlloc {
using value_type = T;
using arena_type = Arena<N>;

ShortAlloc(arena_type& arena) noexcept : arena_(arena) {}

template <class U>
ShortAlloc(const ShortAlloc<U, N>& other) noexcept : arena_(other.arena_) {}

T* allocate(size_t n) {
return reinterpret_cast<T*>(arena_.allocate(n * sizeof(T)));
}

void deallocate(T* p, size_t n) noexcept {
arena_.deallocate(reinterpret_cast<char*>(p), n * sizeof(T));
}

template <class U, size_t M>
bool operator==(const ShortAlloc<U, M>& other) const noexcept {
return N == M && std::addressof(arena_) == std::addressof(other.arena_);
}

template <class U, size_t M>
bool operator!=(const ShortAlloc<U, M>& other) const noexcept {
return !(*this == other);
}

private:
arena_type& arena_;
};

  • 自定义分配器提升性能,减少堆内存使用。

MetaProgramming

什么是模板元编程?

模板元编程(Template Metaprogramming)是在编译期用模板机制编写代码,用于自动生成最终可编译的 C++ 代码

  • 正常的代码在 运行时 执行;
  • 元编程是在 编译时 发生,生成新的代码。

用处:

  • 实现通用函数和类(支持多种类型)
  • 消除重复代码(如浮点版和整型版的函数)
  • 做到编译期检查、优化甚至计算
1
2
3
4
5
6
7
8
template <typename T>
auto pow_n(const T& v, int n) {
auto product = T{1};
for (int i = 0; i < n; ++i) {
product *= v;
}
return product;
}

编译器行为: 每当你使用不同的类型 T(比如 float, int),编译器会为你自动生成一个版本:

1
2
auto x = pow_n<float>(2.0f, 3); // 生成 float 版本
auto y = pow_n<int>(3, 3); // 生成 int 版本

编译器会为每种模板实例化 生成真正的函数/类

模板也可以做限制和校验

1
2
3
4
5
template <int N, typename T>
auto const_pow_n(const T& v) {
static_assert(N >= 0, "Exponent must be non-negative");
...
}

Type Traits(类型萃取)

type_traits 是 C++ 标准库中提供的一组 编译期工具类模板,用于检查或转换类型信息,全部在 <type_traits> 头文件里。

两种类型萃取

返回布尔/整型值的类型萃取

这些用来判断类型的某些特性(如是否是浮点数、是否是指针等):

  • C++17 引入了简洁写法:xxx_v

    1
    2
    std::is_floating_point_v<float>  // true
    std::is_same_v<int, int> // true

使用 type traits 提升函数智能性

1
2
3
4
5
6
7
template <typename T>
auto sign_func(const T& v) -> int {
if (std::is_unsigned_v<T>) {
return 1;
}
return v < 0 ? -1 : 1;
}

编译器在编译时就知道 T 是不是 unsigned,可以直接“裁剪”掉分支:这就是类型萃取结合模板的强大之处:根据类型做出“静态分支优化”,无运行时开销!

decltype:获取变量或表达式的类型

1
2
3
4
5
6
7
8
9
auto sign_func = [](const auto& v) -> int {
using ReferenceType = decltype(v); // auto const&
using ValueType = std::remove_reference_t<ReferenceType>; // 去引用得到原始类型

if (std::is_unsigned_v<ValueType>) {
return 1;
}
return v < 0 ? -1 : 1;
};

例子2

1
2
3
4
5
6
7
8
template <typename Range>
auto to_vector(const Range& r) {
using IteratorType = decltype(r.begin()); // 容器的迭代器类型
using ReferenceType = decltype(*IteratorType()); // 迭代器解引用的值类型(如 int&)
using ValueType = std::decay_t<ReferenceType>; // 去掉引用和 const(得到真正元素类型)

return std::vector<ValueType>(r.begin(), r.end());
}

std::enable_if_t 条件启用函数(SFINAE)

1
2
3
4
5
template <typename T>
auto interpolate(T left, T right, T power)
-> std::enable_if_t<std::is_floating_point_v<T>, T> {
return left + (right - left) * power;
}

检测类成员函数 —— std::is_detected(实验性)

1
2
3
4
template <typename T>
using has_to_string = decltype(&T::to_string);

static_assert(std::experimental::is_detected<has_to_string, MyClass>::value, "");

enable_if + is_detected 组合用法

1
2
3
4
5
6
7
8
template <typename T>
using has_name = decltype(T::name_);

template <typename T>
std::enable_if_t<std::experimental::is_detected<has_name, T>::value>
print(const T& v) {
std::cout << v.name_ << '\n';
}

constexpr 编译期函数

当所有参数是编译期常量时,它会在编译时执行。

1
2
3
4
5
constexpr auto sum(int x, int y, int z) {
return x + y + z;
}
const auto value = std::integral_constant<int, sum(1, 2, 3)>;
// 如果不是constexpr,编译报错

if constexpr 编译期条件分支

1
2
3
4
5
template <typename Animal>
auto speak(const Animal& a) {
if constexpr (std::is_same_v<Animal, Bear>) a.roar();
else if constexpr (std::is_same_v<Animal, Duck>) a.quack();
}

编译期 vs 运行时 多态性对比

编译期多态(模板 + if constexpr):

  • 类型信息在编译时已知,无需虚表,开销小,内联优化能力强
  • 使用场景:追求性能,类型已知。

运行时多态(虚函数 + 多态继承):

  • 类型信息运行时才知道,有虚表开销
  • 使用场景:类型不确定或接口抽象统一。

std::tuple —— 固定大小的异构容器

1
2
3
4
auto tpl = std::make_tuple(42, std::string{"hi"}, true);
auto i = std::get<0>(tpl); // int
auto str = std::get<1>(tpl); // std::string
auto flag = std::get<2>(tpl); // bool

为什么 std::tuple 不能用 range-based for 循环?

因为 std::tuple异构容器,不同于 std::vector 等同构容器,它的每个元素可能是不同类型。而 for (const auto& v : tuple)v 的类型只能是一个确定的类型,无法适配多个类型,编译器在编译期就会报错。另外,std::tuple 也没有 begin()/end(),因此也不能用算法如 std::for_each

如何“遍历”一个 std::tuple

通过 模板元编程+递归调用+索引展开(index sequence) 实现编译期“展开”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <size_t Index, typename Tuple, typename Functor>
void tuple_at(const Tuple& tpl, const Functor& func) {
func(std::get<Index>(tpl));
}

template <typename Tuple, typename Functor, size_t Index = 0>
void tuple_for_each(const Tuple& tpl, const Functor& func) {
if constexpr (Index < std::tuple_size_v<Tuple>) {
tuple_at<Index>(tpl, func);
tuple_for_each<Tuple, Functor, Index + 1>(tpl, func);
}
}

auto tpl = std::make_tuple(1, true, std::string{"Jedi"});
tuple_for_each(tpl, [](const auto& v) { std::cout << v << " "; });

如何实现类似 any_of 的操作?

1
2
3
4
5
6
7
8
9
template <typename Tuple, typename Functor, size_t Index = 0>
bool tuple_any_of(const Tuple& tpl, const Functor& f) {
if constexpr (Index < std::tuple_size_v<Tuple>) {
return f(std::get<Index>(tpl)) ? true :
tuple_any_of<Tuple, Functor, Index + 1>(tpl, f);
} else {
return false;
}
}

结构化绑定(C++17)简化 tuple 解包

1
2
3
4
5
auto [name, id, license] = std::make_tuple("James", 7, true);

for (auto&& [name, id, license] : agents) {
std::cout << name << ", " << id << ", " << license << '\n';
}

用结构体代替 tuple 返回值(更可读)

1
2
3
4
auto make_bond() {
struct Agent { std::string name; int id; bool license; };
return Agent{"James", 7, true};
}

变参模板(Variadic Templates)

使函数可以接收任意数量参数

1
2
3
4
5
6
7
template <typename ...Ts>
auto expand_pack(const Ts& ...values) {
auto tuple = std::tie(values...);
}
// 变参模板在编译时会展开成:
expand_pack(42, std::string("hi"));
auto tuple = std::tie(42, "hi");

将变参模板转为字符串(结合 tuple_for_each)

1
2
3
4
5
6
7
8
template <typename ...Ts>
auto make_string(const Ts& ...values) {
std::ostringstream sstr;
auto tuple = std::tie(values...);
tuple_for_each(tuple, [&sstr](const auto& v){ sstr << v; });
return sstr.str();
}
auto s = make_string(1, "abc", true); // → "1abctrue"

动态大小 异构类型

方法一:std::any(C++17 起)

1
2
3
4
5
6
7
8
9
10
std::vector<std::any> container{42, "hi", true};
std::cout << a; // ❌ 不知道 std::any 里是什么类型,编译失败

for (const auto& a : container) {
if (a.type() == typeid(int)) {
std::cout << std::any_cast<int>(a);
} else if (a.type() == typeid(const char*)) {
std::cout << std::any_cast<const char*>(a);
} // ...
}

方法二:std::variant(C++17 起)

1
2
3
4
using VariantType = std::variant<int, std::string, bool>;
VariantType v = 7;
v = std::string{"Bjarne"};
v = false;

它是一个“受限类型的类型联合体”。

std::tuple 不同,它一次只存一个值,但类型由一个 固定列表指定。

编译器知道它可能的类型组合,无需手动判断类型,使用 std::visit 自动分发。

1
2
3
std::visit([](const auto& v) {
std::cout << v;
}, v);

编译器会自动为每个可能的类型生成对应的 operator() 重载,如:

1
2
3
4
5
struct FunctorImpl {
void operator()(const int& v) { std::cout << v; }
void operator()(const std::string& v) { std::cout << v; }
void operator()(const bool& v) { std::cout << v; }
};

实际上,std::visit 做的就是一个 type-switch:

构建动态大小、异构类型容器

1
2
3
4
5
using VariantType = std::variant<int, std::string, bool>;
std::vector<VariantType> container;
container.push_back(false);
container.push_back(std::string{"I am a string"});
container.push_back(13);

遍历

1
2
3
4
5
6
7
8
9
10
11
for (const auto& val : container) {
std::visit([](const auto& v){ std::cout << v << '\n'; }, val);
}
int count = std::count_if(container.begin(), container.end(), [](const auto& v){
return std::holds_alternative<bool>(v);
});

bool contains = std::any_of(container.begin(), container.end(), [](const auto& v){
return std::holds_alternative<std::string>(v) &&
std::get<std::string>(v) == "needle";
});

反射

什么是反射(Reflection)?

“反射”是指程序在运行时可以“查看”或“操作”自己的结构,比如成员变量、类型信息等。

C++ 不支持原生反射,所以我们要“伪造”一个。

问题:如何让类暴露出它的成员变量?

方法:实现一个 reflect() 成员函数,返回 std::tie(...) 构成的 std::tuple 引用。

1
2
3
4
5
6
7
8
9
10
11
12
class Town {
public:
Town(size_t houses, size_t settlers, const std::string& name)
: houses_{houses}, settlers_{settlers}, name_{name} {}

auto reflect() const { return std::tie(houses_, settlers_, name_); }

private:
size_t houses_{};
size_t settlers_{};
std::string name_{};
};

std::tie 会构造一个 std::tuple 的引用。

这样我们可以操作、比较、访问成员变量,就像处理 tuple 一样。

如何使用 reflect()

🎯 目标:自动实现如下功能:

  • 比较(==, !=, <)
  • 输出(重载 operator<<
1
2
3
4
5
6
7
8
9
10
auto operator==(const Town& t) const {
return reflect() == t.reflect();
}

auto& operator<<(std::ostream& os, const Town& t) {
tuple_for_each(t.reflect(), [&os](const auto& val){
os << val << " ";
});
return os;
}

提升:用元编程自动化支持这些功能

🛠 判断类是否 reflect() 可用:

利用 std::experimental::is_detected 判断类是否有 reflect() 方法:

1
2
3
4
5
template <typename T>
using has_reflect_member = decltype(&T::reflect);

template <typename T>
constexpr bool is_reflectable_v = std::experimental::is_detected<has_reflect_member, T>::value;

基于此,自动启用操作符重载:

1
2
3
4
template <typename T, bool B = is_reflectable_v<T>>
auto operator==(const T& a, const T& b) -> std::enable_if_t<B, bool> {
return a.reflect() == b.reflect();
}

完整

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Town {
public:
Town(size_t h, size_t s, std::string n) : houses_(h), settlers_(s), name_(n) {}
auto reflect() const { return std::tie(houses_, settlers_, name_); }

private:
size_t houses_;
size_t settlers_;
std::string name_;
};

// 全局支持:自动提供 ==, !=, <, << 等
template <typename T, bool B = is_reflectable_v<T>>
auto operator==(const T& a, const T& b) -> std::enable_if_t<B, bool> {
return a.reflect() == b.reflect();
}

template <typename T, bool B = is_reflectable_v<T>>
auto operator<<(std::ostream& os, const T& t) -> std::enable_if_t<B, std::ostream&> {
tuple_for_each(t.reflect(), [&os](const auto& m){ os << m << " "; });
return os;
}

这套“伪反射”机制虽然比不上Java/C#的内建反射强大,但在 C++ 中非常实用。它是一种利用元编程提升代码复用性和可维护性的典范,广泛用于:

  • JSON/XML 序列化
  • 数据库 ORM 映射
  • UI 数据绑定
  • 自动日志/诊断系统
  • 泛型算法适配器

安全泛型类型转换

为什么需要 safe_cast

在 C++ 中使用 static_castreinterpret_castdynamic_castconst_cast 时,可能会发生以下问题:

  1. 精度丢失:比如 doublefloat,或者 int64_tint32_t
  2. 符号混淆:比如负数转无符号类型;
  3. 指针类型转换错误:不同类型的指针强转可能导致 UB(未定义行为);
  4. 指针转整数:只有 uintptr_t/intptr_t 是合法的;
  5. float -> float 的截断溢出(如 1e39 转 float 得到 inf);

因此,我们希望有一种方法:

  • 调试模式 下执行运行时检查;
  • 发布模式 下快速转换;
  • 类型不合法时 编译报错(而不是运行时爆炸)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
template <typename T> constexpr auto make_false() { return false; }

template <typename Dst, typename Src>
auto safe_cast(const Src& v) -> Dst {
using namespace std;

constexpr auto is_same_type = is_same_v<Src, Dst>;
constexpr auto is_pointer_to_pointer = is_pointer_v<Src> && is_pointer_v<Dst>;
constexpr auto is_float_to_float = is_floating_point_v<Src> && is_floating_point_v<Dst>;
constexpr auto is_number_to_number = is_arithmetic_v<Src> && is_arithmetic_v<Dst>;
constexpr auto is_intptr_to_ptr = (is_same_v<uintptr_t, Src> || is_same_v<intptr_t, Src>) && is_pointer_v<Dst>;
constexpr auto is_ptr_to_intptr = is_pointer_v<Src> && (is_same_v<uintptr_t, Dst> || is_same_v<intptr_t, Dst>);

if constexpr (is_same_type) {
return v;
}
else if constexpr (is_intptr_to_ptr || is_ptr_to_intptr) {
return reinterpret_cast<Dst>(v);
}
else if constexpr (is_pointer_to_pointer) {
assert(dynamic_cast<Dst>(v) != nullptr); // 确保能安全 downcast
return static_cast<Dst>(v);
}
else if constexpr (is_float_to_float) {
auto casted = static_cast<Dst>(v);
auto casted_back = static_cast<Src>(casted);
assert(!isnan(casted_back) && !isinf(casted_back));
return casted;
}
else if constexpr (is_number_to_number) {
auto casted = static_cast<Dst>(v);
auto casted_back = static_cast<Src>(casted);
assert(casted == casted_back); // 确保无精度损失
return casted;
}
else {
static_assert(make_false<Src>(), "safe_cast(): Unsupported cast");
return Dst{};
}
}

编译期字符串哈希

目标

避免运行时重复对字符串计算哈希,提高性能。

std::unordered_map 中,key 是 std::string 时,每次查找都要重新计算哈希。而如果我们能在编译期就计算好哈希值,可以显著优化:

1
2
3
4
5
6
7
constexpr auto hash_function(const char* str) -> size_t {
size_t sum = 0;
for (auto p = str; *p != '\0'; ++p) {
sum += *p;
}
return sum;
}

实现 PrehashedString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class PrehashedString {
public:
template <size_t N>
constexpr PrehashedString(const char (&str)[N])
: hash_{hash_function(str)},
size_{N - 1}, // 不包括 null terminator
strptr_{str} {}

auto operator==(const PrehashedString& other) const {
return size_ == other.size_ &&
std::equal(c_str(), c_str() + size_, other.c_str());
}

auto get_hash() const { return hash_; }
auto c_str() const -> const char* { return strptr_; }

private:
size_t hash_;
size_t size_;
const char* strptr_;
};

并在 std 命名空间里自定义 std::hash

1
2
3
4
5
6
7
8
namespace std {
template <>
struct hash<PrehashedString> {
constexpr size_t operator()(const PrehashedString& s) const {
return s.get_hash();
}
};
}

编译期验证示例

1
2
3
4
5
auto test() {
const auto& s = PrehashedString("abc");
return std::hash<PrehashedString>{}(s);
}
// 编译期就确定 hash = 294,汇编中出现 .quad 294