diff --git a/README.md b/README.md index ddcf6c179..57eb0e5d3 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ --- -![English Coverage](https://img.shields.io/badge/en_coverage-100%25-green.svg) 403/403 docs translated +![English Coverage](https://img.shields.io/badge/en_coverage-98%25-green.svg) 403/411 docs translated ## 这是什么项目 diff --git a/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/CMakeLists.txt b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/CMakeLists.txt new file mode 100644 index 000000000..ed40afcac --- /dev/null +++ b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.14) +project(pointer-semantics-demo LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +set(COMMON_FLAGS -Wall -Wextra -Wpedantic -g) + +# 文章 01:Borrowed 和 ObserverPtr +add_executable(test_borrowed_observer test_borrowed_observer.cpp) +target_compile_options(test_borrowed_observer PRIVATE ${COMMON_FLAGS}) + +# 文章 02:UnsafeWeakPtr UB 演示 +add_executable(test_unsafe_ub test_unsafe_weak_ptr_ub.cpp) +target_compile_options(test_unsafe_ub PRIVATE ${COMMON_FLAGS}) + +# 文章 02:UnsafeWeakPtr UB 演示(ASan 版本) +add_executable(test_unsafe_ub_asan test_unsafe_weak_ptr_ub.cpp) +target_compile_options(test_unsafe_ub_asan PRIVATE ${COMMON_FLAGS} -fsanitize=address,undefined) +target_link_options(test_unsafe_ub_asan PRIVATE -fsanitize=address,undefined) + +# 文章 03:SimpleWeakPtr 安全失效检测 +add_executable(test_simple_wp test_simple_weak_ptr.cpp) +target_compile_options(test_simple_wp PRIVATE ${COMMON_FLAGS}) + +# 文章 04:Chrome-like WeakPtr 生命周期验证 +add_executable(test_lifetime test_lifetime_verification.cpp) +target_compile_options(test_lifetime PRIVATE ${COMMON_FLAGS}) diff --git a/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/borrowed.h b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/borrowed.h new file mode 100644 index 000000000..6bd31d0d6 --- /dev/null +++ b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/borrowed.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +template class Borrowed { + public: + explicit Borrowed(T& ref) noexcept : ptr_(&ref) {} + + Borrowed(T&&) = delete; + + Borrowed(std::nullptr_t) = delete; + + explicit Borrowed(T* ptr) noexcept : ptr_(ptr) { + assert(ptr != nullptr && "Borrowed requires a non-null pointer"); + } + + Borrowed(const Borrowed&) = default; + Borrowed& operator=(const Borrowed&) = default; + Borrowed(Borrowed&&) = default; + Borrowed& operator=(Borrowed&&) = default; + + T& get() const noexcept { return *ptr_; } + T* operator->() const noexcept { return ptr_; } + T& operator*() const noexcept { return *ptr_; } + + template operator Borrowed() const noexcept { + return Borrowed(*ptr_); + } + + private: + T* ptr_; +}; + +template Borrowed borrow(T& ref) noexcept { + return Borrowed(ref); +} diff --git a/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/observer_ptr.h b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/observer_ptr.h new file mode 100644 index 000000000..6a18c6c56 --- /dev/null +++ b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/observer_ptr.h @@ -0,0 +1,51 @@ +#pragma once + +#include + +template class ObserverPtr { + public: + ObserverPtr() noexcept : ptr_(nullptr) {} + ObserverPtr(std::nullptr_t) noexcept : ptr_(nullptr) {} + explicit ObserverPtr(T* ptr) noexcept : ptr_(ptr) {} + + ObserverPtr(const ObserverPtr&) = default; + ObserverPtr& operator=(const ObserverPtr&) = default; + ObserverPtr(ObserverPtr&&) = default; + ObserverPtr& operator=(ObserverPtr&&) = default; + + void reset(T* ptr = nullptr) noexcept { ptr_ = ptr; } + + T* release() noexcept { + T* old = ptr_; + ptr_ = nullptr; + return old; + } + + T* get() const noexcept { return ptr_; } + T& operator*() const noexcept { return *ptr_; } + T* operator->() const noexcept { return ptr_; } + + explicit operator bool() const noexcept { return ptr_ != nullptr; } + + void swap(ObserverPtr& other) noexcept { + T* tmp = ptr_; + ptr_ = other.ptr_; + other.ptr_ = tmp; + } + + private: + T* ptr_; +}; + +template +bool operator==(const ObserverPtr& a, const ObserverPtr& b) noexcept { + return a.get() == b.get(); +} + +template bool operator==(const ObserverPtr& a, std::nullptr_t) noexcept { + return !a; +} + +template ObserverPtr make_observer(T* ptr) noexcept { + return ObserverPtr(ptr); +} diff --git a/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/simple_weak_ptr.h b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/simple_weak_ptr.h new file mode 100644 index 000000000..11b3932dc --- /dev/null +++ b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/simple_weak_ptr.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +struct AtomicFlag { + std::atomic alive{true}; + + void invalidate() { alive.store(false, std::memory_order_release); } + bool is_alive() const { return alive.load(std::memory_order_acquire); } +}; + +template class SimpleWeakPtr { + public: + SimpleWeakPtr() = default; + + SimpleWeakPtr(T* ptr, std::shared_ptr flag) : ptr_(ptr), flag_(std::move(flag)) {} + + bool is_valid() const { return flag_ && flag_->is_alive(); } + + T* get() const { + if (is_valid()) { + return ptr_; + } + return nullptr; + } + + T& operator*() const { return *get(); } + T* operator->() const { return get(); } + explicit operator bool() const { return get() != nullptr; } + + private: + T* ptr_ = nullptr; + std::shared_ptr flag_; +}; + +template class SimpleWeakPtrFactory { + public: + explicit SimpleWeakPtrFactory(T* owner) + : owner_(owner), flag_(std::make_shared()) {} + + SimpleWeakPtrFactory(const SimpleWeakPtrFactory&) = delete; + SimpleWeakPtrFactory& operator=(const SimpleWeakPtrFactory&) = delete; + + SimpleWeakPtr get_weak_ptr() { return SimpleWeakPtr(owner_, flag_); } + + void invalidate() { + if (flag_) { + flag_->invalidate(); + } + } + + ~SimpleWeakPtrFactory() { invalidate(); } + + private: + T* owner_; + std::shared_ptr flag_; +}; diff --git a/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/test_borrowed_observer.cpp b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/test_borrowed_observer.cpp new file mode 100644 index 000000000..35b5f0a47 --- /dev/null +++ b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/test_borrowed_observer.cpp @@ -0,0 +1,60 @@ +// 文章 01 测试:Borrowed 和 ObserverPtr 基本用法 +// 编译:g++ -std=c++17 -Wall -Wextra -g -o test_borrowed_observer test_borrowed_observer.cpp + +#include "borrowed.h" +#include "observer_ptr.h" +#include +#include +#include + +void test_borrowed() { + std::cout << "=== Borrowed tests ===\n"; + + int x = 42; + auto b = borrow(x); + std::cout << " borrow(x) = " << b.get() << " (expect 42)\n"; + + // 从指针构造(非空) + Borrowed bp(&x); + std::cout << " Borrowed(&x) = " << bp.get() << " (expect 42)\n"; + + // const 转换 + Borrowed bc = bp; + std::cout << " const conversion: " << bc.get() << " (expect 42)\n"; + + // 函数参数用法 + auto process = [](Borrowed> data) { return data.get().size(); }; + std::vector v{1, 2, 3}; + std::cout << " process(borrow(v)) = " << process(borrow(v)) << " (expect 3)\n"; + + std::cout << " sizeof(Borrowed) = " << sizeof(Borrowed) << " (expect 8)\n\n"; +} + +void test_observer_ptr() { + std::cout << "=== ObserverPtr tests ===\n"; + + int a = 10, b = 20; + auto obs = make_observer(&a); + std::cout << " make_observer(&a): " << *obs << " (expect 10)\n"; + + // operator bool + ObserverPtr null_obs; + std::cout << " default constructed: " << (null_obs ? "non-null" : "null") + << " (expect null)\n"; + std::cout << " with value: " << (obs ? "non-null" : "null") << " (expect non-null)\n"; + + // reset and release + obs.reset(&b); + std::cout << " after reset(&b): " << *obs << " (expect 20)\n"; + int* released = obs.release(); + std::cout << " after release: obs=" << (obs ? "non-null" : "null") << " released=" << *released + << " (expect null, 20)\n"; + + std::cout << " sizeof(ObserverPtr) = " << sizeof(ObserverPtr) << " (expect 8)\n\n"; +} + +int main() { + test_borrowed(); + test_observer_ptr(); + return 0; +} diff --git a/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/test_lifetime_verification.cpp b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/test_lifetime_verification.cpp new file mode 100644 index 000000000..a88a5d158 --- /dev/null +++ b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/test_lifetime_verification.cpp @@ -0,0 +1,90 @@ +// 生命周期验证测试:Chrome-like WeakPtr vs UnsafeWeakPtr +// 编译:g++ -std=c++17 -O0 -g -o test_lifetime test_lifetime_verification.cpp + +#include "weak_ptr_factory.h" +#include +#include + +// ===== Chrome-like WeakPtr 测试 ===== + +class Session { + public: + explicit Session(int id) : id_(id) { std::cout << "Session(" << id_ << ") constructed\n"; } + + ~Session() { std::cout << "Session(" << id_ << ") destroyed\n"; } + + WeakPtr get_weak_ptr() { return factory_.get_weak_ptr(); } + + void do_work() const { std::cout << "Session(" << id_ << ") doing work\n"; } + + int id() const { return id_; } + + private: + int id_; + // Factory 放在最后——确保在其他成员析构之前 invalidate + WeakPtrFactory factory_{this}; +}; + +void test_weak_ptr_survives_owner() { + std::cout << "=== Test: WeakPtr survives Owner ===\n"; + + WeakPtr weak = [] { + auto s = std::make_unique(42); + auto w = s->get_weak_ptr(); + std::cout << " Before destroy: valid = " << std::boolalpha << w.is_valid() << "\n"; + return w; + // Session 在这里析构 + }(); + + std::cout << " After destroy: valid = " << std::boolalpha << weak.is_valid() << "\n"; + std::cout << " get() returns: " << (weak.get() ? "non-null (BAD)" : "nullptr (GOOD)") + << "\n\n"; +} + +void test_multiple_weak_ptrs() { + std::cout << "=== Test: Multiple WeakPtrs ===\n"; + + auto s = std::make_unique(99); + auto w1 = s->get_weak_ptr(); + auto w2 = s->get_weak_ptr(); + auto w3 = w1; + + std::cout << " All valid: " << w1.is_valid() << " " << w2.is_valid() << " " << w3.is_valid() + << "\n"; + + s.reset(); + + std::cout << " After destroy: " << w1.is_valid() << " " << w2.is_valid() << " " + << w3.is_valid() << "\n\n"; +} + +void test_weak_ptr_in_callback() { + std::cout << "=== Test: WeakPtr in simulated callback ===\n"; + + // 模拟异步回调场景 + WeakPtr weak; + + { + auto s = std::make_unique(7); + weak = s->get_weak_ptr(); + + // 模拟回调执行时对象还活着 + if (auto* self = weak.get()) { + self->do_work(); // 安全 + } + } + + // 模拟回调执行时对象已销毁 + if (auto* self = weak.get()) { + self->do_work(); // 不会执行——get() 返回 nullptr + } else { + std::cout << " Callback skipped: object already destroyed (GOOD)\n\n"; + } +} + +int main() { + test_weak_ptr_survives_owner(); + test_multiple_weak_ptrs(); + test_weak_ptr_in_callback(); + return 0; +} diff --git a/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/test_simple_weak_ptr.cpp b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/test_simple_weak_ptr.cpp new file mode 100644 index 000000000..b673a71ee --- /dev/null +++ b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/test_simple_weak_ptr.cpp @@ -0,0 +1,71 @@ +// 文章 03 测试:SimpleWeakPtr 安全失效检测 +// 编译:g++ -std=c++17 -Wall -Wextra -g -o test_simple_wp test_simple_weak_ptr.cpp + +#include "simple_weak_ptr.h" +#include +#include + +struct Service { + int id; + explicit Service(int id) : id(id) {} + SimpleWeakPtrFactory factory{this}; + + SimpleWeakPtr get_weak_ptr() { return factory.get_weak_ptr(); } +}; + +void test_safe_invalidation() { + std::cout << "=== Test: safe invalidation ===\n"; + + SimpleWeakPtr weak = [] { + auto s = std::make_unique(42); + auto w = s->get_weak_ptr(); + std::cout << " Before destroy: is_valid() = " << std::boolalpha << w.is_valid() + << " (expect true)\n"; + return w; + // Service 析构 → factory 析构 → invalidate + // 但 AtomicFlag 仍然活着(shared_ptr 引用计数保持) + }(); + + // Flag 对象仍然存在,可以安全判空 + std::cout << " After destroy: is_valid() = " << std::boolalpha << weak.is_valid() + << " (expect false)\n"; + std::cout << " get() = " << (weak.get() ? "non-null (BAD)" : "nullptr (GOOD)") << "\n\n"; +} + +void test_multiple_weak_ptrs() { + std::cout << "=== Test: multiple weak ptrs ===\n"; + + auto s = std::make_unique(99); + auto w1 = s->get_weak_ptr(); + auto w2 = s->get_weak_ptr(); + auto w3 = w1; + + std::cout << " All valid: " << w1.is_valid() << " " << w2.is_valid() << " " << w3.is_valid() + << " (expect true true true)\n"; + + s.reset(); + + std::cout << " After destroy: " << w1.is_valid() << " " << w2.is_valid() << " " + << w3.is_valid() << " (expect false false false)\n\n"; +} + +void test_flag_outlives_owner() { + std::cout << "=== Test: flag outlives owner ===\n"; + + SimpleWeakPtr weak; + { + auto s = std::make_unique(7); + weak = s->get_weak_ptr(); + std::cout << " In scope: is_valid() = " << weak.is_valid() << " (expect true)\n"; + } + // Service 和 factory 已析构,但 AtomicFlag 仍然活着 + std::cout << " Out of scope: is_valid() = " << weak.is_valid() << " (expect false)\n"; + std::cout << " Flag is still alive: " << (weak.is_valid() || true) << " (safe to check)\n\n"; +} + +int main() { + test_safe_invalidation(); + test_multiple_weak_ptrs(); + test_flag_outlives_owner(); + return 0; +} diff --git a/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/test_unsafe_weak_ptr_ub.cpp b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/test_unsafe_weak_ptr_ub.cpp new file mode 100644 index 000000000..b140ee6fa --- /dev/null +++ b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/test_unsafe_weak_ptr_ub.cpp @@ -0,0 +1,58 @@ +// 文章 02 测试:UnsafeWeakPtr UB 演示 +// 编译(观察 UB): +// g++ -std=c++17 -O0 -g -o test_unsafe_ub test_unsafe_weak_ptr_ub.cpp +// 编译(ASan 检测 UB): +// g++ -std=c++17 -O0 -g -fsanitize=address,undefined -o test_unsafe_ub_asan +// test_unsafe_weak_ptr_ub.cpp + +#include "unsafe_weak_ptr.h" +#include +#include + +struct Widget { + int value = 42; + UnsafeWeakPtrFactory factory{this}; + + UnsafeWeakPtr get_weak_ptr() { return factory.get_weak_ptr(); } +}; + +void test_ub_after_owner_destroyed() { + std::cout << "=== Test: UB after owner destroyed ===\n"; + + UnsafeWeakPtr weak = [] { + auto w = std::make_unique(); + return w->get_weak_ptr(); + // w 在这里析构 → factory 析构 → Flag 析构 + }(); + + // ⚠️ UB:flag_ 指向已销毁的 Flag + std::cout << " is_valid() = " << std::boolalpha << weak.is_valid() << '\n'; + + if (auto* p = weak.get()) { + std::cout << " value = " << p->value << " (UB: reading freed memory)\n"; + } else { + std::cout << " get() returned nullptr (result is UB-dependent)\n"; + } + std::cout << " Note: run with -fsanitize=address to see heap-use-after-free\n\n"; +} + +void test_valid_while_owner_alive() { + std::cout << "=== Test: valid while owner alive ===\n"; + + auto w = std::make_unique(); + auto weak = w->get_weak_ptr(); + + std::cout << " is_valid() = " << std::boolalpha << weak.is_valid() << " (expect true)\n"; + std::cout << " get()->value = " << weak.get()->value << " (expect 42)\n"; + + w.reset(); + // Flag 随 Widget 析构,weak.flag_ 悬垂 + std::cout << " after reset: is_valid() = " << weak.is_valid() + << " (UB, may print anything)\n\n"; +} + +int main() { + test_valid_while_owner_alive(); + test_ub_after_owner_destroyed(); + return 0; +} diff --git a/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/unsafe_weak_ptr.h b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/unsafe_weak_ptr.h new file mode 100644 index 000000000..39648b96b --- /dev/null +++ b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/unsafe_weak_ptr.h @@ -0,0 +1,47 @@ +#pragma once + +// ⚠️ 教学用反模式实现,不要在生产代码中使用 +// 演示 T* + raw Flag* 为什么不是可靠的 WeakPtr + +struct Flag { + bool alive = true; +}; + +template class UnsafeWeakPtr { + public: + UnsafeWeakPtr(T* ptr, Flag* flag) : ptr_(ptr), flag_(flag) {} + + bool is_valid() const { return flag_ && flag_->alive; } + + T* get() const { + if (is_valid()) { + return ptr_; + } + return nullptr; + } + + T& operator*() const { return *get(); } + T* operator->() const { return get(); } + + private: + T* ptr_; + Flag* flag_; +}; + +template class UnsafeWeakPtrFactory { + public: + explicit UnsafeWeakPtrFactory(T* owner) : owner_(owner) {} + + UnsafeWeakPtrFactory(const UnsafeWeakPtrFactory&) = delete; + UnsafeWeakPtrFactory& operator=(const UnsafeWeakPtrFactory&) = delete; + + UnsafeWeakPtr get_weak_ptr() { return UnsafeWeakPtr(owner_, &flag_); } + + void invalidate() { flag_.alive = false; } + + ~UnsafeWeakPtrFactory() { flag_.alive = false; } + + private: + T* owner_; + Flag flag_; +}; diff --git a/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/weak_flag.h b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/weak_flag.h new file mode 100644 index 000000000..5f88dc05d --- /dev/null +++ b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/weak_flag.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +class WeakFlag { + public: + WeakFlag() = default; + + WeakFlag(const WeakFlag&) = delete; + WeakFlag& operator=(const WeakFlag&) = delete; + + void add_ref() { ref_count_.fetch_add(1, std::memory_order_relaxed); } + + void release() { + if (ref_count_.fetch_sub(1, std::memory_order_acq_rel) == 1) { + delete this; + } + } + + void invalidate() { is_valid_.store(false, std::memory_order_release); } + + bool is_valid() const { return is_valid_.load(std::memory_order_acquire); } + + private: + std::atomic is_valid_{true}; + std::atomic ref_count_{1}; + ~WeakFlag() = default; +}; diff --git a/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/weak_ptr.h b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/weak_ptr.h new file mode 100644 index 000000000..2355d26fe --- /dev/null +++ b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/weak_ptr.h @@ -0,0 +1,75 @@ +#pragma once + +#include "weak_flag.h" + +template class WeakPtr { + public: + WeakPtr() : ptr_(nullptr), flag_(nullptr) {} + + WeakPtr(T* ptr, WeakFlag* flag) : ptr_(ptr), flag_(flag) { + if (flag_) { + flag_->add_ref(); + } + } + + WeakPtr(const WeakPtr& other) : ptr_(other.ptr_), flag_(other.flag_) { + if (flag_) { + flag_->add_ref(); + } + } + + WeakPtr(WeakPtr&& other) noexcept : ptr_(other.ptr_), flag_(other.flag_) { + other.ptr_ = nullptr; + other.flag_ = nullptr; + } + + WeakPtr& operator=(const WeakPtr& other) { + if (this != &other) { + if (flag_) { + flag_->release(); + } + ptr_ = other.ptr_; + flag_ = other.flag_; + if (flag_) { + flag_->add_ref(); + } + } + return *this; + } + + WeakPtr& operator=(WeakPtr&& other) noexcept { + if (this != &other) { + if (flag_) { + flag_->release(); + } + ptr_ = other.ptr_; + flag_ = other.flag_; + other.ptr_ = nullptr; + other.flag_ = nullptr; + } + return *this; + } + + ~WeakPtr() { + if (flag_) { + flag_->release(); + } + } + + bool is_valid() const { return flag_ && flag_->is_valid(); } + + T* get() const { + if (is_valid()) { + return ptr_; + } + return nullptr; + } + + T& operator*() const { return *get(); } + T* operator->() const { return get(); } + explicit operator bool() const { return get() != nullptr; } + + private: + T* ptr_; + WeakFlag* flag_; +}; diff --git a/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/weak_ptr_factory.h b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/weak_ptr_factory.h new file mode 100644 index 000000000..14df54afe --- /dev/null +++ b/code/volumn_codes/vol8/cpp-deep-dives/pointer-semantics/weak_ptr_factory.h @@ -0,0 +1,34 @@ +#pragma once + +#include "weak_flag.h" +#include "weak_ptr.h" + +template class WeakPtrFactory { + public: + explicit WeakPtrFactory(T* owner) : owner_(owner) { flag_ = new WeakFlag(); } + + WeakPtrFactory(const WeakPtrFactory&) = delete; + WeakPtrFactory& operator=(const WeakPtrFactory&) = delete; + WeakPtrFactory(WeakPtrFactory&&) = delete; + WeakPtrFactory& operator=(WeakPtrFactory&&) = delete; + + WeakPtr get_weak_ptr() { return WeakPtr(owner_, flag_); } + + void invalidate_weak_ptrs() { + if (flag_) { + flag_->invalidate(); + } + } + + ~WeakPtrFactory() { + invalidate_weak_ptrs(); + if (flag_) { + flag_->release(); + } + flag_ = nullptr; + } + + private: + T* owner_; + WeakFlag* flag_; +}; diff --git a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md index dcdf536f7..a044be053 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md +++ b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md @@ -62,21 +62,17 @@ You can build an intuition using a rough time scale: if a register access takes The core design philosophy behind this pyramid structure is called the **Principle of Locality**. Locality comes in two forms: **Temporal locality** means that if a piece of data was just accessed, it is very likely to be accessed again in the near future; **Spatial locality** means that if a piece of data is accessed, data at nearby addresses is also likely to be accessed. All Cache design decisions—cache line size, prefetching policies, replacement policies—revolve entirely around these two types of locality. We can use a simple diagram to visualize this pyramid: -```text - ┌─────────────┐ - │ 寄存器 │ ~1 周期 | 容量: ~数百字节 - ├─────────────┤ - │ L1 Cache │ ~3-4 周期 | 容量: 32-64 KB - ├─────────────┤ - │ L2 Cache │ ~10-14 周期| 容量: 256 KB-1 MB - ├─────────────┤ - │ L3 Cache │ ~30-50 周期| 容量: 数 MB-数十 MB - ├─────────────┤ - │ 主存 DRAM │ ~100-300 周期 | 容量: GB 级 - ├─────────────┤ - │ SSD/HDD │ ~微秒/毫秒 | 容量: TB 级 - └─────────────┘ - 越往上越快、越小、越贵;越往下越慢、越大、越便宜 +```mermaid +graph TD + subgraph "Faster, smaller, more expensive toward top; slower, larger, cheaper toward bottom" + Reg["Registers
~1 cycle | Capacity: ~hundreds of bytes"] + L1["L1 Cache
~3-4 cycles | Capacity: 32-64 KB"] + L2["L2 Cache
~10-14 cycles | Capacity: 256 KB-1 MB"] + L3["L3 Cache
~30-50 cycles | Capacity: several MB-tens of MB"] + DRAM["Main Memory DRAM
~100-300 cycles | Capacity: GB level"] + Disk["SSD/HDD
~microseconds/milliseconds | Capacity: TB level"] + end + Reg ~~~ L1 ~~~ L2 ~~~ L3 ~~~ DRAM ~~~ Disk ``` On Linux, you can use the `lscpu` command to check your machine's Cache configuration. The `L1d cache`, `L2 cache`, and `L3 cache` lines in the output reflect your CPU's actual setup. Let us break this down layer by layer. @@ -301,14 +297,23 @@ typedef struct { Let us compare the differences in memory layout between the two: -```text -AoS 布局:每个元素的 x,y,z,r,g,b 紧挨在一起 -|x0 y0 z0 r0 g0 b0| x1 y1 z1 r1 g1 b1| x2 y2 z2 r2 g2 b2| ... -└─── 24 字节 ───┘ +```mermaid +graph LR + subgraph "AoS layout: each element's x,y,z,r,g,b are contiguous (24 bytes)" + A0["x0 y0 z0 r0 g0 b0"] + A1["x1 y1 z1 r1 g1 b1"] + A2["x2 y2 z2 r2 g2 b2"] + A0 --> A1 --> A2 + end +``` -SoA 布局:所有 x 连续,所有 y 连续,以此类推 -|x0|x1|x2|x3|x4|...|y0|y1|y2|y3|y4|...|z0|z1|z2|... -└── 连续的 x ──┘ └── 连续的 y ──┘ └── 连续的 z ──┘ +```mermaid +graph LR + subgraph "SoA layout: all x are contiguous, all y are contiguous, etc." + SX["Contiguous x
x0 x1 x2 x3 x4 ..."] + SY["Contiguous y
y0 y1 y2 y3 y4 ..."] + SZ["Contiguous z
z0 z1 z2 z3 z4 ..."] + end ``` If your hot path only processes the coordinates `x`, `y`, and `z`, without touching the colors `r`, `g`, and `b`, the advantage of SoA becomes very obvious. Traversing `x[0]`, `x[1]`, `x[2]`, and so on means the data is completely contiguous in memory, and the Cache hit rate approaches 100%. With AoS, accessing each `x` incidentally pulls the `y`, `z`, `r`, `g`, and `b` from the same struct into the Cache (because they are on the same cache line), but we do not need the color data yet, so that space is wasted. diff --git a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md index 310357793..9dc2b638e 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md +++ b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md @@ -374,15 +374,15 @@ Circle("Moon", r=2.00) Through the unified `shape_area()` and `shape_draw()` interfaces, each call routes to the correct concrete implementation—this is runtime polymorphism, and it is **exactly the same** as the underlying mechanism of C++ virtual functions. The memory layout comparison looks like this: -```text -vtable 方案:每个对象只有 1 个 vptr(8 字节) -┌──────────────────┐ -│ vtable ─────────────→ kCircleVtable(全局共享) -│ name │ ┌──────────────────┐ -│ radius │ │ circle_area │ -└──────────────────┘ │ circle_perimeter │ - │ circle_draw │ - └──────────────────┘ +```mermaid +graph LR + subgraph "Circle object (each object has only 1 vptr)" + Obj["vtable
name
radius"] + end + subgraph "kCircleVtable (globally shared)" + VT["circle_area
circle_perimeter
circle_draw"] + end + Obj -->|vptr| VT ``` ## Step 5 — Interfaces via Function Pointer Tables diff --git a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md index 6cf7b0209..81f7e3d8b 100644 --- a/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md +++ b/documents/en/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md @@ -179,24 +179,32 @@ bool linked_list_push_front(LinkedList* list, int data) { Let's draw this process. Suppose the list was originally `1 -> 2 -> 3`, and now we want to insert `0` at the head: -```text -插入前: -head - │ - ▼ -[10] -> [20] -> [30] -> NULL - -Step 1: 创建新节点 node(5) -[node(5) | next=NULL] - -Step 2: node->next = list->head -[node(5) | next] ──→ [10] -> [20] -> [30] -> NULL - -Step 3: list->head = node -head - │ - ▼ -[node(5)] -> [10] -> [20] -> [30] -> NULL +```mermaid +graph LR + subgraph "Before insertion" + H0["head"] --> N10a["10"] --> N20a["20"] --> N30a["30"] --> NULL0["NULL"] + end +``` + +```mermaid +graph LR + subgraph "Step 1: Create new node node(5)" + S1["node(5)
next=NULL"] + end +``` + +```mermaid +graph LR + subgraph "Step 2: node->next = list->head" + S2["node(5)"] --> N10b["10"] --> N20b["20"] --> N30b["30"] --> NULL1["NULL"] + end +``` + +```mermaid +graph LR + subgraph "Step 3: list->head = node" + H3["head"] --> S3["node(5)"] --> N10c["10"] --> N20c["20"] --> N30c["30"] --> NULL2["NULL"] + end ``` The whole process only changes two pointers, with no traversal, so it is O(1). Note that the order of these two steps cannot be reversed — if you update `head` first, the address of the original first node is lost, and the list is instantly broken. This order is an iron rule for head operations on linked lists: **connect first, disconnect second** — hook the new node onto the chain first, then modify the `head` pointer. diff --git a/documents/en/vol1-fundamentals/ch08/02-virtual-functions.md b/documents/en/vol1-fundamentals/ch08/02-virtual-functions.md index 4d54d43fc..49e27198f 100644 --- a/documents/en/vol1-fundamentals/ch08/02-virtual-functions.md +++ b/documents/en/vol1-fundamentals/ch08/02-virtual-functions.md @@ -165,19 +165,21 @@ After understanding the effect of `virtual`, let's look at what the compiler doe Taking our shape class hierarchy as an example, the compiler roughly generates three vtables: -```text -Shape 的 vtable: [ &Shape::draw ] -Circle 的 vtable: [ &Circle::draw ] -Rectangle 的 vtable: [ &Rectangle::draw ] +```mermaid +graph LR + ShapeVT["Shape vtable
&Shape::draw"] + CircleVT["Circle vtable
&Circle::draw"] + RectVT["Rectangle vtable
&Rectangle::draw"] ``` And every object that contains virtual functions has an extra hidden member in its memory layout—the **virtual table pointer** (vptr), which points to the vtable of the object's class. When you write `shapes[i]->draw()`, the code generated by the compiler roughly does the following: first finds the `vptr` through the object, locates the corresponding vtable, then retrieves the function pointer for `draw()` from the table, and finally makes an indirect call through this pointer: -```text -shapes[1] (Shape*) -----> Circle 对象 - [ vptr ] -------> Circle 的 vtable: [ &Circle::draw ] +```mermaid +graph LR + A["shapes[1]
(Shape*)"] --> B["Circle object
[ vptr ]"] + B --> C["Circle vtable
[ &Circle::draw ]"] ``` This is the entire overhead that a virtual function call adds compared to a normal function call—**one extra indirect jump**. On a PC, this overhead is almost negligible. But in resource-constrained embedded environments, we need to take it seriously: every class with virtual functions adds one vtable (occupying Flash), every object adds one `vptr` (usually 4 or 8 bytes, occupying RAM), and every virtual function call adds one indirect jump (which may affect the pipeline and branch prediction). Fortunately, in the vast majority of scenarios, these overheads are trivial compared to the "architectural benefits gained from decoupling." diff --git a/documents/en/vol1-fundamentals/ch12/01-memory-layout.md b/documents/en/vol1-fundamentals/ch12/01-memory-layout.md index 2744da53e..54e7e672e 100644 --- a/documents/en/vol1-fundamentals/ch12/01-memory-layout.md +++ b/documents/en/vol1-fundamentals/ch12/01-memory-layout.md @@ -40,25 +40,27 @@ Understanding memory layout essentially comes down to two things: **where data l When a C++ program runs, the operating system allocates a block of virtual address space for it. This space is not a single homogeneous region; rather, it is divided into several segments, each with its own purpose and management method. For our purposes, the four most important regions are: -```text -高地址 -┌─────────────────────────┐ -│ 栈 (Stack) │ ← 局部变量、函数调用帧 -│ ↓↓↓↓↓ │ 向低地址增长 -├─────────────────────────┤ -│ │ ← 未使用空间 -│ │ -├─────────────────────────┤ -│ 堆 (Heap) │ ← new/malloc 动态分配 -│ ↑↑↑↑↑ │ 向高地址增长 -├─────────────────────────┤ -│ BSS 段(未初始化数据) │ ← 未初始化的全局/static 变量 -├─────────────────────────┤ -│ 数据段(已初始化数据) │ ← 已初始化的全局/static 变量 -├─────────────────────────┤ -│ 代码段 (Text) │ ← 机器指令、只读常量 -└─────────────────────────┘ -低地址 +```mermaid +graph TD + subgraph "High Address" + Stack["Stack
Local variables, function call frames
↓ grows toward low addresses"] + end + subgraph " " + Free["Free Space"] + end + subgraph " " + Heap["Heap
new/malloc dynamic allocation
↑ grows toward high addresses"] + end + subgraph " " + BSS["BSS Segment (uninitialized data)
Uninitialized global/static variables"] + end + subgraph " " + Data["Data Segment (initialized data)
Initialized global/static variables"] + end + subgraph "Low Address" + Text["Text Segment
Machine instructions, read-only constants"] + end + Stack ~~~ Free ~~~ Heap ~~~ BSS ~~~ Data ~~~ Text ``` The text segment stores compiled machine instructions and some read-only data (such as string literals `"hello"`). This region is typically read-only, and attempting to modify it will directly trigger a segmentation fault. The data segment stores initialized global and `static` variables, whose values are determined at program startup. The BSS segment is part of the data segment, specifically holding uninitialized global and `static` variables—these are automatically initialized to zero, so the executable file doesn't need to store their initial values, only their size. The heap and stack are regions used dynamically at runtime; the former is managed manually by the programmer, while the latter is managed automatically by the compiler. diff --git a/documents/en/vol4-advanced/05-spaceship-operator.md b/documents/en/vol4-advanced/05-spaceship-operator.md index 2f0484a51..fb8290220 100644 --- a/documents/en/vol4-advanced/05-spaceship-operator.md +++ b/documents/en/vol4-advanced/05-spaceship-operator.md @@ -417,20 +417,20 @@ struct ConfigKey { ### Comparison Category Relationship Diagram -```text -strong_ordering (最强) - ├─ 替换性:a == b 意味着 a 可以完全替代 b - ├─ 等价即相等 - └─ 例子:整数、枚举 - -weak_ordering - ├─ 替换性:a == b 不一定能完全替代 b - ├─ 等价但不相等 - └─ 例子:大小写不敏感字符串 - -partial_ordering (最弱) - ├─ 某些值不可比较 - └─ 例子:浮点数(NaN) +```mermaid +graph TD + subgraph strong["strong_ordering (strongest)"] + strong_props["Substitutability: a == b means a can fully substitute b
Equivalent means equal
Examples: integers, enums"] + end + subgraph weak["weak_ordering"] + weak_props["Substitutability: a == b does not necessarily mean a can substitute b
Equivalent but not equal
Examples: case-insensitive strings"] + end + subgraph partial["partial_ordering (weakest)"] + partial_props["Some values are not comparable
Examples: floating-point (NaN)"] + end + + strong -.->|weakens| weak + weak -.->|weakens| partial ``` ------ diff --git a/documents/en/vol5-concurrency/ch05-future-task-threadpool/02-promise-and-packaged-task.md b/documents/en/vol5-concurrency/ch05-future-task-threadpool/02-promise-and-packaged-task.md index d0affe508..93b0aa339 100644 --- a/documents/en/vol5-concurrency/ch05-future-task-threadpool/02-promise-and-packaged-task.md +++ b/documents/en/vol5-concurrency/ch05-future-task-threadpool/02-promise-and-packaged-task.md @@ -439,21 +439,20 @@ The entire flow is as follows: the caller submits a task (a callable object + ar Let's use a pseudocode diagram to illustrate this flow: -```text -调用者线程 任务队列 工作线程 - | | | - |-- submit(func, args) ---->| | - | (创建 packaged_task) | | - | (获取 future) | | - | (打包入队列) | | - |<-- 返回 future | | - | |--- 取出 task --------->| - | | |-- task() - | | | (调用 func) - | | | (promise.set_value) - | | | - |-- future.get() <--------- | 共享状态就绪 - | (拿到结果或异常) +```mermaid +sequenceDiagram + participant Caller as Caller Thread + participant Queue as Task Queue + participant Worker as Worker Thread + + Caller->>Queue: submit(func, args)
(create packaged_task) + Note right of Caller: Get future + Queue-->>Caller: Return future + Queue->>Worker: Dequeue task + Worker->>Worker: task() — call func + Note right of Worker: promise.set_value + Note over Caller,Worker: Shared state ready + Caller->>Caller: future.get()
(get result or exception) ``` The core advantage of this pattern lies in **decoupling**: the caller does not need to know which thread the task executes on or when it executes; the worker thread does not need to know the source of the task or where the return value goes. The two communicate through a shared state (jointly held by the promise inside the packaged_task and the future returned to the caller), and all synchronization details are encapsulated within the implementation of `std::promise`/`std::future`. diff --git a/documents/en/vol5-concurrency/ch06-async-io-coroutine/05-coroutine-echo-server.md b/documents/en/vol5-concurrency/ch06-async-io-coroutine/05-coroutine-echo-server.md index 4db4dc229..c133f86a4 100644 --- a/documents/en/vol5-concurrency/ch06-async-io-coroutine/05-coroutine-echo-server.md +++ b/documents/en/vol5-concurrency/ch06-async-io-coroutine/05-coroutine-echo-server.md @@ -58,18 +58,20 @@ The **handle_connection coroutine** is an independent coroutine corresponding to The data flow looks roughly like this: -```text -客户端连接 → epoll 通知 listen_fd 可读 -→ accept_loop 协程恢复,accept 拿到 client_fd -→ 启动 handle_connection(client_fd) 协程 -→ handle_connection 执行 co_await async_read(client_fd) -→ async_read 发现没数据,把 client_fd 注册到 epoll,协程挂起 -→ 客户端发送数据 → epoll 通知 client_fd 可读 -→ EventLoop 恢复 handle_connection 协程 -→ async_read 读取数据,返回字节数 -→ handle_connection 执行 co_await async_write(client_fd) -→ 数据写回客户端 -→ 回到循环开头,继续等下一次读 +```mermaid +flowchart TD + A["Client connects"] --> B["epoll notifies listen_fd readable"] + B --> C["accept_loop coroutine resumes
accept gets client_fd"] + C --> D["Start handle_connection(client_fd) coroutine"] + D --> E["handle_connection executes
co_await async_read(client_fd)"] + E --> F["async_read finds no data
registers client_fd with epoll, coroutine suspends"] + F --> G["Client sends data"] + G --> H["epoll notifies client_fd readable"] + H --> I["EventLoop resumes handle_connection coroutine"] + I --> J["async_read reads data, returns byte count"] + J --> K["handle_connection executes
co_await async_write(client_fd)"] + K --> L["Data written back to client"] + L --> E ``` The entire process completes within a single thread, but handles multiple clients concurrently—because each client has its own coroutine, and coroutines yield execution when waiting for I/O without blocking anyone. diff --git a/documents/en/vol5-concurrency/ch09-distributed-bridge/02-distributed-primitives.md b/documents/en/vol5-concurrency/ch09-distributed-bridge/02-distributed-primitives.md index a98bf7094..c86f1cff5 100644 --- a/documents/en/vol5-concurrency/ch09-distributed-bridge/02-distributed-primitives.md +++ b/documents/en/vol5-concurrency/ch09-distributed-bridge/02-distributed-primitives.md @@ -84,14 +84,11 @@ The advantage of eventual consistency lies in performance and availability: beca Great, now let us look at all four models together. They form a hierarchy from strong to weak: -```text -线性一致性 (Linearizability) - ↓ 满足线性一致 → 必然满足以下所有 -顺序一致性 (Sequential Consistency) - ↓ 满足顺序一致 → 必然满足以下所有 -因果一致性 (Causal Consistency) - ↓ 满足因果一致 → 必然满足以下所有 -最终一致性 (Eventual Consistency) +```mermaid +flowchart TD + A["Linearizability"] -->|"Satisfies linearizability → implies all below"| B["Sequential Consistency"] + B -->|"Satisfies sequential consistency → implies all below"| C["Causal Consistency"] + C -->|"Satisfies causal consistency → implies all below"| D["Eventual Consistency"] ``` The hierarchical relationship means: a system that satisfies linearizability also satisfies sequential consistency, causal consistency, and eventual consistency. Conversely, a system that satisfies eventual consistency does not necessarily satisfy causal consistency. For each step up the hierarchy, you gain a stronger consistency guarantee, but you also pay a higher price in latency and availability. diff --git a/documents/en/vol7-engineering/msvc-debugging-internals.md b/documents/en/vol7-engineering/msvc-debugging-internals.md index 591c201bc..9823ad0e0 100644 --- a/documents/en/vol7-engineering/msvc-debugging-internals.md +++ b/documents/en/vol7-engineering/msvc-debugging-internals.md @@ -80,25 +80,14 @@ When the debugging task ends, the debugger provides two graceful exit methods. T To help blog readers understand, you can envision an architecture diagram like this: -```text - -[ 开发者 (User) ] - | 交互 (F5, F10, 查看变量) - v -[ Visual Studio IDE (UI层) ] - | 发送指令 - v -[ 调试引擎 (Debug Engine) ] <----读取----> [ PDB 符号文件 ] - | 跨进程通讯 (RPC) - v -[ msvsmon.exe (调试监视器) ] - | 调用 Win32 Debug API - v -[ Windows Kernel (操作系统内核) ] - | 控制 / 捕获异常 - v -[ 目标进程 (App.exe) ] - +```mermaid +flowchart TD + A["Developer (User)"] -->|"Interact (F5, F10, view variables)"| B["Visual Studio IDE (UI Layer)"] + B -->|"Send commands"| C["Debug Engine"] + C <-->|"Read"| D["PDB Symbol File"] + C -->|"Cross-process communication (RPC)"| E["msvsmon.exe (Debug Monitor)"] + E -->|"Call Win32 Debug API"| F["Windows Kernel (OS Kernel)"] + F -->|"Control / Capture exceptions"| G["Target Process (App.exe)"] ``` --- diff --git a/documents/en/vol8-domains/embedded/01-led/04-hal-gpio-clock.md b/documents/en/vol8-domains/embedded/01-led/04-hal-gpio-clock.md index 07dcd4289..7ba23d5a4 100644 --- a/documents/en/vol8-domains/embedded/01-led/04-hal-gpio-clock.md +++ b/documents/en/vol8-domains/embedded/01-led/04-hal-gpio-clock.md @@ -43,35 +43,29 @@ To understand clock enabling, simply knowing to "flip a switch" is not enough. W Below is a simplified clock tree under our project's configuration. Note that this is the **configuration we actually use**, not the complete clock tree in the STM32 reference manual that gives you a headache at first glance. We will only look at the parts relevant to us: -```text - ┌──────────────┐ - │ HSI 8MHz │ - │ (内部RC振荡器) │ - └──────┬───────┘ - │ - /2 分频 - │ - 4MHz ──→ PLL ×16 ──→ 64MHz - │ - SYSCLK - 64MHz - │ - ┌─────────────────────────┤ - │ │ - AHB /1 AHB /1 - HCLK = 64MHz HCLK = 64MHz - │ │ - ┌──────────┤ ┌──────┤ - │ │ │ │ - APB1 /2 APB2 /1 DMA Flash - 32MHz 64MHz 控制器 接口 - │ │ - ┌────┤ ┌────┴────┐ - │ │ │ │ - TIM2-4 USART1 GPIOA-E - USART2-3 SPI1 ADC1-2 - I2C1-2 TIM1 - SPI2-3 ... +```mermaid +graph TD + HSI["HSI 8MHz
(Internal RC Oscillator)"] + DIV["/2 Divider
4MHz"] + PLL["PLL x16
64MHz"] + SYSCLK["SYSCLK
64MHz"] + AHB1["AHB /1
HCLK = 64MHz"] + APB1["APB1 /2
32MHz"] + APB2["APB2 /1
64MHz"] + DMA["DMA Controller"] + Flash["Flash Interface"] + APB1_PERIPH["TIM2-4, USART2-3
I2C1-2, SPI2-3"] + APB2_PERIPH["USART1, SPI1
TIM1, ..."] + GPIO["GPIOA-E"] + ADC["ADC1-2"] + + HSI --> DIV --> PLL --> SYSCLK --> AHB1 + AHB1 --> APB1 --> APB1_PERIPH + AHB1 --> APB2 --> APB2_PERIPH + APB2 --> GPIO + APB2 --> ADC + AHB1 --> DMA + AHB1 --> Flash ``` Let us examine this tree layer by layer. diff --git a/documents/en/vol8-domains/embedded/02-button/07-debounce-state-machine.md b/documents/en/vol8-domains/embedded/02-button/07-debounce-state-machine.md index a5745c038..a4507cb8c 100644 --- a/documents/en/vol8-domains/embedded/02-button/07-debounce-state-machine.md +++ b/documents/en/vol8-domains/embedded/02-button/07-debounce-state-machine.md @@ -59,29 +59,37 @@ Don't let the 7 states intimidate you. The core flow only has 4 states: `Idle ### State Transition Diagram -```text - ┌──────────────────────────────────────────────────┐ - │ │ - ▼ │ -┌──────────┐ 按下 ┌──────────────┐ 稳定 ┌─────────┐ 释放 ┌────────────────┐ -│ Idle │───────→│DebouncingPress│───────→│ Pressed │───────→│DebouncingRelease│ -│ (松开中) │←───────│ (消抖中) │ │(按住中) │←───────│ (消抖中) │ -└──────────┘ 反弹 └──────────────┘ └─────────┘ 反弹 └────────────────┘ - ↑ │ - │ 确认释放 │ 稳定 - └───────────────────────────────────────────────────────┘ - -启动路径(上电时按钮已按住): -┌──────────┐ ┌──────────────┐ ┌───────────────────────┐ -│ BootSync │──按下──→│ BootPressed │──释放──→│ BootReleaseDebouncing │ -│ (初始同步)│ │ (启动锁定中) │ │ (启动释放消抖) │ -└──────────┘ └──────────────┘ └───────────────────────┘ - │ 稳定 - ▼ - ┌──────────┐ - │ Idle │ - │ (解锁,无事件)│ - └──────────┘ +```mermaid +stateDiagram-v2 + state "Core Path" as Core { + direction LR + Idle: Idle (Released) + DebouncingPress: DebouncingPress (Debouncing) + Pressed: Pressed (Held) + DebouncingRelease: DebouncingRelease (Debouncing) + + [*] --> Idle + Idle --> DebouncingPress : Press detected + DebouncingPress --> Idle : Signal bounce + DebouncingPress --> Pressed : Stable confirmed + Pressed --> DebouncingRelease : Release detected + DebouncingRelease --> Pressed : Signal bounce + DebouncingRelease --> Idle : Release confirmed\n(Trigger Released event) + } + + state "Boot Path (button held at power-on)" as Boot { + direction LR + BootSync: BootSync (Initial sync) + BootPressed: BootPressed (Boot locked) + BootReleaseDebouncing: BootReleaseDebouncing (Boot release debounce) + + BootSync --> BootPressed : Press detected\n(set boot_locked) + BootSync --> Idle : Release detected + BootPressed --> BootReleaseDebouncing : Release detected + BootReleaseDebouncing --> Idle : Stable confirmed\n(Unlock, no event) + } + + [*] --> BootSync ``` --- diff --git a/documents/en/vol8-domains/embedded/02-button/09-cpp-variant-and-visit.md b/documents/en/vol8-domains/embedded/02-button/09-cpp-variant-and-visit.md index 9456ea7e9..ba8a9c604 100644 --- a/documents/en/vol8-domains/embedded/02-button/09-cpp-variant-and-visit.md +++ b/documents/en/vol8-domains/embedded/02-button/09-cpp-variant-and-visit.md @@ -209,11 +209,13 @@ In our `-fno-exceptions -fno-rtti` compilation environment, `std::variant` is a The memory layout of `std::variant`: -```text -┌──────────┬──────────┐ -│ tag (1B) │ payload │ -│ 0 或 1 │ (空) │ -└──────────┴──────────┘ +```mermaid +graph LR + subgraph "std::variant<Pressed, Released> Memory Layout" + TAG["tag (1B)
0 = Pressed
1 = Released"] + PAYLOAD["payload
(empty struct, no extra space)"] + end + TAG --- PAYLOAD ``` Since both `Pressed` and `Released` are empty structs (`sizeof = 1`), `variant` only needs a single tag byte to identify which type it currently holds. With alignment, `sizeof(ButtonEvent)` is typically 2 bytes. diff --git a/documents/en/vol8-domains/embedded/03-uart/12-command-processor-and-main-walkthrough.md b/documents/en/vol8-domains/embedded/03-uart/12-command-processor-and-main-walkthrough.md index 36ab24ddd..09edc54d5 100644 --- a/documents/en/vol8-domains/embedded/03-uart/12-command-processor-and-main-walkthrough.md +++ b/documents/en/vol8-domains/embedded/03-uart/12-command-processor-and-main-walkthrough.md @@ -131,24 +131,19 @@ int main() { The first half of `main()` is initialization, executed in a strict order: -```text -HAL_Init() ← HAL 库初始化(SysTick 等) - ↓ -ClockConfig::instance().setup... ← 系统时钟配置(64 MHz HSI) - ↓ -LED led ← LED 对象构造(零开销) - ↓ -Button button ← Button 对象构造(零开销) - ↓ -Logger::driver().set_gpio_init(...) ← 注册 GPIO 初始化回调 - ↓ -Logger::driver().init(UartConfig) ← 使能时钟 → GPIO → HAL init - ↓ -Logger::driver().enable_interrupt() ← NVIC 使能 USART1 中断 - ↓ -send_string("UART Logger Ready!") ← 阻塞式发送欢迎信息 - ↓ -uart_start_receive() ← 启动中断接收流水线 +```mermaid +graph TD + A["HAL_Init()
HAL library init (SysTick, etc.)"] + B["ClockConfig::instance().setup...
System clock config (64 MHz HSI)"] + C["LED<Port::C, PIN_13> led
LED object construction (zero overhead)"] + D["Button<Port::A, PIN_0> button
Button object construction (zero overhead)"] + E["Logger::driver().set_gpio_init(...)
Register GPIO init callback"] + F["Logger::driver().init(UartConfig)
Enable clock → GPIO → HAL init"] + G["Logger::driver().enable_interrupt()
NVIC enable USART1 interrupt"] + H["send_string(\"UART Logger Ready!\")
Blocking send welcome message"] + I["uart_start_receive()
Start interrupt receive pipeline"] + + A --> B --> C --> D --> E --> F --> G --> H --> I ``` The order of every step cannot be swapped. Calling HAL functions before configuring the clock will cause a hard fault. If GPIO is not configured, USART signals will not reach the pins. If interrupts are not enabled before starting reception, arriving bytes will not trigger the ISR. Placing `send_string` before `uart_start_receive` is intentional—we first send a welcome message to confirm the transmit path is working, then start receiving. @@ -247,20 +242,32 @@ In a bare-metal environment, dynamic memory allocation (`new`/`malloc`) can lead Drawing all the data flows together, the architecture of the entire system looks like this: -```text -┌─────────┐ TX (PA9) ┌────────────┐ USB ┌─────┐ -│ │─────────────→│ USB-TTL │───────→│ PC │ -│ STM32 │ │ 适配器 │ │终端 │ -│ │←─────────────│ │←───────│ │ -└─────────┘ RX (PA10) └────────────┘ USB └─────┘ - │ - │ 按钮事件 → send_string("Button pressed!") - │ 命令响应 → send_string("OK: LED ON") - │ - │ 中断接收 → rx_ring → 行解析 → handle_command → led.on() - │ - ├── PC13 (LED) - └── PA0 (Button) +```mermaid +graph LR + subgraph STM32["STM32"] + TX["TX (PA9)"] + RX["RX (PA10)"] + LED["PC13 (LED)"] + BTN["PA0 (Button)"] + end + + subgraph ADAPTER["USB-TTL Adapter"] + TTL_TX["TTL TX"] + TTL_RX["TTL RX"] + end + + subgraph PC["PC Terminal"] + PC_TX["USB TX"] + PC_RX["USB RX"] + end + + TX -->|"Button events / Command responses"| TTL_RX + TTL_RX -->|"USB"| PC_RX + PC_TX -->|"USB"| TTL_TX + TTL_TX -->|"Command input"| RX + + BTN -.->|"poll_events()"| TX + RX -.->|"rx_ring → Line parsing
→ handle_command"| LED ``` Chip → PC direction: Button events and command responses are sent out via `send_string()`. These calls use blocking transmission (`HAL_UART_Transmit`) because the send volume is small (a few dozen bytes), the blocking time is controllable (less than one millisecond), and there is no impact on system responsiveness. diff --git a/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/full/01-5-once-callback-then-chaining.md b/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/full/01-5-once-callback-then-chaining.md index 703ebd74f..c923e5447 100644 --- a/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/full/01-5-once-callback-then-chaining.md +++ b/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/full/01-5-once-callback-then-chaining.md @@ -72,8 +72,9 @@ The newly chained callback needs to hold **ownership** of both the original and The entire ownership chain looks like this: -```text -新 OnceCallback → move_only_function → lambda 闭包 → [原回调 + 后续回调] +```mermaid +graph LR + A["New OnceCallback"] --> B["move_only_function"] --> C["Lambda closure"] --> D["Original callback + Next callback"] ``` Each layer transfers ownership via move semantics, without any sharing or copying. This is the complete embodiment of move-only semantics in `then()`. diff --git a/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-03-once-callback-lambda-advanced.md b/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-03-once-callback-lambda-advanced.md index 54feb78a6..ffaea70ba 100644 --- a/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-03-once-callback-lambda-advanced.md +++ b/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-03-once-callback-lambda-advanced.md @@ -134,8 +134,9 @@ cont = std::forward(next) Looking at these two captures together, the new lambda created by `then()` holds **complete ownership** of both the original callback and the subsequent callback. This lambda is then stored in the `std::move_only_function` of a new `OnceCallback`. The entire ownership chain looks like this: -```text -新 OnceCallback -> move_only_function -> lambda 闭包 -> [原 OnceCallback + 后续回调] +```mermaid +graph LR + A["New OnceCallback"] --> B["move_only_function"] --> C["Lambda closure"] --> D["Original OnceCallback + Next callback"] ``` Each layer passes ownership via move semantics, without any sharing or copying. This is the complete embodiment of OnceCallback's move-only semantics in `then()`—ownership transfers layer by layer from outside to inside, leaving no gaps. diff --git a/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-05-once-callback-move-only-function.md b/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-05-once-callback-move-only-function.md index 734b0032e..99a536428 100644 --- a/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-05-once-callback-move-only-function.md +++ b/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-05-once-callback-move-only-function.md @@ -162,19 +162,18 @@ f = nullptr; // 清空 f,析构之前持有的可调用对象 `std::move_only_function` (like `std::function`) internally implements **Small Buffer Optimization** (SBO). The idea is simple: the object reserves a fixed-size internal buffer (typically a few pointers in size). If the callable is small enough, it is stored directly in the buffer, avoiding heap allocation; if it is too large, memory is allocated on the heap to store it. -```text -┌──────────────────────────────────┐ -│ std::move_only_function │ -│ ┌──────────────────────────────┐ │ -│ │ 函数指针/虚表指针 │ │ ← 用于类型擦除的调用分派 -│ ├──────────────────────────────┤ │ -│ │ SBO 缓冲区(通常 16-32 字节)│ │ ← 小对象直接存这里 -│ └──────────────────────────────┘ │ -│ 或 │ -│ ┌──────────────────────────────┐ │ -│ │ 堆指针(指向动态分配的对象) │ │ ← 大对象存在堆上 -│ └──────────────────────────────┘ │ -└──────────────────────────────────┘ +```mermaid +graph TD + subgraph outer["std::move_only_function"] + direction TB + subgraph sbo_path["SBO path: small objects stored inline"] + vtable["Function pointer / vtable pointer
For type-erased call dispatch"] + sbo_buf["SBO buffer (typically 16-32 bytes)
Small objects stored directly here"] + end + subgraph heap_path["Heap path: large objects dynamically allocated"] + heap_ptr["Heap pointer (points to dynamically allocated object)
Large objects stored on the heap"] + end + end ``` The SBO threshold is implementation-defined—typically around two to three pointers in size (16–24 bytes). A lambda capturing a small number of parameters (such as `[x = 42]` or `[&ref]`) usually fits within the SBO and does not trigger heap allocation. However, if a lambda captures a large amount of data (such as a `std::string` plus a few `int`s), exceeding the SBO threshold, construction will allocate on the heap. diff --git a/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/02-once-callback-implementation.md b/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/02-once-callback-implementation.md index 2547b1454..f442940b3 100644 --- a/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/02-once-callback-implementation.md +++ b/documents/en/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/02-once-callback-implementation.md @@ -330,8 +330,9 @@ It sounds simple, but `then()` is the most ingeniously designed of the four feat The new chained callback needs to hold the **ownership** of both the original callback and the subsequent callback—otherwise, the original callback might be consumed prematurely on the outside, breaking the pipeline. Since `OnceCallback` is move-only, this means `then()` must consume `*this` (the original callback) and `next` (the subsequent callback), transferring both of their ownerships into a new lambda closure. The entire ownership chain looks like this: -```text -新回调 → move_only_function → lambda 闭包 → [原回调 + 后续回调] +```mermaid +graph LR + A["New callback"] --> B["move_only_function"] --> C["Lambda closure"] --> D["Original callback + Next callback"] ``` The skeleton of the implementation approach looks roughly like this: diff --git a/documents/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md b/documents/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md index f78a714a8..08f233d63 100644 --- a/documents/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md +++ b/documents/vol1-fundamentals/c_tutorials/advanced_feature/02-cache-and-memory-hierarchy.md @@ -59,21 +59,17 @@ Python 和 Java 这类语言把内存管理彻底抽象掉了,程序员基本 这个金字塔结构的核心设计思想叫做**局部性原理**(Principle of Locality)。局部性分两种:**时间局部性**指的是如果一个数据刚被访问过,那它很可能会在不久之后再次被访问;**空间局部性**指的是如果一个数据被访问了,那它附近地址的数据很可能也会被访问。Cache 的所有设计决策——缓存行的大小、预取策略、替换策略——全都是围绕这两个局部性来的。我们可以用一张简图来直观感受这个金字塔: -```text - ┌─────────────┐ - │ 寄存器 │ ~1 周期 | 容量: ~数百字节 - ├─────────────┤ - │ L1 Cache │ ~3-4 周期 | 容量: 32-64 KB - ├─────────────┤ - │ L2 Cache │ ~10-14 周期| 容量: 256 KB-1 MB - ├─────────────┤ - │ L3 Cache │ ~30-50 周期| 容量: 数 MB-数十 MB - ├─────────────┤ - │ 主存 DRAM │ ~100-300 周期 | 容量: GB 级 - ├─────────────┤ - │ SSD/HDD │ ~微秒/毫秒 | 容量: TB 级 - └─────────────┘ - 越往上越快、越小、越贵;越往下越慢、越大、越便宜 +```mermaid +graph TD + subgraph "越往上越快、越小、越贵;越往下越慢、越大、越便宜" + Reg["寄存器
~1 周期 | 容量: ~数百字节"] + L1["L1 Cache
~3-4 周期 | 容量: 32-64 KB"] + L2["L2 Cache
~10-14 周期 | 容量: 256 KB-1 MB"] + L3["L3 Cache
~30-50 周期 | 容量: 数 MB-数十 MB"] + DRAM["主存 DRAM
~100-300 周期 | 容量: GB 级"] + Disk["SSD/HDD
~微秒/毫秒 | 容量: TB 级"] + end + Reg ~~~ L1 ~~~ L2 ~~~ L3 ~~~ DRAM ~~~ Disk ``` 你可以在 Linux 上用 `lscpu` 命令查看自己机器的 Cache 配置,输出的 `L1d cache`、`L2 cache`、`L3 cache` 那几行就是你的 CPU 实际情况。接下来我们一层一层往下拆。 @@ -298,14 +294,23 @@ typedef struct { 对比一下两者在内存中的布局差异: -```text -AoS 布局:每个元素的 x,y,z,r,g,b 紧挨在一起 -|x0 y0 z0 r0 g0 b0| x1 y1 z1 r1 g1 b1| x2 y2 z2 r2 g2 b2| ... -└─── 24 字节 ───┘ +```mermaid +graph LR + subgraph "AoS 布局:每个元素的 x,y,z,r,g,b 紧挨在一起(24 字节)" + A0["x0 y0 z0 r0 g0 b0"] + A1["x1 y1 z1 r1 g1 b1"] + A2["x2 y2 z2 r2 g2 b2"] + A0 --> A1 --> A2 + end +``` -SoA 布局:所有 x 连续,所有 y 连续,以此类推 -|x0|x1|x2|x3|x4|...|y0|y1|y2|y3|y4|...|z0|z1|z2|... -└── 连续的 x ──┘ └── 连续的 y ──┘ └── 连续的 z ──┘ +```mermaid +graph LR + subgraph "SoA 布局:所有 x 连续,所有 y 连续,以此类推" + SX["连续的 x
x0 x1 x2 x3 x4 ..."] + SY["连续的 y
y0 y1 y2 y3 y4 ..."] + SZ["连续的 z
z0 z1 z2 z3 z4 ..."] + end ``` 如果你的热路径只处理坐标 `x`、`y`、`z`,而不碰颜色 `r`、`g`、`b`,那 SoA 的优势就非常明显了——你连续遍历 `x[0]`、`x[1]`、`x[2]`……数据在内存里完全连续,Cache 命中率接近 100%。而 AoS 的情况下,每访问一个 `x` 都会顺带把同一结构体里的 `y`、`z`、`r`、`g`、`b` 也拉进 Cache(因为它们在同一条缓存行上),但我们暂时用不到颜色数据,这些空间就浪费了。 diff --git a/documents/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md b/documents/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md index b0ca9afc7..9e635b11f 100644 --- a/documents/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md +++ b/documents/vol1-fundamentals/c_tutorials/advanced_feature/04-oop-in-c.md @@ -372,15 +372,15 @@ Circle("Moon", r=2.00) 通过统一的 `shape_area()`、`shape_draw()` 接口调用,每次都走到了正确的具体实现——这就是运行时多态,和 C++ 虚函数的底层机制**完全一样**。内存布局对比如下: -```text -vtable 方案:每个对象只有 1 个 vptr(8 字节) -┌──────────────────┐ -│ vtable ─────────────→ kCircleVtable(全局共享) -│ name │ ┌──────────────────┐ -│ radius │ │ circle_area │ -└──────────────────┘ │ circle_perimeter │ - │ circle_draw │ - └──────────────────┘ +```mermaid +graph LR + subgraph "Circle 对象(每个对象只有 1 个 vptr)" + Obj["vtable
name
radius"] + end + subgraph "kCircleVtable(全局共享)" + VT["circle_area
circle_perimeter
circle_draw"] + end + Obj -->|vptr| VT ``` ## 第五步——用函数指针表实现接口 diff --git a/documents/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md b/documents/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md index b2378c325..22743a963 100644 --- a/documents/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md +++ b/documents/vol1-fundamentals/c_tutorials/advanced_feature/06-handmade-linked-list.md @@ -177,24 +177,32 @@ bool linked_list_push_front(LinkedList* list, int data) { 我们来画一下这个过程。假设链表原来是 `10 -> 20 -> 30`,现在要在头部插入 `5`: -```text -插入前: -head - │ - ▼ -[10] -> [20] -> [30] -> NULL - -Step 1: 创建新节点 node(5) -[node(5) | next=NULL] - -Step 2: node->next = list->head -[node(5) | next] ──→ [10] -> [20] -> [30] -> NULL - -Step 3: list->head = node -head - │ - ▼ -[node(5)] -> [10] -> [20] -> [30] -> NULL +```mermaid +graph LR + subgraph "插入前" + H0["head"] --> N10a["10"] --> N20a["20"] --> N30a["30"] --> NULL0["NULL"] + end +``` + +```mermaid +graph LR + subgraph "Step 1: 创建新节点 node(5)" + S1["node(5)
next=NULL"] + end +``` + +```mermaid +graph LR + subgraph "Step 2: node->next = list->head" + S2["node(5)"] --> N10b["10"] --> N20b["20"] --> N30b["30"] --> NULL1["NULL"] + end +``` + +```mermaid +graph LR + subgraph "Step 3: list->head = node" + H3["head"] --> S3["node(5)"] --> N10c["10"] --> N20c["20"] --> N30c["30"] --> NULL2["NULL"] + end ``` 整个过程只改了两个指针,没有遍历,所以是 O(1)。注意这两步的顺序不能反——如果先 `list->head = node`,那原来第一个节点的地址就丢了,链表直接断链。这个顺序是链表头部操作的铁律:**先连后断**——先把新节点接到链上,再改 `head` 指针。 diff --git a/documents/vol1-fundamentals/ch08/02-virtual-functions.md b/documents/vol1-fundamentals/ch08/02-virtual-functions.md index a79c234fd..edb478ef9 100644 --- a/documents/vol1-fundamentals/ch08/02-virtual-functions.md +++ b/documents/vol1-fundamentals/ch08/02-virtual-functions.md @@ -161,19 +161,21 @@ error: 'void Circle::draw()' marked 'override', but does not override any base c 拿我们的图形类层次来说,编译器大致生成了三张 vtable: -```text -Shape 的 vtable: [ &Shape::draw ] -Circle 的 vtable: [ &Circle::draw ] -Rectangle 的 vtable: [ &Rectangle::draw ] +```mermaid +graph LR + ShapeVT["Shape 的 vtable
&Shape::draw"] + CircleVT["Circle 的 vtable
&Circle::draw"] + RectVT["Rectangle 的 vtable
&Rectangle::draw"] ``` 而每个包含虚函数的对象,在内存布局中都会多出一个隐藏的成员——**虚表指针**(vptr),指向该对象所属类的 vtable。 当你写下 `shapes[i]->draw()` 时,编译器生成的代码大致做了这几步:先通过对象找到 `vptr`,定位到对应的 vtable,然后从表中取出 `draw()` 对应的函数指针,最后通过这个指针发起间接调用: -```text -shapes[1] (Shape*) -----> Circle 对象 - [ vptr ] -------> Circle 的 vtable: [ &Circle::draw ] +```mermaid +graph LR + A["shapes[1]
(Shape*)"] --> B["Circle 对象
[ vptr ]"] + B --> C["Circle 的 vtable
[ &Circle::draw ]"] ``` 这就是虚函数调用比普通函数调用多出来的全部开销——**一次额外的间接跳转**。在 PC 上,这个开销几乎可以忽略不计。但在资源紧张的嵌入式环境里需要认真对待:每个含虚函数的类多一张 vtable(占用 Flash),每个对象多一个 `vptr`(通常 4 或 8 字节,占用 RAM),每次虚函数调用多一次间接跳转(可能影响流水线和分支预测)。好在绝大多数场景下,这些开销和"解耦带来的架构收益"相比微不足道。 diff --git a/documents/vol1-fundamentals/ch12/01-memory-layout.md b/documents/vol1-fundamentals/ch12/01-memory-layout.md index 7a05e426a..0eb73cb95 100644 --- a/documents/vol1-fundamentals/ch12/01-memory-layout.md +++ b/documents/vol1-fundamentals/ch12/01-memory-layout.md @@ -36,25 +36,27 @@ cpp_standard: [11, 14, 17, 20] 一个 C++ 程序运行时,操作系统会为它分配一块虚拟地址空间。这块空间并不是一整块 homogeneous 的区域,而是被划分成了若干个段(segment),每个段有各自的用途和管理方式。对于我们来说,最核心的是以下四个区域: -```text -高地址 -┌─────────────────────────┐ -│ 栈 (Stack) │ ← 局部变量、函数调用帧 -│ ↓↓↓↓↓ │ 向低地址增长 -├─────────────────────────┤ -│ │ ← 未使用空间 -│ │ -├─────────────────────────┤ -│ 堆 (Heap) │ ← new/malloc 动态分配 -│ ↑↑↑↑↑ │ 向高地址增长 -├─────────────────────────┤ -│ BSS 段(未初始化数据) │ ← 未初始化的全局/static 变量 -├─────────────────────────┤ -│ 数据段(已初始化数据) │ ← 已初始化的全局/static 变量 -├─────────────────────────┤ -│ 代码段 (Text) │ ← 机器指令、只读常量 -└─────────────────────────┘ -低地址 +```mermaid +graph TD + subgraph "高地址" + Stack["栈 (Stack)
局部变量、函数调用帧
↓ 向低地址增长"] + end + subgraph " " + Free["未使用空间"] + end + subgraph " " + Heap["堆 (Heap)
new/malloc 动态分配
↑ 向高地址增长"] + end + subgraph " " + BSS["BSS 段(未初始化数据)
未初始化的全局/static 变量"] + end + subgraph " " + Data["数据段(已初始化数据)
已初始化的全局/static 变量"] + end + subgraph "低地址" + Text["代码段 (Text)
机器指令、只读常量"] + end + Stack ~~~ Free ~~~ Heap ~~~ BSS ~~~ Data ~~~ Text ``` 代码段(Text segment)存放编译后的机器指令和一些只读数据(比如字符串字面量 `"hello"`),这个区域通常是只读的,尝试修改会直接触发段错误。数据段(Data segment)存放已初始化的全局变量和 `static` 变量,它们的值在程序启动时就已经确定。BSS 段是数据段的一部分,专门放未初始化的全局和 `static` 变量——这些变量会被自动初始化为零,所以可执行文件里不需要存储它们的初始值,只记录大小就够了。堆和栈则是运行时动态使用的区域,前者由程序员手动管理,后者由编译器自动管理。 diff --git a/documents/vol2-modern-features/ch01-smart-pointers/03-shared-ptr.md b/documents/vol2-modern-features/ch01-smart-pointers/03-shared-ptr.md index 7cd2feb4f..d4b070b7d 100644 --- a/documents/vol2-modern-features/ch01-smart-pointers/03-shared-ptr.md +++ b/documents/vol2-modern-features/ch01-smart-pointers/03-shared-ptr.md @@ -86,17 +86,20 @@ Disconnected from 192.168.1.1:8080 我们用一个简化的示意图来理解: -```text -shared_ptr 对象 (栈上) -┌─────────────────────┐ -│ T* ptr ─────────────┼──→ T 对象 -│ ControlBlock* cb ───┼──→ ControlBlock (堆上) -└─────────────────────┘ ┌──────────────────────┐ - │ strong_count: 2 │ - │ weak_count: 0 │ - │ deleter (可选) │ - │ allocator (可选) │ - └──────────────────────┘ +```mermaid +graph LR + subgraph SP["shared_ptr 对象 (栈上)"] + ptr["T* ptr"] + cb["ControlBlock* cb"] + end + ptr --> T["T 对象 (堆上)"] + cb --> CB + subgraph CB["ControlBlock (堆上)"] + sc["strong_count: 2"] + wc["weak_count: 0"] + del["deleter (可选)"] + alloc["allocator (可选)"] + end ``` 所以一个 `shared_ptr` 对象本身的大小是 `2 * sizeof(void*)`——两个指针。在 64 位系统上是 16 字节,比 `unique_ptr`(8 字节)大一倍。控制块本身的大小取决于实现(GNU libstdc++ 在 x86_64 上约为 32 字节)。 diff --git a/documents/vol4-advanced/05-spaceship-operator.md b/documents/vol4-advanced/05-spaceship-operator.md index 24c3e7725..95ea1cbf3 100644 --- a/documents/vol4-advanced/05-spaceship-operator.md +++ b/documents/vol4-advanced/05-spaceship-operator.md @@ -416,20 +416,20 @@ struct ConfigKey { ### 比较类别关系图 -```text -strong_ordering (最强) - ├─ 替换性:a == b 意味着 a 可以完全替代 b - ├─ 等价即相等 - └─ 例子:整数、枚举 - -weak_ordering - ├─ 替换性:a == b 不一定能完全替代 b - ├─ 等价但不相等 - └─ 例子:大小写不敏感字符串 - -partial_ordering (最弱) - ├─ 某些值不可比较 - └─ 例子:浮点数(NaN) +```mermaid +graph TD + subgraph strong["strong_ordering(最强)"] + strong_props["替换性:a == b 意味着 a 可以完全替代 b
等价即相等
例子:整数、枚举"] + end + subgraph weak["weak_ordering"] + weak_props["替换性:a == b 不一定能完全替代 b
等价但不相等
例子:大小写不敏感字符串"] + end + subgraph partial["partial_ordering(最弱)"] + partial_props["某些值不可比较
例子:浮点数(NaN)"] + end + + strong -.->|弱化| weak + weak -.->|弱化| partial ``` ------ diff --git a/documents/vol5-concurrency/ch05-future-task-threadpool/02-promise-and-packaged-task.md b/documents/vol5-concurrency/ch05-future-task-threadpool/02-promise-and-packaged-task.md index 3b7622fb2..36a2746a8 100644 --- a/documents/vol5-concurrency/ch05-future-task-threadpool/02-promise-and-packaged-task.md +++ b/documents/vol5-concurrency/ch05-future-task-threadpool/02-promise-and-packaged-task.md @@ -429,21 +429,20 @@ int main() 我们用一张伪代码图来表示这个流程: -```text -调用者线程 任务队列 工作线程 - | | | - |-- submit(func, args) ---->| | - | (创建 packaged_task) | | - | (获取 future) | | - | (打包入队列) | | - |<-- 返回 future | | - | |--- 取出 task --------->| - | | |-- task() - | | | (调用 func) - | | | (promise.set_value) - | | | - |-- future.get() <--------- | 共享状态就绪 - | (拿到结果或异常) +```mermaid +sequenceDiagram + participant 调用者线程 + participant 任务队列 + participant 工作线程 + + 调用者线程->>任务队列: submit(func, args)
(创建 packaged_task) + Note right of 调用者线程: 获取 future + 任务队列-->>调用者线程: 返回 future + 任务队列->>工作线程: 取出 task + 工作线程->>工作线程: task() — 调用 func + Note right of 工作线程: promise.set_value + Note over 调用者线程,工作线程: 共享状态就绪 + 调用者线程->>调用者线程: future.get()
(拿到结果或异常) ``` 这个模式的核心优势在于**解耦**:调用者不需要知道任务在哪个线程上执行、什么时候执行;工作线程不需要知道任务的来源和返回值去向。两者通过共享状态(由 packaged_task 内部的 promise 和返回给调用者的 future 共同持有)进行通信,所有同步细节都被封装在 `std::promise`/`std::future` 的实现里了。 diff --git a/documents/vol5-concurrency/ch06-async-io-coroutine/05-coroutine-echo-server.md b/documents/vol5-concurrency/ch06-async-io-coroutine/05-coroutine-echo-server.md index 8f43be0d1..c3fa77af3 100644 --- a/documents/vol5-concurrency/ch06-async-io-coroutine/05-coroutine-echo-server.md +++ b/documents/vol5-concurrency/ch06-async-io-coroutine/05-coroutine-echo-server.md @@ -51,18 +51,20 @@ sudo apt install netcat-openbsd wrk apache2-utils 数据流大概是这样的: -```text -客户端连接 → epoll 通知 listen_fd 可读 -→ accept_loop 协程恢复,accept 拿到 client_fd -→ 启动 handle_connection(client_fd) 协程 -→ handle_connection 执行 co_await async_read(client_fd) -→ async_read 发现没数据,把 client_fd 注册到 epoll,协程挂起 -→ 客户端发送数据 → epoll 通知 client_fd 可读 -→ EventLoop 恢复 handle_connection 协程 -→ async_read 读取数据,返回字节数 -→ handle_connection 执行 co_await async_write(client_fd) -→ 数据写回客户端 -→ 回到循环开头,继续等下一次读 +```mermaid +flowchart TD + A["客户端连接"] --> B["epoll 通知 listen_fd 可读"] + B --> C["accept_loop 协程恢复
accept 拿到 client_fd"] + C --> D["启动 handle_connection(client_fd) 协程"] + D --> E["handle_connection 执行
co_await async_read(client_fd)"] + E --> F["async_read 发现没数据
把 client_fd 注册到 epoll,协程挂起"] + F --> G["客户端发送数据"] + G --> H["epoll 通知 client_fd 可读"] + H --> I["EventLoop 恢复 handle_connection 协程"] + I --> J["async_read 读取数据,返回字节数"] + J --> K["handle_connection 执行
co_await async_write(client_fd)"] + K --> L["数据写回客户端"] + L --> E ``` 整个流程在单线程内完成,但多客户端并发处理——因为每个客户端有自己的协程,协程在等待 I/O 时挂起让出执行权,不阻塞任何人。 diff --git a/documents/vol5-concurrency/ch09-distributed-bridge/02-distributed-primitives.md b/documents/vol5-concurrency/ch09-distributed-bridge/02-distributed-primitives.md index f50a5f2af..b0b5b2497 100644 --- a/documents/vol5-concurrency/ch09-distributed-bridge/02-distributed-primitives.md +++ b/documents/vol5-concurrency/ch09-distributed-bridge/02-distributed-primitives.md @@ -75,14 +75,11 @@ related: 很好,现在我们把四个模型放在一起看,它们构成了一个从强到弱的层级: -```text -线性一致性 (Linearizability) - ↓ 满足线性一致 → 必然满足以下所有 -顺序一致性 (Sequential Consistency) - ↓ 满足顺序一致 → 必然满足以下所有 -因果一致性 (Causal Consistency) - ↓ 满足因果一致 → 必然满足以下所有 -最终一致性 (Eventual Consistency) +```mermaid +flowchart TD + A["线性一致性
(Linearizability)"] -->|"满足线性一致 → 必然满足以下所有"| B["顺序一致性
(Sequential Consistency)"] + B -->|"满足顺序一致 → 必然满足以下所有"| C["因果一致性
(Causal Consistency)"] + C -->|"满足因果一致 → 必然满足以下所有"| D["最终一致性
(Eventual Consistency)"] ``` 层级关系意味着:满足线性一致性的系统一定也满足顺序一致性、因果一致性和最终一致性。反过来,满足最终一致性的系统不一定满足因果一致性。每往上一层,你获得更强的一致性保证,但也付出更高的延迟和可用性代价。 diff --git a/documents/vol7-engineering/msvc-debugging-internals.md b/documents/vol7-engineering/msvc-debugging-internals.md index 70fa5623e..610a19989 100644 --- a/documents/vol7-engineering/msvc-debugging-internals.md +++ b/documents/vol7-engineering/msvc-debugging-internals.md @@ -78,25 +78,14 @@ PDB 文件 (Program Database)是连接"二进制世界"与"源代码世界"的 为了方便博客读者理解,你可以构思这样一张架构图: -```text - -[ 开发者 (User) ] - | 交互 (F5, F10, 查看变量) - v -[ Visual Studio IDE (UI层) ] - | 发送指令 - v -[ 调试引擎 (Debug Engine) ] <----读取----> [ PDB 符号文件 ] - | 跨进程通讯 (RPC) - v -[ msvsmon.exe (调试监视器) ] - | 调用 Win32 Debug API - v -[ Windows Kernel (操作系统内核) ] - | 控制 / 捕获异常 - v -[ 目标进程 (App.exe) ] - +```mermaid +flowchart TD + A["开发者 (User)"] -->|"交互 (F5, F10, 查看变量)"| B["Visual Studio IDE (UI层)"] + B -->|"发送指令"| C["调试引擎 (Debug Engine)"] + C <-->|"读取"| D["PDB 符号文件"] + C -->|"跨进程通讯 (RPC)"| E["msvsmon.exe (调试监视器)"] + E -->|"调用 Win32 Debug API"| F["Windows Kernel (操作系统内核)"] + F -->|"控制 / 捕获异常"| G["目标进程 (App.exe)"] ``` --- diff --git a/documents/vol8-domains/cpp-deep-dives/.pages b/documents/vol8-domains/cpp-deep-dives/.pages new file mode 100644 index 000000000..cafb426bb --- /dev/null +++ b/documents/vol8-domains/cpp-deep-dives/.pages @@ -0,0 +1 @@ +title: C++ 深度专题 diff --git a/documents/vol8-domains/cpp-deep-dives/index.md b/documents/vol8-domains/cpp-deep-dives/index.md new file mode 100644 index 000000000..c23e8a83d --- /dev/null +++ b/documents/vol8-domains/cpp-deep-dives/index.md @@ -0,0 +1,14 @@ +--- +title: "C++ 深度专题" +description: "C++ 语言机制与设计模式的深度探讨" +--- + +# C++ 深度专题 + +这里收录的是一些不适合归入某一卷、但值得系统性深入探讨的 C++ 专题。它们通常横跨多个知识领域——从语言机制到工程实践,从内存模型到异步设计——需要拉通来看才能讲透。 + +## 专题导航 + + + 指针语义与弱引用设计 + diff --git a/documents/vol8-domains/cpp-deep-dives/pointer-semantics/.pages b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/.pages new file mode 100644 index 000000000..7c27fe6c6 --- /dev/null +++ b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/.pages @@ -0,0 +1 @@ +title: 指针语义与弱引用设计 diff --git a/documents/vol8-domains/cpp-deep-dives/pointer-semantics/01-non-owning-pointer-overview.md b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/01-non-owning-pointer-overview.md new file mode 100644 index 000000000..3e8783c04 --- /dev/null +++ b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/01-non-owning-pointer-overview.md @@ -0,0 +1,326 @@ +--- +title: "非拥有指针全景:从 T* 到 Borrowed 到 ObserverPtr" +description: "理解 C++ 中借用、观察与非拥有指针的语义边界,手搓 Borrowed 和 ObserverPtr" +chapter: 1 +order: 1 +tags: + - host + - cpp-modern + - intermediate + - 智能指针 + - 内存管理 +difficulty: intermediate +platform: host +reading_time_minutes: 20 +prerequisites: + - "卷二 · 第一章:RAII 深入理解" + - "卷二 · 第一章:weak_ptr 与循环引用" +related: + - "WeakPtr 反模式:T* + raw Flag* 的致命陷阱" +cpp_standard: [17, 20] +--- + +# 非拥有指针全景:从 T* 到 Borrowed 到 ObserverPtr + +## 引言 + +我很好奇,不知道有没有人有过这样的经历:拿到一个项目,按照需要打开一个函数,看到参数列表里赫然写着 `T* ptr`,然后就开始犯嘀咕——这个指针到底是"拥有"这个对象呢,还是只是"借用"一下?调用者是不是需要检查 nullptr?函数返回后这个对象还活着吗? + +裸指针 `T*` 什么都可能是,也什么都没承诺。它可能是拥有者(比如 `new` 出来还没交给智能指针的那个瞬间),可能是借用者(传给函数用一下),也可能是一个悬垂指针(对象早就没了,指针还留着)。编译器不会帮你区分,注释也不一定靠得住(没准注释是AI写的,代) + +C++ Core Guidelines 里有一条 R.3 说得很直白:**裸指针(非 `owner` 的 `T*`)应该只用来表示非拥有的观察或借用**。但在实际代码里,我们拿到一个 `T*`,根本分不清它到底想表达什么语义。 + +所以我们今天要做的事情很明确:梳理 C++ 中"不拥有对象"的各种指针表达方式,然后手搓两个语义明确的类型——`Borrowed` 和 `ObserverPtr`——让代码自己说话。 + +先把结论放在前面:非拥有不等于安全,可空不等于能判活,这些类型各有各的适用场景,用错了比用裸指针还坑。 + +## 核心概念:四层语义模型 + +在动手写代码之前,我们需要先理清一件事——C++ 里的"不拥有"到底有几种语义。这里我们把它分成四层: + +**第一层:借用(Borrowing)。** `T*` 和 `T&` 是最原始的借用方式。你拿到一个指针或引用,用完就还回去,不管理对象的生命周期,也不关心对象什么时候销毁。适合函数参数这种"短暂同步使用"的场景,但千万别存下来以后再用。毕竟资源没有义务告诉你,咱们这资源炸了,请您另寻高就。 + +**第二层:显式观察(Observation)。** 从这里开始,我们就出现了更语义化的说明。我的意思是——当我们持有`ObserverPtr`的适合,我们不过是想说——他虽然被持久化了,但是我们丝毫不拥有它,甚至我们没办法知道他是否失效。"我只是观察它,我知道有这个事情。但是我不拥有它,或者说,他到底能不能用,我一点保证不了"。和裸指针的区别在于**可读性**(听着有点鸡肋了哈哈):看到 `ObserverPtr` 就知道这是一个纯观察关系。但它和 `T*` 一样,不能判活——对象销毁了你还拿着 ObserverPtr,解引用就是 UB。 + +**第三层:非 owning 弱引用(Weak Reference)。** 这是 `WeakPtr` 登场的层次。它和 ObserverPtr 的核心区别是:对象销毁后,你可以安全地检测到失效。为此它需要一个独立于对象的 control block 来记录"对象是否还活着"。但是你说。我还想lock一下,把他延长生命周期,额,做不到。 + +**第四层:shared ownership 弱引用。** 这就是 `std::weak_ptr`,它和第三层的区别在于它依赖 `std::shared_ptr` 的控制块,调用 `lock()` 会临时延长对象生命周期。 + +现在我们用一个表格来对比这四层: + +| 特性 | T* | T& | Borrowed\ | ObserverPtr\ | WeakPtr\ | std::weak_ptr\ | +|------|----|----|---------------|-----------------|-------------|-------------------| +| 可空 | 是 | 否 | 否(设计上) | 是 | 是 | 是 | +| 拥有对象 | 否 | 否 | 否 | 否 | 否 | 否 | +| 延长生命周期 | 否 | 否 | 否 | 否 | 否 | lock() 临时延长 | +| 对象销毁后安全判空 | 否 | 否 | 否 | 否 | **是** | **是** | +| 适合函数参数 | 是 | 是 | **推荐** | 可以 | 过重 | 过重 | +| 适合类成员 | 可以但不明确 | 可以 | 不推荐 | **推荐** | 推荐 | 推荐 | +| 适合异步回调 | **危险** | **危险** | **危险** | **危险** | 是 | 是 | + +⚠️ 注意看这一行——"对象销毁后安全判空"。前四种类型(T*、T&、Borrowed、ObserverPtr)全部做不到。只有真正拥有独立 control block 的 WeakPtr 才行。这一点我们第二篇会展开讲,先记住这个结论。 + +## 手搓 Borrowed\:让借用语义显式化 + +`Borrowed` 想解决的问题很简单:函数参数里出现 `const T&` 或者 `T*` 的时候,调用者和阅读者无法一眼看出"这只是一个借用"。我们需要一个类型来把"非空、非拥有、短期使用"这个语义钉死在类型系统里。 + +C++ Core Guidelines 里的 `gsl::not_null` 做了类似的事情——它约束指针不能为空,但不表达借用语义。我们的 `Borrowed` 在此基础上更进一步:它是非空的,它是非拥有的,而且它**禁止从临时对象构造**——因为你不能"借用"一个马上就销毁的东西。 + +先看核心实现: + +```cpp +// borrowed.h +// 教学版 Borrowed:显式非空借用语义 +// 注意:这不是生产级实现,用于教学演示 + +#pragma once + +#include +#include + +template +class Borrowed { +public: + // 从左值引用构造——这是最正常的用法 + explicit Borrowed(T& ref) noexcept : ptr_(&ref) {} + + // 禁止从临时对象构造 + Borrowed(T&&) = delete; + + // 禁止从 nullptr 构造(T* 重载只接受非空指针) + Borrowed(std::nullptr_t) = delete; + + // 从裸指针构造,但调用者需保证非空 + explicit Borrowed(T* ptr) noexcept : ptr_(ptr) + { + assert(ptr != nullptr && "Borrowed requires a non-null pointer"); + } + + // 默认拷贝和移动——借用是可以传递的 + Borrowed(const Borrowed&) = default; + Borrowed& operator=(const Borrowed&) = default; + Borrowed(Borrowed&&) = default; + Borrowed& operator=(Borrowed&&) = default; + + // 访问接口 + T& get() const noexcept { return *ptr_; } + T* operator->() const noexcept { return ptr_; } + T& operator*() const noexcept { return *ptr_; } + +private: + T* ptr_; +}; + +// 辅助函数:从引用创建 Borrowed,省去写 explicit 构造 +template +Borrowed borrow(T& ref) noexcept +{ + return Borrowed(ref); +} +``` + +那很显然我们会有这些问题: + +**为什么禁止从临时对象构造?** 这是 `Borrowed` 和裸引用之间最关键的区别。看这个场景: + +```cpp +std::string get_name(); + +// 如果允许从临时对象构造,就会出这种事: +// Borrowed b(get_name()); // 临时对象在表达式结束时销毁 +// 到这里,get_name返回的对象就被销毁掉了,这个时候访问持有的引用就是踩到地雷了 +// b.get(); // 悬垂引用! +``` + +`T&&` 被标记为 `= delete` 之后,编译器会在编译期直接拒绝这种用法。这是 Rust 借用检查器在 C++ 里能做的最接近的模拟——虽然不像 Rust 那样全面,但至少堵住了最常见的坑。 + +**为什么构造函数是 explicit?** 防止隐式转换。你不会希望某个函数接受 `Borrowed` 然后被隐式地从 `Foo&` 调用——借用的动作应该是有意识的。 + +**为什么有 `borrow()` 辅助函数?** 纯粹是为了方便。因为构造函数是 explicit 的,每次写 `Borrowed(foo)` 有点啰嗦,`borrow(foo)` 更清爽。标准库也有类似的设计,比如 `std::make_pair`、`std::make_shared`。 + +**为什么不禁止作为类成员?** 技术上可以做到(比如通过 `static_assert` 加 SFINAE),但实际上过度工程化了。我们在文档和惯例中约定"Borrowed 不应该作为类成员保存"就足够了。编译器强制和团队规范之间,我们选择后者——因为 C++ 的类型系统本来就不擅长表达生命周期约束(要不然,为什么我们在这里坐下来谈这个,用蹩脚的方式来表达我们的意思呢),硬做反而容易引入不必要的复杂度。 + +一个典型的正确用法: + +```cpp +void process_data(Borrowed> data) +{ + // 调用者保证 data 非空,我们直接用 + for (const auto& item : data.get()) { + // ... + } +} + +int main() +{ + std::vector v{1, 2, 3}; + process_data(borrow(v)); // 清晰:我在借用 v +} +``` + +和直接用 `const std::vector&` 相比,`Borrowed` 版本的优势不在运行时行为(它们生成的代码几乎一样),而在于**可读性**——函数签名直接告诉你"这是一个借用"。 + +## 手搓 ObserverPtr\:可空的非拥有观察 + +如果说 `Borrowed` 是给函数参数用的,那 `ObserverPtr` 就是给类成员用的。它的语义是"我观察这个对象,但我不拥有它,我也不负责它的生命周期"。 + +事实上,C++ 标准委员会曾经提出过一个非常类似的类型:`std::experimental::observer_ptr`,收录在 Library Fundamentals TS v2 中。它的定义是: + +> A non-owning pointer, or observer. The observer stores a pointer to a second object, known as the watched object. An observer_ptr may also have no watched object. + +遗憾的是,截至 C++26(似乎是26,我没翻到新消息,如果我又搞错了,欢迎喷我),`observer_ptr` 仍未被正式纳入标准,仍停留在 TS 阶段。但它设计得非常清晰,值得参考。我们的教学版会在其基础上做简化: + +```cpp +// observer_ptr.h +// 教学版 ObserverPtr:可空非拥有观察指针 +// 参考了 std::experimental::observer_ptr (Library Fundamentals TS v2) + +#pragma once + +#include + +template +class ObserverPtr { +public: + // 默认构造:空观察 + ObserverPtr() noexcept : ptr_(nullptr) {} + + // 从 nullptr 构造:空观察 + ObserverPtr(std::nullptr_t) noexcept : ptr_(nullptr) {} + + // 从裸指针构造:开始观察 + explicit ObserverPtr(T* ptr) noexcept : ptr_(ptr) {} + + // 拷贝和移动 + ObserverPtr(const ObserverPtr&) = default; + ObserverPtr& operator=(const ObserverPtr&) = default; + ObserverPtr(ObserverPtr&&) = default; + ObserverPtr& operator=(ObserverPtr&&) = default; + + // 重新绑定观察对象 + void reset(T* ptr = nullptr) noexcept { ptr_ = ptr; } + + // 释放观察关系,返回原指针 + T* release() noexcept + { + T* old = ptr_; + ptr_ = nullptr; + return old; + } + + // 访问 + T* get() const noexcept { return ptr_; } + T& operator*() const noexcept { return *ptr_; } + T* operator->() const noexcept { return ptr_; } + + // 检查是否有观察对象 + explicit operator bool() const noexcept { return ptr_ != nullptr; } + + // 交换 + void swap(ObserverPtr& other) noexcept + { + T* tmp = ptr_; + ptr_ = other.ptr_; + other.ptr_ = tmp; + } + +private: + T* ptr_; +}; + +// 相等比较 +template +bool operator==(const ObserverPtr& a, const ObserverPtr& b) noexcept +{ + return a.get() == b.get(); +} + +template +bool operator==(const ObserverPtr& a, std::nullptr_t) noexcept +{ + return !a; +} + +// 辅助函数 +template +ObserverPtr make_observer(T* ptr) noexcept +{ + return ObserverPtr(ptr); +} +``` + +**ObserverPtr 和 Borrowed 有什么区别?** 核心区别在两个字上:**可空**。Borrowed 表达的是"我保证非空的借用",ObserverPtr 表达的是"我可能为空的观察"。前者适合函数参数(调用者保证非空),后者适合作为持久化的类成员或者存储成员(观察对象可能还没设置,或者被设置成空)。 + +**为什么 ObserverPtr 不是 WeakPtr?** 这是最常见的误解。ObserverPtr 和 WeakPtr 的区别不在于 API 长什么样(它们都有 `get()`、`operator->`、`operator bool()`),而在于**对象销毁后会发生什么**。ObserverPtr 内部就是一个裸指针,对象销毁了它什么都不知道,解引用就是 UB。真正的 WeakPtr 需要一个独立于对象的 control block 来记录存活状态——这是后面笔者计划投稿到其他问题和专栏的文章了! + +典型的正确用法——类成员观察关系: + +```cpp +class Logger; + +class Service { +public: + void set_logger(Logger* log) { logger_.reset(log); } + + void do_work() + { + if (logger_) { + // 有 Logger 才记录,没有就算了 + // ... + } + } + +private: + ObserverPtr logger_; // 我观察 Logger,但不拥有它 +}; +``` + +典型的错误用法——异步回调: + +```cpp +// 错误!ObserverPtr 不能保证对象还活着 +void Service::async_task() +{ + // 如果 Service 在回调执行前被销毁,logger_ 就是悬垂的 + // 这个 callback 捕获了 logger_,执行时可能 UB + auto callback = [this]() { + if (logger_) { // 孩子们,这种东西很危险 + // logger_ 的 ptr_ 指向的 Logger 可能已经不存在了 + // operator bool 只检查 ptr_ 是否为 nullptr + // 如果 Logger 被销毁但 ptr_ 没被 reset,这里就是 UB + } + }; + // post_callback(callback); // 别这么做 +} +``` + +## Borrowed、ObserverPtr 和裸指针的关系 + +现在我们回头看,把这三个类型和裸指针的关系说清楚。 + +`Borrowed` 本质上是 `T&` 的一个类型安全的包装。它比 `T&` 多了"禁止从临时对象构造"的约束,比 `T*` 多了"非空"的保证。它的开销等于零——编译器优化后和裸引用完全一样。它的限制也和裸引用一样:**不能判活**。 + +`ObserverPtr` 本质上是 `T*` 的一个语义标注。它和裸指针的运行时行为完全一致,区别只在于可读性——当你看到一个 `ObserverPtr` 类型的成员变量时,你不需要猜测它是不是拥有那个 Logger,类型名字已经替你回答了。但同样的,**它不能判活**。 + +而裸指针 `T*` 的问题不在于它"不安全",而在于它"不表态"——拿到一个 `T*`,你不知道它是拥有的还是非拥有的,是可空的还是保证非空的,是短期的还是长期的。`Borrowed` 和 `ObserverPtr` 解决的是这个"不表态"的问题。 + +## 小结 + +我们把这一篇的关键要点总结一下: + +- **T\*** 和 **T&** 是 C++ 最原始的借用机制,本身不表达所有权语义 +- **Borrowed\** 表达非空借用,适合函数参数,禁止从临时对象构造,不延长生命周期 +- **ObserverPtr\** 表达可空非拥有观察,适合类成员,不提供判活能力 +- **非拥有不等于安全**——Borrowed 和 ObserverPtr 都不能在对象销毁后安全检测失效 +- 它们的核心价值是**语义表达**,不是运行时安全——让代码自己说话,减少歧义 + +到这里我们只解决了"借用"和"观察"两个语义层。真正麻烦的是"弱引用"——当你需要在一个对象可能随时销毁的世界里安全地持有它的引用时,光靠 Borrowed 和 ObserverPtr 是不够的。 + +下一篇我们就来拆解一个看起来很像 WeakPtr 但实际上不是的东西:`T* + raw Flag*`。 + +## 参考资源 + +- [C++ Core Guidelines - R.3: A raw pointer (a T\*) is non-owning](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-ptr) +- [std::experimental::observer_ptr - cppreference](https://en.cppreference.com/cpp/experimental/observer_ptr) +- [GSL: Guidelines Support Library (Microsoft)](https://github.com/microsoft/GSL) — `gsl::not_null` 和 `gsl::span` +- [C++ Core Guidelines - F.7: For general use, take T\* or T\& arguments rather than smart pointers](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rf-smartptrref) diff --git a/documents/vol8-domains/cpp-deep-dives/pointer-semantics/02-unsafe-weakptr-ub.md b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/02-unsafe-weakptr-ub.md new file mode 100644 index 000000000..a760829bd --- /dev/null +++ b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/02-unsafe-weakptr-ub.md @@ -0,0 +1,312 @@ +--- +title: "WeakPtr 反模式:T* + raw Flag* 的致命陷阱" +description: "深入分析 T* + raw Flag* 为什么不是可靠 WeakPtr,用最小示例复现 UB" +chapter: 1 +order: 2 +tags: + - host + - cpp-modern + - advanced + - 智能指针 + - 引用计数 +difficulty: advanced +platform: host +reading_time_minutes: 11 +prerequisites: + - "非拥有指针全景:从 T* 到 Borrowed 到 ObserverPtr" +related: + - "SimpleWeakPtr:T* + shared_ptr 的安全改进" +cpp_standard: [17, 20] +--- + +# WeakPtr 反模式:`T* + raw Flag*` 的致命陷阱 + +## 引言 + +上一篇我们讲完了借用和观察——`Borrowed` 和 `ObserverPtr` 解决了"这个指针到底想说什么"的问题,但它们都有一个共同的硬伤:对象销毁之后,你拿着它们毫无办法。解引用就是 UB,没有任何转圜余地。 + +所以很自然地,下一个需求就是"弱引用"——我要持有一个对象的引用,但我不拥有它,而且我希望在对象销毁之后能安全地检测到失效,而不是解引用一个悬垂指针。 + +最直觉的方案是什么?搞一个 Flag: + +```cpp +struct Flag { + bool alive = true; +}; +``` + +`WeakPtr` 里保存一个 `T*` 和一个 `Flag*`,用的时候检查 `flag_->alive`。Owner 析构的时候把 `alive` 设成 `false`。听起来很完美——但这篇文章要讲的核心论点是:**这个方案根本不安全,它不应该叫 WeakPtr。** + +## 为什么这个设计有诱惑力 + +我们先实现它,看看为什么它"看起来能工作"。 + +```cpp +// unsafe_weak_ptr.h +// ⚠️ 教学用反模式实现,不要在生产代码中使用 + +#pragma once + +#include + +struct Flag { + bool alive = true; +}; + +template +class UnsafeWeakPtr { +public: + UnsafeWeakPtr(T* ptr, Flag* flag) : ptr_(ptr), flag_(flag) {} + + // 检查对象是否还活着 + bool is_valid() const + { + return flag_ && flag_->alive; + } + + // 获取对象指针,如果已失效则返回 nullptr + T* get() const + { + if (is_valid()) { + return ptr_; + } + return nullptr; + } + + T& operator*() const { return *get(); } + T* operator->() const { return get(); } + +private: + T* ptr_; + Flag* flag_; +}; + +template +class UnsafeWeakPtrFactory { +public: + explicit UnsafeWeakPtrFactory(T* owner) : owner_(owner) {} + + UnsafeWeakPtr get_weak_ptr() + { + return UnsafeWeakPtr(owner_, &flag_); + } + + void invalidate() + { + flag_.alive = false; + } + + ~UnsafeWeakPtrFactory() + { + flag_.alive = false; + } + +private: + T* owner_; + Flag flag_; // Flag 作为 Factory 的成员变量存在 +}; +``` + +看起来相当合理——`Flag` 和 `Owner` 绑定在一起,Owner 析构的时候 `flag_.alive` 被设成 `false`,外部 WeakPtr 再调用 `get()` 就会返回 `nullptr`。 + +在同步、单线程、WeakPtr 的生命周期严格短于 Owner 的场景下,这个实现**确实能工作**。问题在于,这些前提条件在真实工程中极其脆弱。生命周期严格短于 Owner 的场景,那要这个抽象干嘛呢,这是不太可靠的。 + +## 为什么它本质不安全 + +核心问题只有一个:**Flag 的生命周期和 Owner 绑定在一起。** + +当 Owner 析构时,`UnsafeWeakPtrFactory` 作为 Owner 的成员也会析构。`Flag flag_` 作为 `UnsafeWeakPtrFactory` 的成员变量,也会随之销毁。此时,外部任何还活着的 `UnsafeWeakPtr` 手里的 `flag_` 指针——变成了悬垂指针。 + +所以 `UnsafeWeakPtr::is_valid()` 这个函数做了什么?它解引用了一个可能已经悬垂的 `Flag*`,去读一个已经不存在的 `bool alive`。这就是 **未定义行为**(Undefined Behavior)。 + +让我们画一个生命周期图来把这个过程看清: + +**阶段 1:Owner 存活时** — `flag_->alive == true`,一切正常: + +```mermaid +graph LR + subgraph Owner["Owner"] + Factory["Factory"] + Flag["Flag\nalive = true"] + end + Factory --> Flag + subgraph WP["WeakPtr"] + ptr["ptr_"] + fp["flag_"] + end + fp -.->|"有效引用"| Flag + ptr -->|"有效引用"| T["对象 T"] + style Flag fill:#4CAF50,color:#fff + style T fill:#2196F3,color:#fff +``` + +**阶段 2:Owner 析构后** — `flag_` 和 `ptr_` 均为悬垂指针: + +```mermaid +graph LR + subgraph Dead["已销毁"] + FactoryX["Factory ✗"] + FlagX["Flag ✗\n已释放"] + end + subgraph WP["WeakPtr(仍存活)"] + ptr["ptr_"] + fp["flag_"] + end + fp -.->|"💀 悬垂指针"| FlagX + ptr -.->|"💀 悬垂指针"| DeadT["???"] + style FlagX fill:#f44336,color:#fff + style DeadT fill:#f44336,color:#fff + style Dead fill:#ffebee +``` + +`is_valid()` 检查 `flag_->alive` 的那一刻,`flag_` 指向的内存可能已经被回收、被复用、或者被覆盖。返回 `true` 还是 `false` 完全取决于那块内存现在是什么状态——这就是 UB。 + +## 最小 UB 复现 + +接下来我们写一段最小示例来实际触发这个问题。需要注意:UB 的行为是不可预测的,以下代码在某些编译器/优化级别下可能"看起来正常",但这不意味着它是安全的。 + +```cpp +// unsafe_weak_ptr_ub_demo.cpp +// 编译:g++ -std=c++17 -O0 -g unsafe_weak_ptr_ub_demo.cpp +// 注意:UB 的表现因编译器、优化级别、运行环境而异 +// 这里用 -O0 是为了让 UB 更容易被观察到 + +#include +#include + +struct Flag { + bool alive = true; +}; + +template +class UnsafeWeakPtr { +public: + UnsafeWeakPtr(T* ptr, Flag* flag) : ptr_(ptr), flag_(flag) {} + bool is_valid() const { return flag_ && flag_->alive; } + T* get() const { return is_valid() ? ptr_ : nullptr; } + +private: + T* ptr_; + Flag* flag_; +}; + +template +class UnsafeWeakPtrFactory { +public: + explicit UnsafeWeakPtrFactory(T* owner) : owner_(owner) {} + UnsafeWeakPtr get_weak_ptr() + { + return UnsafeWeakPtr(owner_, &flag_); + } + ~UnsafeWeakPtrFactory() { flag_.alive = false; } + +private: + T* owner_; + Flag flag_; +}; + +struct Widget { + int value = 42; + UnsafeWeakPtrFactory factory{this}; + + UnsafeWeakPtr get_weak_ptr() + { + return factory.get_weak_ptr(); + } +}; + +int main() +{ + UnsafeWeakPtr weak = [] { + auto w = std::make_unique(); + return w->get_weak_ptr(); + // w 在这里析构 + // Widget 析构 → factory 析构 → Flag 析构 + }(); + + // 此时 weak.flag_ 指向已销毁的 Flag + // weak.ptr_ 指向已销毁的 Widget + + // ⚠️ UB:解引用已释放的 Flag + std::cout << "is_valid() = " << std::boolalpha << weak.is_valid() << '\n'; + + // ⚠️ UB:如果 is_valid() 恰好返回 true,get() 返回悬垂指针 + if (auto* p = weak.get()) { + std::cout << "value = " << p->value << '\n'; // UB:读取已释放的内存 + } else { + std::cout << "Widget 已失效(但这个结果本身就是 UB 的产物)\n"; + } +} +``` + +在我的测试环境(GCC 16, -O0)下,这段代码的输出是: + +```text +is_valid() = false +Widget 已失效(但这个结果本身就是 UB 的产物) +``` + +看起来 `is_valid()` 正确返回了 `false`——但这不意味着它是安全的。之所以返回 `false`,是因为 `~UnsafeWeakPtrFactory()` 先把 `alive` 设成了 `false`,然后 Widget 的内存才被释放。`is_valid()` 读到的恰好是析构函数写入的值——因为那块内存还没被分配器复用。用 AddressSanitizer 编译(`-fsanitize=address`)可以清楚地看到 `heap-use-after-free` 错误:`is_valid()` 在访问已经释放的内存。 + +换成不同的分配器、不同的优化级别、或者在析构和读取之间插入更多内存操作,结果可能完全不同——`is_valid()` 可能返回 `true`,`get()` 可能返回一个指向已释放内存的非空指针。UB 的行为是不可预测的,**"看起来能工作"恰恰是 UB 最危险的表现形式**。 + +## 为什么异步回调会彻底打破约束 + +有人可能会说:"只要保证 WeakPtr 不比 Owner 长寿就行了。" 这个约束在同步代码里还能靠人工检查勉强维持,但在异步回调场景下几乎不可能保证。 + +```cpp +// 定时器回调场景 +class Session { +public: + UnsafeWeakPtr get_weak() + { + return factory_.get_weak_ptr(); + } + + void start_heartbeat() + { + auto weak = get_weak(); + // 1 秒后执行回调 + timer_.schedule(1000ms, [weak]() { + // Session 可能已经在回调执行前被销毁了 + // weak.is_valid() 访问已销毁的 Flag → UB + if (weak.is_valid()) { + // ... + } + }); + } + +private: + UnsafeWeakPtrFactory factory_{this}; + Timer timer_; +}; +``` + +异步回调的本质就是"把引用保存起来,以后再用"。"以后"是什么时候?对象还活着吗?你不知道。而 `UnsafeWeakPtr` 的安全前提——"WeakPtr 不比 Owner 长寿"——在异步场景下就是一个笑话。 + +## 那它到底应该叫什么 + +这个 `T* + raw Flag*` 的组合不是一无是处。在特定约束下(同步使用、WeakPtr 生命周期严格受控于 Owner),它能工作。但它不应该叫 `WeakPtr`,因为这个名字暗示了"对象销毁后可以安全检测失效"——而它做不到。 + +更诚实的名字是: + +- **`UnsafeWeakPtr`**:明确标注不安全 +- **`OwnerBoundWeakPtr`**:表达它和 Owner 生命周期绑定 +- **`BorrowedWeakPtr`**:表达它本质上还是借用 + +如果一定要使用它,必须在文档和命名中清楚说明约束条件。但更好的做法是——用真正的 WeakPtr。下一篇我们就来实现一个安全的版本。 + +## 小结 + +- `T* + raw Flag*` 看起来像 WeakPtr,但 `Get()` 访问 `flag_->alive` 本身就可能是 UB +- 核心问题:Flag 的生命周期绑定在 Owner 上,Owner 销毁后 Flag 也不存在了 +- 在同步且 WeakPtr 严格短命于 Owner 的场景下可能"能工作",但这不是可靠的 WeakPtr +- 异步回调会彻底打破"WeakPtr 不比 Owner 长"的约束 +- 它最多应该叫 `UnsafeWeakPtr` 或 `OwnerBoundWeakPtr` +- 要安全:control block 必须独立于 Owner 的生命周期——这是下一篇的内容 + +## 参考资源 + +- [Chromium Smart Pointer Guidelines](https://www.chromium.org/developers/smart-pointer-guidelines/) — Chrome 的 WeakPtr 用独立 control block 解决了这个问题 +- [C++ Core Guidelines - CP.50: Define a mutex together with the data it guards](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) — 虽然讲的是 mutex,但"control block 和对象生命周期分离"的设计思路类似 +- [What is undefined behavior? - StackOverflow](https://stackoverflow.com/questions/23979841/what-is-undefined-behavior) diff --git a/documents/vol8-domains/cpp-deep-dives/pointer-semantics/03-simple-weakptr.md b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/03-simple-weakptr.md new file mode 100644 index 000000000..430a62f83 --- /dev/null +++ b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/03-simple-weakptr.md @@ -0,0 +1,214 @@ +--- +title: "SimpleWeakPtr:T* + shared_ptr 的安全改进" +description: "用 shared_ptr 构建 control block,实现对象销毁后的安全判空" +chapter: 1 +order: 3 +tags: + - host + - cpp-modern + - intermediate + - 智能指针 + - 引用计数 +difficulty: intermediate +platform: host +reading_time_minutes: 8 +prerequisites: + - "WeakPtr 反模式:T* + raw Flag* 的致命陷阱" +related: + - "Chrome-like WeakPtr:引用计数控制块与 WeakPtrFactory" +cpp_standard: [17, 20] +--- + +# SimpleWeakPtr:T* + shared_ptr\ 的安全改进 + +## 引言 + +上一篇我们拆解了 `T* + raw Flag*` 的致命问题:Flag 的生命周期绑定在 Owner 上,Owner 析构后 Flag 也跟着没了,外部 WeakPtr 手里的 `flag_` 变成悬垂指针——`is_valid()` 本身就是 UB。 + +解法很直接:让 Flag 的生命周期独立于 Owner。怎么做?用 `std::shared_ptr` 来持有它——Factory 和所有 WeakPtr 共享同一个 Flag 的所有权。Owner 析构时只 invalidate Flag(设 `alive = false`),但 Flag 对象本身继续活着,直到最后一个持有它的 WeakPtr 也销毁。 + +这样,`is_valid()` 永远不会访问已释放的内存,因为它访问的 Flag 对象一定还活着。 + +## 核心设计 + +先看实现,然后我们逐段解释为什么这样设计。 + +```cpp +// simple_weak_ptr.h +// 教学版 SimpleWeakPtr:T* + shared_ptr +// control block 通过 shared_ptr 管理,保证生命周期独立于 Owner + +#pragma once + +#include + +struct Flag { + bool alive = true; + + void invalidate() { alive = false; } +}; + +template +class SimpleWeakPtr { +public: + SimpleWeakPtr() = default; + + SimpleWeakPtr(T* ptr, std::shared_ptr flag) + : ptr_(ptr), flag_(std::move(flag)) {} + + // 检查对象是否还有效 + // 安全:flag_ 是 shared_ptr,只要这个 WeakPtr 还活着,Flag 就一定活着 + bool is_valid() const + { + return flag_ && flag_->alive; + } + + // 获取对象指针,已失效则返回 nullptr + T* get() const + { + if (is_valid()) { + return ptr_; + } + return nullptr; + } + + T& operator*() const { return *get(); } + T* operator->() const { return get(); } + explicit operator bool() const { return get() != nullptr; } + +private: + T* ptr_ = nullptr; + std::shared_ptr flag_; +}; + +template +class SimpleWeakPtrFactory { +public: + explicit SimpleWeakPtrFactory(T* owner) + : owner_(owner), flag_(std::make_shared()) {} + + SimpleWeakPtr get_weak_ptr() + { + return SimpleWeakPtr(owner_, flag_); + } + + void invalidate() + { + if (flag_) { + flag_->invalidate(); + } + } + + ~SimpleWeakPtrFactory() + { + invalidate(); + } + +private: + T* owner_; + std::shared_ptr flag_; // Factory 和 WeakPtr 共享同一个 Flag +}; +``` + +## 为什么这样就安全了 + +上一篇的问题在于 `Flag*` 是裸指针——它不拥有 Flag,不能保证 Flag 还活着。现在我们换成了 `std::shared_ptr`,情况就完全不同了。 + +`std::shared_ptr` 内部维护一个引用计数。当 Factory 创建 `SimpleWeakPtr` 时,它把自己的 `flag_` 拷贝给 WeakPtr,引用计数 +1。此时有两个 `shared_ptr` 指向同一个 Flag:Factory 持有一个,WeakPtr 持有一个。 + +当 Owner 析构时,Factory 析构函数调用 `invalidate()` 把 `flag_->alive` 设成 `false`。然后 Factory 的 `shared_ptr` 析构,引用计数从 2 变成 1。但 Flag 对象**不会**被销毁,因为还有一个 `shared_ptr`(WeakPtr 手里的那个)在引用它。 + +只有当最后一个持有 Flag 的 `shared_ptr` 也析构时,Flag 才会被销毁。这意味着只要还有任何 `SimpleWeakPtr` 活着,`is_valid()` 就是在访问一个确实存在的 Flag 对象——而不是悬垂指针。 + +生命周期图: + +```mermaid +graph TD + subgraph "时间段 1:Owner 存活" + direction TB + O1["Owner"] + F1["Factory\nshared_ptr"] + W1["WeakPtr\nshared_ptr"] + FL1["Flag\nalive = true\nref_count = 2\n(heap allocated)"] + O1 --> F1 + F1 --> FL1 + W1 --> FL1 + style FL1 fill:#4CAF50,color:#fff + end + + subgraph "时间段 2:Owner 析构后" + direction TB + O2["Owner 已销毁"] + F2["Factory 已销毁\n(shared_ptr 析构)"] + W2["WeakPtr(仍存活)\nshared_ptr"] + FL2["Flag\nalive = false\nref_count = 1\n(仍然活着!)"] + W2 --> FL2 + style FL2 fill:#FF9800,color:#fff + end + + subgraph "时间段 3:WeakPtr 也析构后" + direction TB + FL3["Flag ref_count = 0\nFlag 被销毁\n一切正常,无 UB"] + style FL3 fill:#f44336,color:#fff + end +``` + +## shared_ptr\ 不等于拥有 T + +这里有一个容易混淆的地方需要强调:`shared_ptr` 只是拥有 Flag 这个控制块,**不拥有 T**。 + +Flag 里只有一个 `bool alive`,它不持有 T 的指针,不参与 T 的析构,也不延长 T 的生命周期。T 的生命周期完全由 Owner 自己管理(可能是栈上对象、`unique_ptr` 管理的堆对象、或者其他方式)。Flag 唯一做的事情是记录"T 还活着吗"这个状态。 + +这个区分很重要——如果你把 `shared_ptr` 理解成了"shared_ptr 拥有 T",那就和 `std::shared_ptr` 混淆了。后者拥有 T,前者只拥有控制块。 + +## 线程安全讨论 + +到这里,我们解决了生命周期安全问题。但如果你在多线程场景下使用 `SimpleWeakPtr`,还有新的坑等着。 + +**问题一:`bool alive` 的数据竞争。** 如果一个线程在 `invalidate()` 中写 `alive = false`,另一个线程在 `is_valid()` 中读 `alive`,而且没有任何同步机制,这就是标准意义上的数据竞争——UB。 + +修复方案很简单,把 `bool` 换成 `std::atomic`: + +```cpp +#include + +struct Flag { + std::atomic alive{true}; + + void invalidate() { alive.store(false, std::memory_order_release); } + bool is_alive() const { return alive.load(std::memory_order_acquire); } +}; +``` + +**问题二:即使 Flag 是 atomic 的,T 的并发访问仍然不安全。** 这是最容易被忽略的地方。假设线程 A 调用 `is_valid()` 返回 `true`,然后准备调用 `get()` 获取 T* 并访问 T 的成员。但在 `is_valid()` 和实际访问 T 之间,线程 B 可能正在析构 T。这就是经典的 TOCTOU(Time-of-check-to-time-of-use)竞态。 + +```mermaid +sequenceDiagram + participant A as 线程 A + participant B as 线程 B + + A->>A: is_valid() → true + B->>B: ~Owner() → invalidate() + B->>B: 析构 T + A->>A: get() → T* + A->>A: 访问 T 的成员 → UB! + Note over A,B: T 已经被析构,线程 A 持有的是悬垂指针 +``` + +`atomic` 解决的是 Flag 本身的数据竞争,不是 T 的并发安全问题。这一点我们后面第五篇讨论异步回调的时候会详细展开。 + +## 小结 + +- `shared_ptr` 让 control block 的生命周期独立于 Owner,解决了 `raw Flag*` 的悬垂问题 +- `is_valid()` 现在总是安全的——只要 WeakPtr 还活着,Flag 就一定还活着 +- `shared_ptr` 只拥有控制块,不拥有 T,不延长 T 的生命周期 +- 线程安全需要两步:Flag 用 `atomic` 解决数据竞争,但 T 的并发访问需要额外的同步机制 +- `atomic` 解决的是"读 Flag 不会 UB",不是"读到 alive=true 后访问 T 就安全" + +这是从"不安全的弱引用"到"安全的弱引用"的关键一步。但 `shared_ptr` 引入了堆分配和原子引用计数的开销。有没有一种更轻量的方式实现同样的安全保证?有——Chrome 风格的引用计数 control block。下一篇我们来实现它。 + +## 参考资源 + +- [std::shared_ptr - cppreference](https://en.cppreference.com/w/cpp/memory/shared_ptr) +- [std::atomic - cppreference](https://en.cppreference.com/w/cpp/atomic/atomic) +- [C++ Memory Order 详解](../../../vol5-concurrency/ch03-atomic-memory-model/02-memory-ordering.md) — 本教程卷五深入讨论了 memory order diff --git a/documents/vol8-domains/cpp-deep-dives/pointer-semantics/04-chrome-weakptr.md b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/04-chrome-weakptr.md new file mode 100644 index 000000000..0501334b4 --- /dev/null +++ b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/04-chrome-weakptr.md @@ -0,0 +1,361 @@ +--- +title: "Chrome-like WeakPtr:引用计数控制块与 WeakPtrFactory" +description: "实现教学版 Chrome WeakPtr,理解 ref-counted control block 与序列绑定模型" +chapter: 1 +order: 4 +tags: + - host + - cpp-modern + - advanced + - 智能指针 + - 引用计数 + - 回调机制 +difficulty: advanced +platform: host +reading_time_minutes: 12 +prerequisites: + - "SimpleWeakPtr:T* + shared_ptr 的安全改进" +related: + - "std::weak_ptr 对比与异步回调实战" +cpp_standard: [17, 20] +--- + +# Chrome-like WeakPtr:引用计数控制块与 WeakPtrFactory + +## 引言 + +上一篇我们用 `shared_ptr` 解决了 control block 的生命周期安全问题。它确实管用,但也带来了 `shared_ptr` 自身的开销——堆分配、两个原子引用计数(strong count + weak count)、控制块对象本身的内存占用。 + +对于一个只是存了 `bool alive` 的小结构来说,这些开销有点重了。 + +Chromium 项目很早就遇到了这个问题。Chrome 的代码库里到处都是异步回调、定时器、消息循环——它们需要 WeakPtr,但不需要也不应该用 `shared_ptr` 来管理所有对象。所以 Chrome 设计了一套自己的 WeakPtr 机制,核心思路是:**用引用计数的 control block 来管理失效状态,但这个 control block 比 `shared_ptr` 的控制块简单得多。** + +这一篇我们来实现一个教学版的 Chrome-like WeakPtr,理解它为什么比 `shared_ptr` 更轻量、比 `raw Flag*` 更安全。 + +## 核心设计思路 + +Chrome 的 WeakPtr 设计有几个关键特征: + +**第一,control block 是引用计数的,但不使用 `shared_ptr`。** Chrome 自己管理引用计数,只维护一个简单的计数器——没有 weak count,没有自定义删除器,没有 allocator 支持。这意味着控制块可以更小、更快。 + +**第二,Factory 模式。** 创建 WeakPtr 的唯一途径是通过 `WeakPtrFactory`。Factory 持有控制块并负责在 Owner 析构时 invalidate 所有 WeakPtr。这种集中式管理避免了"谁来 invalidate"的混乱。 + +**第三,序列绑定(Sequence-bound)。** Chrome 的 WeakPtr 设计上不是跨线程安全的——它假设所有使用同一个 WeakPtr 的代码都跑在同一个 sequence(逻辑线程)上。这和 `std::weak_ptr` 的跨线程设计有本质区别。 + +接下来我们实现教学版。 + +## 实现 + +### WeakFlag —— 引用计数的控制块 + +```cpp +// weak_flag.h +// 教学版引用计数控制块 + +#pragma once + +#include + +class WeakFlag { +public: + WeakFlag() = default; + + // 禁止拷贝和移动——控制块是不可复制的 + WeakFlag(const WeakFlag&) = delete; + WeakFlag& operator=(const WeakFlag&) = delete; + + void add_ref() { ref_count_.fetch_add(1, std::memory_order_relaxed); } + + void release() + { + if (ref_count_.fetch_sub(1, std::memory_order_acq_rel) == 1) { + delete this; + } + } + + void invalidate() + { + is_valid_.store(false, std::memory_order_release); + } + + bool is_valid() const + { + return is_valid_.load(std::memory_order_acquire); + } + +private: + std::atomic is_valid_{true}; + std::atomic ref_count_{1}; // Factory 初始持有一份引用 + // 注意:不使用虚析构函数,不使用 delete 的自定义删除器 + // 这个控制块的设计目标是比 shared_ptr 的控制块更轻量 + + ~WeakFlag() = default; +}; +``` + +和 `shared_ptr` 的控制块相比,`WeakFlag` 只有两个原子变量:`is_valid_` 和 `ref_count_`。没有 strong/weak 双计数、没有虚析构、没有 allocator。一个 `WeakFlag` 对象只有 8 字节(`atomic` 1 字节 + 对齐填充 3 字节 + `atomic` 4 字节)。 + +### WeakPtr\ + +```cpp +// weak_ptr.h +// 教学版 Chrome-like WeakPtr + +#pragma once + +#include "weak_flag.h" + +template +class WeakPtr { +public: + WeakPtr() : ptr_(nullptr), flag_(nullptr) {} + + WeakPtr(T* ptr, WeakFlag* flag) : ptr_(ptr), flag_(flag) + { + if (flag_) { + flag_->add_ref(); + } + } + + // 拷贝构造:增加引用计数 + WeakPtr(const WeakPtr& other) : ptr_(other.ptr_), flag_(other.flag_) + { + if (flag_) { + flag_->add_ref(); + } + } + + // 移动构造:转移引用 + WeakPtr(WeakPtr&& other) noexcept + : ptr_(other.ptr_), flag_(other.flag_) + { + other.ptr_ = nullptr; + other.flag_ = nullptr; + } + + // 赋值 + WeakPtr& operator=(const WeakPtr& other) + { + if (this != &other) { + // 先释放旧的 + if (flag_) { + flag_->release(); + } + ptr_ = other.ptr_; + flag_ = other.flag_; + if (flag_) { + flag_->add_ref(); + } + } + return *this; + } + + WeakPtr& operator=(WeakPtr&& other) noexcept + { + if (this != &other) { + if (flag_) { + flag_->release(); + } + ptr_ = other.ptr_; + flag_ = other.flag_; + other.ptr_ = nullptr; + other.flag_ = nullptr; + } + return *this; + } + + // 析构:减少引用计数 + ~WeakPtr() + { + if (flag_) { + flag_->release(); + } + } + + // 检查是否有效 + bool is_valid() const { return flag_ && flag_->is_valid(); } + + // 获取指针 + T* get() const + { + if (is_valid()) { + return ptr_; + } + return nullptr; + } + + T& operator*() const { return *get(); } + T* operator->() const { return get(); } + explicit operator bool() const { return get() != nullptr; } + +private: + T* ptr_; + WeakFlag* flag_; +}; +``` + +### WeakPtrFactory\ + +```cpp +// weak_ptr_factory.h +// 教学版 WeakPtrFactory + +#pragma once + +#include "weak_flag.h" +#include "weak_ptr.h" + +template +class WeakPtrFactory { +public: + explicit WeakPtrFactory(T* owner) : owner_(owner) + { + // Factory 创建时分配 control block + flag_ = new WeakFlag(); + } + + // 禁止拷贝和移动——Factory 和 Owner 绑定 + WeakPtrFactory(const WeakPtrFactory&) = delete; + WeakPtrFactory& operator=(const WeakPtrFactory&) = delete; + + // 创建一个新的 WeakPtr + WeakPtr get_weak_ptr() + { + return WeakPtr(owner_, flag_); + } + + // 使所有已发出的 WeakPtr 失效 + void invalidate_weak_ptrs() + { + if (flag_) { + flag_->invalidate(); + } + } + + // Factory 析构时自动 invalidate + ~WeakPtrFactory() + { + invalidate_weak_ptrs(); + // Factory 释放自己持有的引用 + // 如果还有 WeakPtr 活着,flag_ 不会被 delete + // 最后一个 WeakPtr 析构时才会 delete flag_ + if (flag_) { + flag_->release(); + } + flag_ = nullptr; + } + +private: + T* owner_; + WeakFlag* flag_; +}; +``` + +## 为什么 control block 要引用计数 + +和第三篇的 `shared_ptr` 一样,引用计数的目的是保证 control block 活得比所有 WeakPtr 都久。但 Chrome 的实现比 `shared_ptr` 更轻量,因为: + +**计数器只有一个。** `shared_ptr` 内部有 strong count 和 weak count 两个原子变量。`WeakFlag` 只有一个 `ref_count_`——因为这里没有"共享所有权"的概念,只有"谁还持有着这个控制块"的计数。 + +**没有控制块的堆上额外管理开销。** `shared_ptr` 的控制块通常通过 `new` 分配(除非用 `make_shared`),并且要维护虚析构函数表、allocator 信息等。`WeakFlag` 就是简单的 `new` + `delete`,没有额外开销。 + +**失效机制更直接。** `shared_ptr` 的失效需要通过修改 Flag 的成员变量,而 `WeakFlag::invalidate()` 直接修改原子变量——一次原子 store。 + +## 为什么它比 raw Flag* 安全 + +这个问题上一篇已经回答过了,但用 `WeakFlag` 再说一遍: + +`raw Flag*` 的问题是 Flag 的生命周期绑定在 Factory/Owner 上。Factory 析构 → Flag 析构 → 外部 WeakPtr 手里的 `flag_` 悬垂 → `is_valid()` 就是 UB。 + +`WeakFlag*` + 引用计数解决了这个问题。Factory 析构时调用 `flag_->release()` 把引用计数 -1,但只要还有 WeakPtr 活着,引用计数就还 > 0,`WeakFlag` 对象就不会被 `delete`。`is_valid()` 访问的一定是一个还活着的 `WeakFlag` 对象。 + +## 为什么它比 std::weak_ptr 更适合某些场景 + +`std::weak_ptr` 依赖 `std::shared_ptr` 的控制块。如果你想用 `std::weak_ptr`,你必须先用 `std::shared_ptr` 来管理对象。但很多场景下对象并不是由 `shared_ptr` 管理的——它们可能是栈上对象、`unique_ptr` 管理的堆对象、或者属于某个框架的对象池。为了用 `weak_ptr` 而强行把所有对象都改成 `shared_ptr` 管理,是一种常见的过度设计。 + +Chrome-like WeakPtr 不要求对象由 `shared_ptr` 管理。它只要求对象内部有一个 `WeakPtrFactory` 成员——对象本身可以是任何所有权模式。这使它非常适合 UI 框架、游戏引擎、网络库这些"对象生命周期由框架管理、不是由 shared_ptr 管理"的场景。 + +## 序列绑定模型:为什么它不是跨线程安全的 + +Chrome 的 WeakPtr 在设计上假设所有使用者都跑在同一个 sequence 上。一个 sequence 是一个逻辑上的执行顺序——可以是单线程,也可以是带消息循环的多线程(每个线程有自己的 task runner)。 + +在这个假设下,`is_valid()` 和 `get()` 之间不会有 TOCTOU 竞态——因为 invalidate 和 get 不可能同时执行(它们在同一个 sequence 上排队执行)。 + +但如果跨 sequence 使用——比如一个 sequence 上 invalidate,另一个 sequence 上 get——就可能出现第三篇提到的竞态问题。`atomic` 保证 `is_valid()` 本身不会 UB,但"读到 valid=true 后访问 T"和"T 的析构"之间仍然可能有竞态。 + +所以 Chrome-like WeakPtr 的正确使用方式是:**同一个 sequence 上创建、使用和 invalidate。** 跨 sequence 的场景应该用 `std::weak_ptr` 或者额外的同步机制。 + +## 使用示例 + +```cpp +#include +#include +#include "weak_ptr_factory.h" + +class Session { +public: + Session(int id) : id_(id) {} + + WeakPtr get_weak_ptr() + { + return factory_.get_weak_ptr(); + } + + void do_work() + { + std::cout << "Session " << id_ << " working\n"; + } + + int id() const { return id_; } + +private: + int id_; + // Factory 作为最后一个成员变量——确保在其他成员析构之前 invalidate + WeakPtrFactory factory_{this}; +}; + +int main() +{ + WeakPtr weak = [] { + auto s = std::make_unique(42); + auto w = s->get_weak_ptr(); + std::cout << "Before destroy: valid = " << w.is_valid() << "\n"; + return w; + // Session 在这里析构 + // factory_ 析构 → invalidate → release (ref_count: 2→1) + // WeakFlag 仍然活着(weak 持有) + }(); + + // Session 已经销毁 + std::cout << "After destroy: valid = " << weak.is_valid() << "\n"; + std::cout << "get() returns: " + << (weak.get() ? "non-null" : "nullptr") << "\n"; + + // weak 析构 → release (ref_count: 1→0) → delete WeakFlag +} +``` + +输出: + +```text +Before destroy: valid = true +After destroy: valid = false +get() returns: nullptr +``` + +和第二篇的 `UnsafeWeakPtr` 对比——同样的场景,`UnsafeWeakPtr` 会 UB,而 Chrome-like WeakPtr 安全返回 `false`。 + +## 小结 + +- Chrome-like WeakPtr 用自定义的引用计数 control block(`WeakFlag`)替代 `shared_ptr`,更轻量 +- `WeakPtrFactory` 集中管理 control block 的创建和失效,避免混乱 +- 引用计数保证 control block 活得比所有 WeakPtr 都久——`is_valid()` 永远安全 +- 不要求对象由 `shared_ptr` 管理——适合框架内部的对象生命周期模式 +- 设计上绑定到单个 sequence,不适合任意跨线程使用 +- `atomic` 解决 Flag 数据竞争,但不解决 T 的并发访问安全 + +## 参考资源 + +- [Chromium Smart Pointer Guidelines](https://www.chromium.org/developers/smart-pointer-guidelines/) +- [Chromium 源码:base/memory/weak_ptr.h](https://source.chromium.org/chromium/chromium/src/+/main:base/memory/weak_ptr.h) +- [C++ Core Guidelines - CP.50: Define a mutex together with the data it guards](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) diff --git a/documents/vol8-domains/cpp-deep-dives/pointer-semantics/05-weakptr-comparison-and-async.md b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/05-weakptr-comparison-and-async.md new file mode 100644 index 000000000..2b9adb0b6 --- /dev/null +++ b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/05-weakptr-comparison-and-async.md @@ -0,0 +1,215 @@ +--- +title: "std::weak_ptr 对比与异步回调实战" +description: "对比 std::weak_ptr 与 Chrome WeakPtr,六种异步回调捕获模式的安全分析" +chapter: 1 +order: 5 +tags: + - host + - cpp-modern + - advanced + - 智能指针 + - 异步编程 + - 回调机制 +difficulty: advanced +platform: host +reading_time_minutes: 8 +prerequisites: + - "Chrome-like WeakPtr:引用计数控制块与 WeakPtrFactory" + - "卷二 · 第一章:weak_ptr 与循环引用" +related: + - "跨线程安全、性能取舍与设计原则总结" +cpp_standard: [17, 20] +--- + +# std::weak_ptr 对比与异步回调实战 + +## 引言 + +前面四篇我们从头到尾手搓了一套非拥有指针类型——从 Borrowed 到 ObserverPtr 到各种 WeakPtr。现在到了把所有东西拉通对比的时候。 + +这一篇要做两件事:第一,把 `std::weak_ptr` 和 Chrome-like `WeakPtr` 放在一起,说清楚它们的核心差异;第二,用六种异步回调捕获模式做实战对比,让你直观感受到"错误捕获"和"正确捕获"之间的区别。 + +## std::weak_ptr 与 Chrome WeakPtr 的核心差异 + +先说一个经常被忽略的事实:**`std::weak_ptr` 和 Chrome-like `WeakPtr` 解决的不是同一个问题。** + +`std::weak_ptr` 解决的是"在 shared ownership 模型下的弱引用"。它依赖 `std::shared_ptr` 的控制块,调用 `lock()` 成功后会获得一个 `shared_ptr`,从而**临时延长对象的生命周期**。这意味着只要你 `lock()` 成功了,在你的 `shared_ptr` 存活期间,对象一定不会被析构。 + +Chrome-like `WeakPtr` 解决的是"在非 shared_ptr 管理的对象上的弱引用"。它不依赖 `shared_ptr`,调用 `get()` 不会延长对象生命周期——它只是返回一个指针。对象可能在任何时候被析构,你拿到的指针可能在你用之前就失效了。它只保证你能**安全地检测到失效**,不保证你拿到指针后对象还活着。 + +这是两种完全不同的生命周期策略: + +| 特性 | Chrome-like WeakPtr\ | std::weak_ptr\ | +|------|-------------------------|-------------------| +| 依赖 shared_ptr | 否 | 是 | +| 获取引用时延长生命周期 | **否** | **是**(lock 返回 shared_ptr) | +| 对象析构后安全判空 | 是 | 是 | +| 适合非 shared_ptr 管理的对象 | **是** | 否 | +| 天然跨线程安全 | 否(sequence-bound) | 部分(lock() 原子,但 T 的访问需要同步) | +| 控制块开销 | 小(自定义 ref count) | 较大(shared_ptr 的 control block) | + +**什么时候用 `std::weak_ptr`?** 当对象已经由 `shared_ptr` 管理,你需要在异步场景中安全地观察它,且可能需要临时延长它的生命周期时。 + +**什么时候用 Chrome-like WeakPtr?** 当对象不是由 `shared_ptr` 管理(栈对象、`unique_ptr`、框架管理的对象),你需要在异步回调中安全地检测失效时。 + +**什么时候不应该用 `std::weak_ptr`?** 为了用 `weak_ptr` 而强行把对象改成 `shared_ptr` 管理。这会引入不必要的引用计数开销,而且在多线程下容易引发性能瓶颈(原子引用计数争用)。 + +## 六种异步回调捕获模式 + +接下来我们用实际代码来对比六种在异步回调中捕获对象引用的方式。每种方式我们都会分析:哪里危险、对象销毁后会发生什么、是不是 UB。 + +### 模式 1:捕获裸 `this` —— 危险 + +```cpp +class NetworkClient { +public: + void start_request() + { + // 错误!lambda 捕获了裸 this + timer_.schedule(1000ms, [this]() { + process_response(); // 如果 NetworkClient 已析构,this 是悬垂指针 + }); + } + + void process_response() { /* ... */ } + +private: + Timer timer_; +}; + +// 使用场景 +void test() +{ + auto client = std::make_unique(); + client->start_request(); + // client 在这里析构 +} // 1 秒后回调执行 → this 悬垂 → UB +``` + +**问题**:`this` 只是一个裸指针,不携带任何生命周期信息。对象析构后,回调中的 `this` 是悬垂指针,任何成员访问都是 UB。这是 C++ 异步编程中最常见的崩溃来源。 + +### 模式 2:捕获 `T*` —— 同样危险 + +```cpp +void start_request() +{ + auto* raw_ptr = this; + timer_.schedule(1000ms, [raw_ptr]() { + raw_ptr->process_response(); // 同样的悬垂问题 + }); +} +``` + +**问题**:和捕获 `this` 没有本质区别。`T*` 不提供任何生命周期保障。唯一的区别是它"看起来"像是有意识地捕获了一个指针,但实际上没有比裸 `this` 更安全。 + +### 模式 3:捕获 `ObserverPtr` —— 仍然危险 + +```cpp +void start_request() +{ + auto obs = make_observer(this); + timer_.schedule(1000ms, [obs]() { + if (obs) { + obs->process_response(); // ObserverPtr::operator bool 只检查是否为 nullptr + } // 对象销毁后 obs.get() 仍非 nullptr → 悬垂解引用 + }); +} +``` + +**问题**:`ObserverPtr` 的 `operator bool()` 只检查内部指针是否为 `nullptr`。对象析构后,内部指针不是 `nullptr`(它是悬垂的),所以 `if (obs)` 会通过,然后解引用悬垂指针。UB。 + +### 模式 4:捕获 `UnsafeWeakPtr` —— UB + +```cpp +void start_request() +{ + auto weak = get_unsafe_weak_ptr(); + timer_.schedule(1000ms, [weak]() { + if (weak.is_valid()) { // 访问已销毁的 Flag → UB! + // ... + } + }); +} +``` + +**问题**:如第二篇详细分析的,`is_valid()` 访问的 `Flag*` 可能已经是悬垂指针。判空动作本身就是 UB。这是六种模式中最隐蔽的危险——看起来有"判活"机制,实际上连判活都不安全。 + +### 模式 5:捕获 Chrome-like `WeakPtr` —— 正确 + +```cpp +class NetworkClient { +public: + void start_request() + { + auto weak = factory_.get_weak_ptr(); + timer_.schedule(1000ms, [weak]() { + if (auto* self = weak.get()) { + self->process_response(); // 安全:get() 先检查 control block + } // 失效时返回 nullptr,不会解引用 + }); + } + +private: + Timer timer_; + WeakPtrFactory factory_{this}; +}; +``` + +**分析**:`weak.get()` 先检查 `WeakFlag::is_valid()`。由于 `WeakFlag` 是引用计数的,只要 `weak` 还活着,`WeakFlag` 就一定存在,所以 `is_valid()` 不会 UB。对象析构后 Factory 的析构函数会 invalidate `WeakFlag`,`get()` 返回 `nullptr`,回调安全跳过。 + +**但有一个前提**:回调的执行和对象的析构在同一个 sequence 上。如果跨 sequence,`get()` 返回非空之后、实际使用 `self` 之前,另一个 sequence 可能正在析构对象——这就是 TOCTOU 竞态。 + +### 模式 6:捕获 `std::weak_ptr` —— 正确 + +```cpp +class NetworkClient : public std::enable_shared_from_this { +public: + void start_request() + { + auto weak = weak_from_this(); // C++17 + timer_.schedule(1000ms, [weak]() { + if (auto self = weak.lock()) { + self->process_response(); // lock() 成功 → shared_ptr 延长生命周期 + } // 在 self 的作用域内,对象不会被析构 + }); + } + +private: + Timer timer_; +}; + +// 使用时必须用 shared_ptr 管理 +auto client = std::make_shared(); +client->start_request(); +``` + +**分析**:`weak.lock()` 是原子操作——它要么返回一个有效的 `shared_ptr`(同时引用计数 +1),要么返回空。如果返回了有效的 `shared_ptr`,在你的 `self` 变量存活期间,对象一定不会被析构。这比 Chrome WeakPtr 更安全——它不仅检测失效,还能防止在检测和使用之间对象被析构。 + +**但代价是**:对象必须由 `shared_ptr` 管理,`lock()` 会增加原子引用计数操作。在高频异步场景下,这些原子操作可能成为性能瓶颈。 + +## 六种模式总结 + +| 模式 | 判活能力 | 对象销毁后的行为 | UB? | 适合场景 | +|------|---------|----------------|------|---------| +| 裸 `this` | 无 | 悬垂指针访问 | 是 | 无——永远不要在异步回调中捕获裸 this | +| `T*` | 无 | 悬垂指针访问 | 是 | 无——同上 | +| `ObserverPtr` | 无 | `operator bool` 通过但指针悬垂 | 是 | 同步观察,不用于异步回调 | +| `UnsafeWeakPtr` | 假的 | 判空本身 UB | 是 | 无——不应该使用 | +| Chrome `WeakPtr` | 有(control block) | 安全返回 nullptr | 否(单 sequence) | 非 shared_ptr 对象的异步回调 | +| `std::weak_ptr` | 有(shared_ptr 控制) | 安全返回空 shared_ptr | 否 | shared_ptr 管理的对象的异步回调 | + +## 小结 + +- `std::weak_ptr` 依赖 `shared_ptr`,`lock()` 会临时延长对象生命周期 +- Chrome-like `WeakPtr` 不依赖 `shared_ptr`,不延长对象生命周期,只检测失效 +- 不要为了用 `weak_ptr` 而强行把对象改成 `shared_ptr` 管理 +- 异步回调中永远不要捕获裸 `this`、裸 `T*`、`ObserverPtr` 或 `UnsafeWeakPtr` +- Chrome `WeakPtr` 适合非 `shared_ptr` 场景,但要注意 sequence 绑定 +- `std::weak_ptr` 适合 `shared_ptr` 场景,`lock()` 提供更强的安全保证 + +## 参考资源 + +- [std::weak_ptr - cppreference](https://en.cppreference.com/w/cpp/memory/weak_ptr) +- [std::enable_shared_from_this - cppreference](https://en.cppreference.com/w/cpp/memory/enable_shared_from_this) +- [C++ Core Guidelines - CP.51: Do not use capturing lambdas that are coroutines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) +- [Chromium WeakPtr 设计文档](https://www.chromium.org/developers/weak-ptrs-in-chromium/) diff --git a/documents/vol8-domains/cpp-deep-dives/pointer-semantics/06-design-principles.md b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/06-design-principles.md new file mode 100644 index 000000000..d4139da10 --- /dev/null +++ b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/06-design-principles.md @@ -0,0 +1,137 @@ +--- +title: "跨线程安全、性能取舍与设计原则总结" +description: "生命周期安全不等于线程安全——指针语义设计的最终工程规则与推荐命名" +chapter: 1 +order: 6 +tags: + - host + - cpp-modern + - intermediate + - 智能指针 + - 内存管理 +difficulty: intermediate +platform: host +reading_time_minutes: 7 +prerequisites: + - "std::weak_ptr 对比与异步回调实战" +related: + - "卷二 · 第一章:智能指针与 RAII" +cpp_standard: [17, 20] +--- + +# 跨线程安全、性能取舍与设计原则总结 + +## 引言 + +到这里,我们已经把非拥有指针的整个光谱走了一遍——从最简单的 `T*` 和 `T&`,到手搓的 `Borrowed` 和 `ObserverPtr`,再到三种弱引用方案(`UnsafeWeakPtr`、`SimpleWeakPtr`、Chrome-like `WeakPtr`),最后和 `std::weak_ptr` 做了全面对比。 + +这一篇是收尾,我们把三个还没彻底展开的话题说清楚:跨线程安全的边界到底在哪里,各种类型的性能开销具体差多少,以及把所有东西综合成一组可以落地的工程规则。 + +## 生命周期安全 ≠ 线程安全 + +这是整个专题最重要的一个结论,值得反复强调。 + +**生命周期安全**说的是"对象销毁后你能不能安全地检测到失效"。`WeakPtr` 的 control block 解决的就是这个问题——你可以安全地调用 `is_valid()` 或 `get()` 而不会 UB。 + +**线程安全**说的是"多个线程同时访问对象时会不会出问题"。这和生命周期安全是完全不同的问题维度。 + +我们用一张 2×2 的表格来把这四个象限说清楚: + +| | 生命周期不安全 | 生命周期安全 | +|------|--------------|------------| +| **线程不安全** | `T*`、`T&`、`ObserverPtr` | Chrome `WeakPtr`(单 sequence) | +| **线程安全(部分)** | N/A(没有意义) | `std::weak_ptr`(lock 原子,但 T 的内部状态需要同步) | + +`T*` 在左上角——既不解决生命周期问题,也不解决线程问题。Chrome `WeakPtr` 解决了生命周期问题,但在跨线程场景下仍然有 TOCTOU 竞态。`std::weak_ptr` 的 `lock()` 是原子的,锁住之后对象不会被析构,但对象**内部状态**的并发访问仍然需要 mutex 或其他机制保护。 + +所以:`WeakPtr` 解决的是"我知道对象死了没有",不是"多个线程同时碰这个对象安不安全"。 + +### 为什么 Chrome WeakPtr 是 sequence-bound 的 + +Chrome 的设计哲学是:大多数 UI 和异步框架中的回调都跑在同一个逻辑序列上。定时器回调、事件处理、IO 完成通知——它们由同一个 task runner 分发执行。在这个模型下,invalidate 和 get 不可能同时执行,因为它们排队运行。 + +这比"加一个 mutex 就跨线程安全了"要高效得多——mutex 有运行时开销,而 sequence-bound 是零开销的设计约束。代价是你的使用方式被限制了:不能跨 sequence 传递 WeakPtr。但这个约束在大多数 UI / 事件循环框架里是自然成立的。 + +### 跨线程场景应该怎么做 + +如果确实需要跨线程弱引用,有几种选择: + +- **用 `std::weak_ptr`**:`lock()` 原子地获取 `shared_ptr`,在你的作用域内对象不会被析构。但 T 内部的线程安全需要另外处理。 +- **用 `std::atomic>`**(C++20):提供原子操作来安全地跨线程读写 `shared_ptr`。 +- **用 message passing**:不直接跨线程共享 WeakPtr,而是通过消息队列传递"请你在你的 sequence 上做这件事"的请求,让目标 sequence 自己处理。 + +## 性能比较 + +我们把本专题涉及的所有类型做一个性能对比。数字是近似值,具体取决于平台和编译器: + +| 类型 | 对象大小 | 控制块分配 | 原子操作 | 适合 | +|------|---------|-----------|---------|------| +| `T*` | 8B | 无 | 无 | 同步函数参数 | +| `T&` | 8B(指针实现) | 无 | 无 | 同步函数参数 | +| `Borrowed` | 8B | 无 | 无 | 同步函数参数(语义显式) | +| `ObserverPtr` | 8B | 无 | 无 | 类成员观察 | +| `UnsafeWeakPtr` | 16B | 无 | 无 | 不应该使用 | +| `SimpleWeakPtr` | 24B(T* + shared_ptr) | 1 次 `new` | 拷贝 1 次、析构 1~2 次原子操作 | 教学、简单场景 | +| Chrome `WeakPtr` | 16B(`T*` + `WeakFlag*`) | 1 次 `new` | 拷贝/析构各 1 次原子操作 | 框架内异步回调 | +| `std::weak_ptr` | 16B | 由 `shared_ptr` 管理 | lock/unlock 各 2 次原子操作 | shared_ptr 体系 | + +几个值得注意的细节: + +`Borrowed` 和 `ObserverPtr` 的开销为零——编译器优化后和裸指针完全一样。它们的价值纯粹在语义层面。 + +`SimpleWeakPtr` 比 Chrome `WeakPtr` 大了 8 个字节,因为 `shared_ptr` 内部有两个指针(对象指针 + 控制块指针),而 Chrome 的 `WeakPtr` 只存 `T*` 和 `WeakFlag*`。每次拷贝 `shared_ptr` 需要两次原子操作(strong count + weak count),Chrome 只需要一次。 + +Chrome `WeakPtr` 的控制块(`WeakFlag`)比 `shared_ptr` 的控制块小得多——只有一个原子 bool 和一个原子 int,没有虚析构、没有 allocator、没有 weak count。 + +`std::weak_ptr` 的额外开销取决于它所依赖的 `shared_ptr`。如果你为了用 `weak_ptr` 而把一个本来不需要 `shared_ptr` 的对象强行改成 `shared_ptr` 管理,你不仅要付出控制块的开销,还引入了原子引用计数争用的风险。 + +## 工程规则 + +总结成一组可落地的规则: + +**函数参数**优先用 `T&`、`T*` 或 `Borrowed`。不要在函数参数上使用智能指针来表达非拥有关系。`Borrowed` 提供最明确的语义(非空 + 非拥有),但 `const T&` 在大多数场景下也够用。 + +**类成员观察关系**可以用 `ObserverPtr`。当你要表达"我观察它但不拥有它"时,`ObserverPtr` 比裸 `T*` 的可读性好得多。但要记住它不能判活。 + +**异步回调**永远不要捕获裸 `this`、裸 `T*`、`ObserverPtr` 或任何没有独立 control block 的"弱引用"。正确的选择是 Chrome-like `WeakPtr`(非 `shared_ptr` 场景)或 `std::weak_ptr`(`shared_ptr` 场景)。 + +**不要把 `ObserverPtr` 当 WeakPtr 用**。`ObserverPtr` 只能表达"我不拥有它",不能表达"我知道它还活着吗"。 + +**不要把 `T* + raw Flag*` 叫 WeakPtr**。如果 Flag 的生命周期绑定在 Owner 上,它不是可靠 WeakPtr。给它一个诚实的名字——`UnsafeWeakPtr` 或 `OwnerBoundWeakPtr`。 + +**跨线程场景**优先考虑 `std::weak_ptr` 或 message passing。Chrome-like `WeakPtr` 设计上是 sequence-bound 的,不要把它当跨线程安全指针用。 + +**WeakPtr 解决生命周期感知,不解决线程安全**。无论用哪种 WeakPtr,T 内部状态的并发访问都需要额外的同步机制。 + +## 推荐命名体系 + +最后给出一套推荐的命名约定: + +| 类型 | 命名 | 含义 | +|------|------|------| +| `Borrowed` | 借用 | 非空、非拥有、短期使用、适合函数参数 | +| `ObserverPtr` | 观察 | 可空、非拥有、不提供判活、适合类成员 | +| `UnsafeWeakPtr` | 不安全弱引用 | `T*` + `raw Flag*`,命名明确标注不安全 | +| `WeakPtr` | 安全弱引用 | 真正能在对象销毁后安全判空的弱引用 | +| `WeakPtrFactory` | 弱引用工厂 | 集中创建和管理 WeakPtr 的失效 | + +`UnsafeWeakPtr` 这个名字不是贬义——它是一种**诚实的命名**。当你在代码库里看到 `UnsafeWeakPtr`,你立刻就知道"这个东西有坑,使用时要注意约束条件"。比把它包装成 `WeakPtr` 然后在文档里埋一行小字说"保证 WeakPtr 不比 Owner 长"要负责任得多。 + +## 小结 + +- 生命周期安全和线程安全是两个正交的问题,WeakPtr 只解决前者 +- Chrome `WeakPtr` 通过 sequence-bound 模型获得零开销的安全性,但限制跨线程使用 +- `Borrowed` 和 `ObserverPtr` 运行时开销为零,价值在语义表达 +- Chrome `WeakPtr` 的控制块比 `shared_ptr` 的控制块更轻量 +- 不要为了用 `weak_ptr` 而强行引入 `shared_ptr` +- 命名应该诚实——不安全的东西就要叫不安全 + +这个专题到这里就全部结束了。我们从 `T*` 出发,手搓了 Borrowed、ObserverPtr、UnsafeWeakPtr、SimpleWeakPtr 和 Chrome-like WeakPtr,每一步都解释了设计理由和工程取舍。希望这些内容能帮你在实际工程中做出更清晰的指针语义选择。 + +## 参考资源 + +- [C++ Core Guidelines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) +- [Chromium Smart Pointer Guidelines](https://www.chromium.org/developers/smart-pointer-guidelines/) +- [std::weak_ptr - cppreference](https://en.cppreference.com/w/cpp/memory/weak_ptr) +- [GSL: Guidelines Support Library](https://github.com/microsoft/GSL) +- [P1408R0: Abandon observer_ptr (Stroustrup)](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1408r0.pdf) diff --git a/documents/vol8-domains/cpp-deep-dives/pointer-semantics/index.md b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/index.md new file mode 100644 index 000000000..1ca3f3259 --- /dev/null +++ b/documents/vol8-domains/cpp-deep-dives/pointer-semantics/index.md @@ -0,0 +1,21 @@ +--- +title: "指针语义与弱引用设计" +description: "从 T* 到 Borrowed、ObserverPtr、Chrome-like WeakPtr,理解非拥有指针的语义边界与安全实现" +--- + +# 指针语义与弱引用设计 + +C++ 里到处都是指针,但并不是所有指针都"拥有"它指向的对象。裸指针 `T*` 可以是拥有的也可以是非拥有的,`T&` 表达借用但不可空,`std::weak_ptr` 解决了 shared ownership 下的弱引用问题——但如果你不用 `shared_ptr` 管理对象呢?Chromium 的 `WeakPtr` 又是怎么设计的?为什么 `T* + raw Flag*` 看起来像 WeakPtr 其实不是? + +这个专题我们从头手搓各种非拥有指针类型,一边实现一边搞清楚它们的语义边界、安全条件和工程取舍。 + +## 本章内容 + + + 非拥有指针全景:从 T* 到 Borrowed 到 ObserverPtr + WeakPtr 反模式:T* + raw Flag* 的致命陷阱 + SimpleWeakPtr:T* + shared_ptr<Flag> 的安全改进 + Chrome-like WeakPtr:引用计数控制块与 WeakPtrFactory + std::weak_ptr 对比与异步回调实战 + 跨线程安全、性能取舍与设计原则总结 + diff --git a/documents/vol8-domains/embedded/01-led/04-hal-gpio-clock.md b/documents/vol8-domains/embedded/01-led/04-hal-gpio-clock.md index d31aa4232..e63d568b2 100644 --- a/documents/vol8-domains/embedded/01-led/04-hal-gpio-clock.md +++ b/documents/vol8-domains/embedded/01-led/04-hal-gpio-clock.md @@ -42,35 +42,29 @@ order: 4 下面是我们项目配置下的简化时钟树。注意,这是**我们实际使用的配置**,而不是STM32参考手册里那张让人看一眼就头疼的完整时钟树。我们先只看与我们相关的部分: -```text - ┌──────────────┐ - │ HSI 8MHz │ - │ (内部RC振荡器) │ - └──────┬───────┘ - │ - /2 分频 - │ - 4MHz ──→ PLL ×16 ──→ 64MHz - │ - SYSCLK - 64MHz - │ - ┌─────────────────────────┤ - │ │ - AHB /1 AHB /1 - HCLK = 64MHz HCLK = 64MHz - │ │ - ┌──────────┤ ┌──────┤ - │ │ │ │ - APB1 /2 APB2 /1 DMA Flash - 32MHz 64MHz 控制器 接口 - │ │ - ┌────┤ ┌────┴────┐ - │ │ │ │ - TIM2-4 USART1 GPIOA-E - USART2-3 SPI1 ADC1-2 - I2C1-2 TIM1 - SPI2-3 ... +```mermaid +graph TD + HSI["HSI 8MHz\n(内部RC振荡器)"] + DIV["÷2 分频\n4MHz"] + PLL["PLL ×16\n64MHz"] + SYSCLK["SYSCLK\n64MHz"] + AHB1["AHB ÷1\nHCLK = 64MHz"] + APB1["APB1 ÷2\n32MHz"] + APB2["APB2 ÷1\n64MHz"] + DMA["DMA 控制器"] + Flash["Flash 接口"] + APB1_PERIPH["TIM2-4, USART2-3\nI2C1-2, SPI2-3"] + APB2_PERIPH["USART1, SPI1\nTIM1, ..."] + GPIO["GPIOA-E"] + ADC["ADC1-2"] + + HSI --> DIV --> PLL --> SYSCLK --> AHB1 + AHB1 --> APB1 --> APB1_PERIPH + AHB1 --> APB2 --> APB2_PERIPH + APB2 --> GPIO + APB2 --> ADC + AHB1 --> DMA + AHB1 --> Flash ``` 我们逐层来看这棵树。 diff --git a/documents/vol8-domains/embedded/02-button/07-debounce-state-machine.md b/documents/vol8-domains/embedded/02-button/07-debounce-state-machine.md index 98911eea7..a3f0f8e15 100644 --- a/documents/vol8-domains/embedded/02-button/07-debounce-state-machine.md +++ b/documents/vol8-domains/embedded/02-button/07-debounce-state-machine.md @@ -59,29 +59,37 @@ enum class State { ### 状态转换图 -```text - ┌──────────────────────────────────────────────────┐ - │ │ - ▼ │ -┌──────────┐ 按下 ┌──────────────┐ 稳定 ┌─────────┐ 释放 ┌────────────────┐ -│ Idle │───────→│DebouncingPress│───────→│ Pressed │───────→│DebouncingRelease│ -│ (松开中) │←───────│ (消抖中) │ │(按住中) │←───────│ (消抖中) │ -└──────────┘ 反弹 └──────────────┘ └─────────┘ 反弹 └────────────────┘ - ↑ │ - │ 确认释放 │ 稳定 - └───────────────────────────────────────────────────────┘ - -启动路径(上电时按钮已按住): -┌──────────┐ ┌──────────────┐ ┌───────────────────────┐ -│ BootSync │──按下──→│ BootPressed │──释放──→│ BootReleaseDebouncing │ -│ (初始同步)│ │ (启动锁定中) │ │ (启动释放消抖) │ -└──────────┘ └──────────────┘ └───────────────────────┘ - │ 稳定 - ▼ - ┌──────────┐ - │ Idle │ - │ (解锁,无事件)│ - └──────────┘ +```mermaid +stateDiagram-v2 + state "核心路径" as Core { + direction LR + Idle: Idle(松开中) + DebouncingPress: DebouncingPress(消抖中) + Pressed: Pressed(按住中) + DebouncingRelease: DebouncingRelease(消抖中) + + [*] --> Idle + Idle --> DebouncingPress : 检测到按下 + DebouncingPress --> Idle : 信号反弹 + DebouncingPress --> Pressed : 稳定确认 + Pressed --> DebouncingRelease : 检测到释放 + DebouncingRelease --> Pressed : 信号反弹 + DebouncingRelease --> Idle : 确认释放\n(触发 Released 事件) + } + + state "启动路径(上电时按钮已按住)" as Boot { + direction LR + BootSync: BootSync(初始同步) + BootPressed: BootPressed(启动锁定中) + BootReleaseDebouncing: BootReleaseDebouncing(启动释放消抖) + + BootSync --> BootPressed : 检测到按下\n(设置 boot_locked) + BootSync --> Idle : 检测到松开 + BootPressed --> BootReleaseDebouncing : 检测到释放 + BootReleaseDebouncing --> Idle : 稳定确认\n(解锁,无事件) + } + + [*] --> BootSync ``` --- diff --git a/documents/vol8-domains/embedded/02-button/09-cpp-variant-and-visit.md b/documents/vol8-domains/embedded/02-button/09-cpp-variant-and-visit.md index 65b8f1829..f44329494 100644 --- a/documents/vol8-domains/embedded/02-button/09-cpp-variant-and-visit.md +++ b/documents/vol8-domains/embedded/02-button/09-cpp-variant-and-visit.md @@ -208,11 +208,13 @@ struct Pressed : ButtonEvent { void handle() override { /* ... */ } }; `std::variant` 的内存布局: -```text -┌──────────┬──────────┐ -│ tag (1B) │ payload │ -│ 0 或 1 │ (空) │ -└──────────┴──────────┘ +```mermaid +graph LR + subgraph "std::variant<Pressed, Released> 内存布局" + TAG["tag (1B)\n0 = Pressed\n1 = Released"] + PAYLOAD["payload\n(空结构体,无额外空间)"] + end + TAG --- PAYLOAD ``` 由于 `Pressed` 和 `Released` 都是空结构体(`sizeof = 1`),`variant` 只需要一个 tag 字节来标识当前持有哪个类型。加上对齐,`sizeof(ButtonEvent)` 通常是 2 字节。 diff --git a/documents/vol8-domains/embedded/03-uart/12-command-processor-and-main-walkthrough.md b/documents/vol8-domains/embedded/03-uart/12-command-processor-and-main-walkthrough.md index ee14b0e8c..46e05ebcb 100644 --- a/documents/vol8-domains/embedded/03-uart/12-command-processor-and-main-walkthrough.md +++ b/documents/vol8-domains/embedded/03-uart/12-command-processor-and-main-walkthrough.md @@ -130,24 +130,19 @@ int main() { `main()` 的前半部分是初始化,按严格的顺序执行: -```text -HAL_Init() ← HAL 库初始化(SysTick 等) - ↓ -ClockConfig::instance().setup... ← 系统时钟配置(64 MHz HSI) - ↓ -LED led ← LED 对象构造(零开销) - ↓ -Button button ← Button 对象构造(零开销) - ↓ -Logger::driver().set_gpio_init(...) ← 注册 GPIO 初始化回调 - ↓ -Logger::driver().init(UartConfig) ← 使能时钟 → GPIO → HAL init - ↓ -Logger::driver().enable_interrupt() ← NVIC 使能 USART1 中断 - ↓ -send_string("UART Logger Ready!") ← 阻塞式发送欢迎信息 - ↓ -uart_start_receive() ← 启动中断接收流水线 +```mermaid +graph TD + A["HAL_Init()\nHAL 库初始化(SysTick 等)"] + B["ClockConfig::instance().setup...\n系统时钟配置(64 MHz HSI)"] + C["LED<Port::C, PIN_13> led\nLED 对象构造(零开销)"] + D["Button<Port::A, PIN_0> button\nButton 对象构造(零开销)"] + E["Logger::driver().set_gpio_init(...)\n注册 GPIO 初始化回调"] + F["Logger::driver().init(UartConfig)\n使能时钟 → GPIO → HAL init"] + G["Logger::driver().enable_interrupt()\nNVIC 使能 USART1 中断"] + H["send_string(\"UART Logger Ready!\")\n阻塞式发送欢迎信息"] + I["uart_start_receive()\n启动中断接收流水线"] + + A --> B --> C --> D --> E --> F --> G --> H --> I ``` 每一步的顺序都不能调换。时钟没配就调 HAL 函数会 hard fault。GPIO 没配好 USART 信号到不了引脚。中断没使能就启动接收的话,字节到了也不会触发 ISR。`send_string` 放在 `uart_start_receive` 之前是故意的——先发欢迎信息确认发送链路正常,再启动接收。 @@ -246,20 +241,32 @@ static void handle_command(std::string_view cmd, 把所有数据流画在一起,整个系统的架构是这样的: -```text -┌─────────┐ TX (PA9) ┌────────────┐ USB ┌─────┐ -│ │─────────────→│ USB-TTL │───────→│ PC │ -│ STM32 │ │ 适配器 │ │终端 │ -│ │←─────────────│ │←───────│ │ -└─────────┘ RX (PA10) └────────────┘ USB └─────┘ - │ - │ 按钮事件 → send_string("Button pressed!") - │ 命令响应 → send_string("OK: LED ON") - │ - │ 中断接收 → rx_ring → 行解析 → handle_command → led.on() - │ - ├── PC13 (LED) - └── PA0 (Button) +```mermaid +graph LR + subgraph STM32["STM32"] + TX["TX (PA9)"] + RX["RX (PA10)"] + LED["PC13 (LED)"] + BTN["PA0 (Button)"] + end + + subgraph ADAPTER["USB-TTL 适配器"] + TTL_TX["TTL TX"] + TTL_RX["TTL RX"] + end + + subgraph PC["PC 终端"] + PC_TX["USB TX"] + PC_RX["USB RX"] + end + + TX -->|"按钮事件 / 命令响应"| TTL_RX + TTL_RX -->|"USB"| PC_RX + PC_TX -->|"USB"| TTL_TX + TTL_TX -->|"命令输入"| RX + + BTN -.->|"poll_events()"| TX + RX -.->|"rx_ring → 行解析\n→ handle_command"| LED ``` 芯片 → PC 方向:按钮事件和命令响应通过 `send_string()` 发出。这些调用使用阻塞式发送(`HAL_UART_Transmit`),因为发送量小(几十字节),阻塞时间可控(不到 1 毫秒),对系统响应没有影响。 diff --git a/documents/vol8-domains/index.md b/documents/vol8-domains/index.md index d496bea72..201ce0beb 100644 --- a/documents/vol8-domains/index.md +++ b/documents/vol8-domains/index.md @@ -14,13 +14,14 @@ tags: ## 概述 -本卷覆盖现代 C++ 在各领域的实际应用,包含五个子领域: +本卷覆盖现代 C++ 在各领域的实际应用,包含六个子领域: - **嵌入式开发**:资源约束、零开销抽象、外设编程、RTOS、STM32 实战 - **网络编程**:Socket、HTTP、异步 I/O、WebSocket、RPC - **GUI 与图形**:图形基础、最小 GUI 框架、ImGui、2D/3D 渲染 - **数据存储**:序列化、文件格式、SQLite、键值存储 - **算法与数据结构**:复杂度分析、经典算法、高级数据结构 +- **C++ 深度专题**:横跨多领域的语言机制深度探讨(指针语义、弱引用设计等) 预计 80-100 篇文章。 @@ -32,4 +33,5 @@ tags: GUI 与图形 — 规划中 数据存储 — 规划中 算法与数据结构 — 规划中 + C++ 深度专题 diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-5-once-callback-then-chaining.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-5-once-callback-then-chaining.md index 485637bff..f575c6c90 100644 --- a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-5-once-callback-then-chaining.md +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-5-once-callback-then-chaining.md @@ -70,8 +70,9 @@ int result = std::move(pipeline).run(3, 4); // result == 14 整个所有权链条是这样的: -```text -新 OnceCallback → move_only_function → lambda 闭包 → [原回调 + 后续回调] +```mermaid +graph LR + A["新 OnceCallback"] --> B["move_only_function"] --> C["lambda 闭包"] --> D["原回调 + 后续回调"] ``` 每一层都通过移动语义传递所有权,没有任何共享或拷贝。这就是 move-only 语义在 `then()` 中的完整体现。 diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-03-once-callback-lambda-advanced.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-03-once-callback-lambda-advanced.md index 20a3209da..d4bc35ab8 100644 --- a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-03-once-callback-lambda-advanced.md +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-03-once-callback-lambda-advanced.md @@ -129,8 +129,9 @@ cont = std::forward(next) 把这两件捕获放在一起看,`then()` 创建的新 lambda 持有了原回调和后续回调的**完整所有权**。这个 lambda 又被存入一个新的 `OnceCallback` 的 `std::move_only_function` 里。整个所有权链条是这样的: -```text -新 OnceCallback -> move_only_function -> lambda 闭包 -> [原 OnceCallback + 后续回调] +```mermaid +graph LR + A["新 OnceCallback"] --> B["move_only_function"] --> C["lambda 闭包"] --> D["原 OnceCallback + 后续回调"] ``` 每一层都通过移动语义传递所有权,没有任何共享或拷贝。这就是 OnceCallback 的 move-only 语义在 `then()` 中的完整体现——所有权从外到内层层传递,没有破绽。 diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-05-once-callback-move-only-function.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-05-once-callback-move-only-function.md index 1f7f17548..cbcb3e9ff 100644 --- a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-05-once-callback-move-only-function.md +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-05-once-callback-move-only-function.md @@ -160,19 +160,18 @@ f = nullptr; // 清空 f,析构之前持有的可调用对象 `std::move_only_function`(和 `std::function` 一样)内部实现了**小对象优化**(Small Buffer Optimization,SBO)。思路很简单:对象内部预留一块固定大小的缓冲区(通常是几个指针大小),如果可调用对象足够小,就把它直接存到缓冲区里,避免堆分配;如果太大,就在堆上分配内存来存储。 -```text -┌──────────────────────────────────┐ -│ std::move_only_function │ -│ ┌──────────────────────────────┐ │ -│ │ 函数指针/虚表指针 │ │ ← 用于类型擦除的调用分派 -│ ├──────────────────────────────┤ │ -│ │ SBO 缓冲区(通常 16-32 字节)│ │ ← 小对象直接存这里 -│ └──────────────────────────────┘ │ -│ 或 │ -│ ┌──────────────────────────────┐ │ -│ │ 堆指针(指向动态分配的对象) │ │ ← 大对象存在堆上 -│ └──────────────────────────────┘ │ -└──────────────────────────────────┘ +```mermaid +graph TD + subgraph outer["std::move_only_function"] + direction TB + subgraph sbo_path["SBO 路径:小对象直接内联存储"] + vtable["函数指针 / 虚表指针
用于类型擦除的调用分派"] + sbo_buf["SBO 缓冲区(通常 16-32 字节)
小对象直接存这里"] + end + subgraph heap_path["堆路径:大对象动态分配"] + heap_ptr["堆指针(指向动态分配的对象)
大对象存在堆上"] + end + end ``` SBO 的阈值是实现定义的——通常在 2-3 个指针大小(16-24 字节)左右。捕获少量参数的 lambda(比如 `[x = 42]` 或 `[&ref]`)通常能放进 SBO,不会触发堆分配。但如果 lambda 捕获了大量数据(比如一个 `std::string` + 几个 `int`),超过了 SBO 阈值,构造时就会在堆上分配。 diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/02-once-callback-implementation.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/02-once-callback-implementation.md index bbddec413..699307873 100644 --- a/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/02-once-callback-implementation.md +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/02-once-callback-implementation.md @@ -328,8 +328,9 @@ public: 串联后的新回调需要持有原回调和后续回调的**所有权**——否则原回调可能在外部被提前消费掉,管道就断了。而 `OnceCallback` 是 move-only 的,这意味着 `then()` 必须消费 `*this`(原回调)和 `next`(后续回调),把两者的所有权转移到一个新的 lambda 闭包里。整个所有权链条是这样的: -```text -新回调 → move_only_function → lambda 闭包 → [原回调 + 后续回调] +```mermaid +graph LR + A["新回调"] --> B["move_only_function"] --> C["lambda 闭包"] --> D["原回调 + 后续回调"] ``` 实现思路的骨架大概是这样: diff --git a/package.json b/package.json index f0a64e72c..e6f8aef8c 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "devDependencies": { "@types/node": "^25.6.2", "markdown-it-mathjax3": "^4.3.2", + "mermaid": "^10.9.6", "tsx": "^4.21.0", "vitepress": "^1.6.4", "vue": "^3.5.34" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8928f66e8..0e38ed945 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: markdown-it-mathjax3: specifier: ^4.3.2 version: 4.3.2 + mermaid: + specifier: ^10.9.6 + version: 10.9.6 tsx: specifier: ^4.21.0 version: 4.21.0 @@ -123,6 +126,9 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@braintree/sanitize-url@6.0.4': + resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} + '@docsearch/css@3.8.2': resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} @@ -611,6 +617,18 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -623,15 +641,27 @@ packages: '@types/markdown-it@14.1.2': resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@3.0.15': + resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@25.6.2': resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -763,6 +793,9 @@ packages: character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + cheerio-select@1.6.0: resolution: {integrity: sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==} @@ -781,10 +814,21 @@ packages: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} @@ -795,6 +839,175 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.33.4: + resolution: {integrity: sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.13: + resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} + + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -802,6 +1015,10 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff@5.2.2: + resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==} + engines: {node: '>=0.3.1'} + dom-serializer@1.4.1: resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} @@ -816,9 +1033,15 @@ packages: resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} engines: {node: '>= 4'} + dompurify@3.4.5: + resolution: {integrity: sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==} + domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + elkjs@0.9.3: + resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} + emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} @@ -879,6 +1102,17 @@ packages: htmlparser2@6.1.0: resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-what@5.5.0: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} @@ -888,6 +1122,23 @@ packages: engines: {node: '>=10.0.0'} hasBin: true + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -901,30 +1152,102 @@ packages: resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} deprecated: Version 4 replaces this package with the scoped package @mathjax/src + mdast-util-from-markdown@1.3.1: + resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} + mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mdast-util-to-string@3.2.0: + resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} + mensch@0.3.4: resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==} + mermaid@10.9.6: + resolution: {integrity: sha512-XRjjRaI4aPCAMpVaOhxIwLYdx3U4Cb6mN0M268ggFAfFRqsvyFW8zxWbEZazN/mPkqsVWThb0oa1UawWK+XMNg==} + mhchemparser@4.2.1: resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} + micromark-core-commonmark@1.1.0: + resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} + + micromark-factory-destination@1.1.0: + resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} + + micromark-factory-label@1.1.0: + resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==} + + micromark-factory-space@1.1.0: + resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} + + micromark-factory-title@1.1.0: + resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==} + + micromark-factory-whitespace@1.1.0: + resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==} + + micromark-util-character@1.2.0: + resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} + micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + micromark-util-chunked@1.1.0: + resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} + + micromark-util-classify-character@1.1.0: + resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==} + + micromark-util-combine-extensions@1.1.0: + resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==} + + micromark-util-decode-numeric-character-reference@1.1.0: + resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==} + + micromark-util-decode-string@1.1.0: + resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==} + + micromark-util-encode@1.1.0: + resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} + micromark-util-encode@2.0.1: resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + micromark-util-html-tag-name@1.2.0: + resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} + + micromark-util-normalize-identifier@1.1.0: + resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==} + + micromark-util-resolve-all@1.1.0: + resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==} + + micromark-util-sanitize-uri@1.2.0: + resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} + micromark-util-sanitize-uri@2.0.1: resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + micromark-util-subtokenize@1.1.0: + resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} + + micromark-util-symbol@1.1.0: + resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} + micromark-util-symbol@2.0.1: resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + micromark-util-types@1.1.0: + resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} + micromark-util-types@2.0.2: resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + micromark@3.2.0: + resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} + mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} engines: {node: '>=4.0.0'} @@ -939,6 +1262,13 @@ packages: mj-context-menu@0.6.1: resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -953,6 +1283,9 @@ packages: encoding: optional: true + non-layered-tidy-tree-layout@2.0.2: + resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -996,11 +1329,24 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + rollup@4.60.3: resolution: {integrity: sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + search-insights@2.17.3: resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} @@ -1028,6 +1374,9 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + superjson@2.2.6: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} @@ -1041,6 +1390,10 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1058,6 +1411,9 @@ packages: unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + unist-util-stringify-position@3.0.3: + resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} + unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} @@ -1067,6 +1423,15 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + hasBin: true + + uvu@0.5.6: + resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} + engines: {node: '>=8'} + hasBin: true + valid-data-url@3.0.1: resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==} engines: {node: '>=10'} @@ -1132,6 +1497,9 @@ packages: resolution: {integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==} engines: {node: '>=10.0.0'} + web-worker@1.5.0: + resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -1271,6 +1639,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@braintree/sanitize-url@6.0.4': {} + '@docsearch/css@3.8.2': {} '@docsearch/js@3.8.2(@algolia/client-search@5.52.1)(search-insights@2.17.3)': @@ -1565,6 +1935,18 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-time@3.0.4': {} + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/estree@1.0.8': {} '@types/hast@3.0.4': @@ -1578,16 +1960,27 @@ snapshots: '@types/linkify-it': 5.0.0 '@types/mdurl': 2.0.0 + '@types/mdast@3.0.15': + dependencies: + '@types/unist': 2.0.11 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 '@types/mdurl@2.0.0': {} + '@types/ms@2.1.0': {} + '@types/node@25.6.2': dependencies: undici-types: 7.19.2 + '@types/trusted-types@2.0.7': + optional: true + + '@types/unist@2.0.11': {} + '@types/unist@3.0.3': {} '@types/web-bluetooth@0.0.21': {} @@ -1729,6 +2122,8 @@ snapshots: character-entities-legacy@3.0.0: {} + character-entities@2.0.2: {} + cheerio-select@1.6.0: dependencies: css-select: 4.3.0 @@ -1753,10 +2148,18 @@ snapshots: commander@6.2.1: {} + commander@7.2.0: {} + + commander@8.3.0: {} + copy-anything@4.0.5: dependencies: is-what: 5.5.0 + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + css-select@4.3.0: dependencies: boolbase: 1.0.0 @@ -1769,12 +2172,207 @@ snapshots: csstype@3.2.3: {} + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.4): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.33.4 + + cytoscape@3.33.4: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.13: + dependencies: + d3: 7.9.0 + lodash-es: 4.18.1 + + dayjs@1.11.20: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + dequal@2.0.3: {} devlop@1.1.0: dependencies: dequal: 2.0.3 + diff@5.2.2: {} + dom-serializer@1.4.1: dependencies: domelementtype: 2.3.0 @@ -1791,12 +2389,18 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.4.5: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@2.8.0: dependencies: dom-serializer: 1.4.1 domelementtype: 2.3.0 domhandler: 4.3.1 + elkjs@0.9.3: {} + emoji-regex-xs@1.0.0: {} entities@2.2.0: {} @@ -1911,6 +2515,14 @@ snapshots: domutils: 2.8.0 entities: 2.2.0 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + internmap@1.0.1: {} + + internmap@2.0.3: {} + is-what@5.5.0: {} juice@8.1.0: @@ -1923,6 +2535,18 @@ snapshots: transitivePeerDependencies: - encoding + katex@0.16.47: + dependencies: + commander: 8.3.0 + + khroma@2.1.0: {} + + kleur@4.1.5: {} + + layout-base@1.0.2: {} + + lodash-es@4.18.1: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1943,6 +2567,23 @@ snapshots: mj-context-menu: 0.6.1 speech-rule-engine: 4.1.4 + mdast-util-from-markdown@1.3.1: + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + decode-named-character-reference: 1.3.0 + mdast-util-to-string: 3.2.0 + micromark: 3.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-decode-string: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-stringify-position: 3.0.3 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.4 @@ -1955,27 +2596,189 @@ snapshots: unist-util-visit: 5.1.0 vfile: 6.0.3 + mdast-util-to-string@3.2.0: + dependencies: + '@types/mdast': 3.0.15 + mensch@0.3.4: {} + mermaid@10.9.6: + dependencies: + '@braintree/sanitize-url': 6.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + cytoscape: 3.33.4 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.4) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.13 + dayjs: 1.11.20 + dompurify: 3.4.5 + elkjs: 0.9.3 + katex: 0.16.47 + khroma: 2.1.0 + lodash-es: 4.18.1 + mdast-util-from-markdown: 1.3.1 + non-layered-tidy-tree-layout: 2.0.2 + stylis: 4.4.0 + ts-dedent: 2.2.0 + uuid: 14.0.0 + web-worker: 1.5.0 + transitivePeerDependencies: + - supports-color + mhchemparser@4.2.1: {} + micromark-core-commonmark@1.1.0: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-factory-destination: 1.1.0 + micromark-factory-label: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-factory-title: 1.1.0 + micromark-factory-whitespace: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-classify-character: 1.1.0 + micromark-util-html-tag-name: 1.2.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-factory-destination@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-label@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-factory-space@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-types: 1.1.0 + + micromark-factory-title@1.1.0: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-whitespace@1.1.0: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-character@1.2.0: + dependencies: + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + micromark-util-character@2.1.1: dependencies: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 + micromark-util-chunked@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-classify-character@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-combine-extensions@1.1.0: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-decode-numeric-character-reference@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-decode-string@1.1.0: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 1.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-symbol: 1.1.0 + + micromark-util-encode@1.1.0: {} + micromark-util-encode@2.0.1: {} + micromark-util-html-tag-name@1.2.0: {} + + micromark-util-normalize-identifier@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-resolve-all@1.1.0: + dependencies: + micromark-util-types: 1.1.0 + + micromark-util-sanitize-uri@1.2.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-encode: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-sanitize-uri@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-encode: 2.0.1 micromark-util-symbol: 2.0.1 + micromark-util-subtokenize@1.1.0: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-util-symbol@1.1.0: {} + micromark-util-symbol@2.0.1: {} + micromark-util-types@1.1.0: {} + micromark-util-types@2.0.2: {} + micromark@3.2.0: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + micromark-core-commonmark: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-combine-extensions: 1.1.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-encode: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + mime@2.6.0: {} minisearch@7.2.0: {} @@ -1984,12 +2787,18 @@ snapshots: mj-context-menu@0.6.1: {} + mri@1.2.0: {} + + ms@2.1.3: {} + nanoid@3.3.12: {} node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 + non-layered-tidy-tree-layout@2.0.2: {} + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -2034,6 +2843,8 @@ snapshots: rfdc@1.4.1: {} + robust-predicates@3.0.3: {} + rollup@4.60.3: dependencies: '@types/estree': 1.0.8 @@ -2065,6 +2876,14 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.3 fsevents: 2.3.3 + rw@1.3.3: {} + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + safer-buffer@2.1.2: {} + search-insights@2.17.3: {} shiki@2.5.0: @@ -2097,6 +2916,8 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + stylis@4.4.0: {} + superjson@2.2.6: dependencies: copy-anything: 4.0.5 @@ -2107,6 +2928,8 @@ snapshots: trim-lines@3.0.1: {} + ts-dedent@2.2.0: {} + tslib@2.8.1: {} tsx@4.21.0: @@ -2126,6 +2949,10 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-stringify-position@3.0.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 @@ -2141,6 +2968,15 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + uuid@14.0.0: {} + + uvu@0.5.6: + dependencies: + dequal: 2.0.3 + diff: 5.2.2 + kleur: 4.1.5 + sade: 1.8.1 + valid-data-url@3.0.1: {} vfile-message@4.0.3: @@ -2231,6 +3067,8 @@ snapshots: transitivePeerDependencies: - encoding + web-worker@1.5.0: {} + webidl-conversions@3.0.1: {} whatwg-url@5.0.0: diff --git a/scripts/validate_frontmatter.py b/scripts/validate_frontmatter.py index b29d6997e..ceb6c17a9 100755 --- a/scripts/validate_frontmatter.py +++ b/scripts/validate_frontmatter.py @@ -77,6 +77,8 @@ def __init__(self, tutorial_dir: Path): 'with_warnings': 0 } + _yaml_warning_printed = False + def parse_frontmatter(self, content: str) -> Tuple[Dict, bool]: """Parse YAML frontmatter from markdown content.""" match = re.match(r'^---\s*\n(.*?)\n---\s*\n(.*)', content, re.DOTALL) @@ -88,9 +90,18 @@ def parse_frontmatter(self, content: str) -> Tuple[Dict, bool]: frontmatter = yaml.safe_load(match.group(1)) return frontmatter if frontmatter else {}, True except ImportError: - print("Warning: PyYAML not installed, skipping YAML validation") + if not self._yaml_warning_printed: + self._yaml_warning_printed = True + venv_python = Path(__file__).parent.parent / '.venv' / 'bin' / 'python' + print( + "Warning: PyYAML not installed — YAML frontmatter validation skipped.\n" + " Install it with:\n" + f" {venv_python} -m pip install pyyaml\n" + " Or if no venv exists yet:\n" + " python -m venv .venv && .venv/bin/pip install pyyaml" + ) return {}, True - except yaml.YAMLError as e: + except Exception as e: return {}, False def validate_field_types(self, frontmatter: Dict, filepath: Path): diff --git a/site/.vitepress/config/index.ts b/site/.vitepress/config/index.ts index d3c54ec79..1910bb738 100644 --- a/site/.vitepress/config/index.ts +++ b/site/.vitepress/config/index.ts @@ -3,8 +3,15 @@ import { navZh, navEn } from './nav' import { buildSidebar } from './sidebar' import { kbdPlugin } from '../plugins/kbd-plugin' import { cppTemplateEscapePlugin } from '../plugins/escape-cpp-templates' +import { mermaidPlugin } from '../plugins/mermaid-plugin' export default defineConfig({ + vite: { + ssr: { + external: ['mermaid'], + }, + }, + srcDir: '../documents', title: '现代 C++ 教程', @@ -59,6 +66,7 @@ export default defineConfig({ config(md) { cppTemplateEscapePlugin(md) md.use(kbdPlugin) + md.use(mermaidPlugin) }, }, diff --git a/site/.vitepress/config/shared.ts b/site/.vitepress/config/shared.ts index 2b26a86f2..63e39de27 100644 --- a/site/.vitepress/config/shared.ts +++ b/site/.vitepress/config/shared.ts @@ -2,6 +2,7 @@ import type { DefaultTheme } from 'vitepress' import { navZh, navEn } from './nav' import { kbdPlugin } from '../plugins/kbd-plugin' import { cppTemplateEscapePlugin } from '../plugins/escape-cpp-templates' +import { mermaidPlugin } from '../plugins/mermaid-plugin' export const sharedBase = { base: '/Tutorial_AwesomeModernCPP/', @@ -36,6 +37,7 @@ export const sharedBase = { config(md) { cppTemplateEscapePlugin(md) md.use(kbdPlugin) + md.use(mermaidPlugin) }, }, } diff --git a/site/.vitepress/plugins/mermaid-plugin.ts b/site/.vitepress/plugins/mermaid-plugin.ts new file mode 100644 index 000000000..c41d3bd9f --- /dev/null +++ b/site/.vitepress/plugins/mermaid-plugin.ts @@ -0,0 +1,24 @@ +import type { PluginSimple } from 'markdown-it' +import type MarkdownIt from 'markdown-it' + +export const mermaidPlugin: PluginSimple = (md: MarkdownIt) => { + // Change mermaid fence tokens to a custom type so Shiki never sees them. + // Core rules run after tokenization but before rendering, so this works + // regardless of when VitePress applies Shiki's fence renderer override. + md.core.ruler.push('mermaid_block', (state) => { + for (let i = 0; i < state.tokens.length; i++) { + const token = state.tokens[i] + if (token.type === 'fence' && token.info.trim() === 'mermaid') { + token.type = 'mermaid_diagram' + token.tag = '' + token.nesting = 0 + } + } + return true + }) + + md.renderer.rules.mermaid_diagram = (tokens, idx) => { + const encoded = encodeURIComponent(tokens[idx].content.trim()) + return `
` + } +} diff --git a/site/.vitepress/theme/custom.css b/site/.vitepress/theme/custom.css index 1e5fd7fd8..d5a77da55 100644 --- a/site/.vitepress/theme/custom.css +++ b/site/.vitepress/theme/custom.css @@ -780,6 +780,29 @@ } } +/* ================================================================ + Mermaid Diagrams + ================================================================ */ + +.mermaid-diagram { + margin: 1rem 0; + overflow-x: auto; + text-align: center; +} + +.mermaid-diagram svg { + max-width: 100%; + height: auto; +} + +.mermaid-error { + padding: 12px; + border: 1px solid var(--vp-c-danger-2); + border-radius: 8px; + overflow-x: auto; + font-size: 0.85em; +} + /* ================================================================ Content Page Enhancements ================================================================ */ diff --git a/site/.vitepress/theme/index.ts b/site/.vitepress/theme/index.ts index b0e319d32..786bc9b59 100644 --- a/site/.vitepress/theme/index.ts +++ b/site/.vitepress/theme/index.ts @@ -9,6 +9,7 @@ import RefLink from './components/RefLink.vue' import ReferenceCard from './components/ReferenceCard.vue' import ReferenceItem from './components/ReferenceItem.vue' import OnlineCompilerDemo from './components/OnlineCompilerDemo.vue' +import { setupMermaid } from './mermaid-client' import './custom.css' export default { @@ -18,6 +19,9 @@ export default { 'home-features-before': () => h(HomeTipBanner) }) }, + setup() { + setupMermaid() + }, enhanceApp({ app }) { app.component('ChapterNav', ChapterNav) app.component('ChapterLink', ChapterLink) diff --git a/site/.vitepress/theme/mermaid-client.ts b/site/.vitepress/theme/mermaid-client.ts new file mode 100644 index 000000000..780a1a634 --- /dev/null +++ b/site/.vitepress/theme/mermaid-client.ts @@ -0,0 +1,100 @@ +import { nextTick, onMounted } from 'vue' +import { useRouter } from 'vitepress' + +declare global { + interface Window { + mermaid?: { + initialize: (config: Record) => void + render: (id: string, text: string) => Promise<{ svg: string; bindFunctions?: (el: Element) => void }> + } + __mermaidLoadingPromise__?: Promise + __mermaidInitialized__?: boolean + } +} + +const MERMAID_CDN = 'https://cdn.jsdelivr.net/npm/mermaid@10.9.6/dist/mermaid.min.js' + +function loadMermaid(): Promise { + if (typeof window === 'undefined') return Promise.resolve() + + if (window.mermaid) { + initMermaid() + return Promise.resolve() + } + + if (window.__mermaidLoadingPromise__) return window.__mermaidLoadingPromise__ + + window.__mermaidLoadingPromise__ = new Promise((resolve, reject) => { + const existing = document.querySelector('script[data-mermaid-runtime]') + if (existing) { + existing.addEventListener('load', () => { initMermaid(); resolve() }) + existing.addEventListener('error', () => reject(new Error('Failed to load Mermaid'))) + return + } + + const script = document.createElement('script') + script.src = MERMAID_CDN + script.async = true + script.dataset.mermaidRuntime = 'true' + script.onload = () => { initMermaid(); resolve() } + script.onerror = () => reject(new Error(`Failed to load Mermaid from ${MERMAID_CDN}`)) + document.head.appendChild(script) + }) + + return window.__mermaidLoadingPromise__ +} + +function initMermaid() { + if (!window.mermaid || window.__mermaidInitialized__) return + window.mermaid.initialize({ + startOnLoad: false, + securityLevel: 'loose', + theme: 'default', + }) + window.__mermaidInitialized__ = true +} + +async function renderMermaidDiagrams() { + if (typeof window === 'undefined') return + + await loadMermaid() + await nextTick() + await new Promise((r) => requestAnimationFrame(() => r())) + + const mermaid = window.mermaid + if (!mermaid) return + + const nodes = Array.from( + document.querySelectorAll('.mermaid-diagram[data-rendered="false"]') + ) + + for (let i = 0; i < nodes.length; i++) { + const el = nodes[i] + const raw = el.dataset.mermaid + if (!raw) continue + + const source = decodeURIComponent(raw) + const id = `mermaid-${Date.now()}-${i}-${Math.random().toString(36).slice(2, 8)}` + + try { + const { svg } = await mermaid.render(id, source) + el.innerHTML = svg + el.dataset.rendered = 'true' + } catch { + el.dataset.rendered = 'error' + el.innerHTML = `
${escapeHtml(source)}
` + } + } +} + +function escapeHtml(s: string) { + return s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>') + .replaceAll('"', '"').replaceAll("'", ''') +} + +export function setupMermaid() { + const router = useRouter() + + onMounted(() => renderMermaidDiagrams()) + router.onAfterRouteChange = () => renderMermaidDiagrams() +}