From 2549e706c4d5b4c58caed8ca08ed47b0ae5af045 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Sun, 7 Jun 2026 11:42:43 +0800 Subject: [PATCH] translation: translate the changed documents --- documents/en/team/index.md | 42 +++++---- .../ch01-smart-pointers/03-shared-ptr.md | 88 +++++++++---------- .../ch04-type-safety/03-variant.md | 57 ++++++------ 3 files changed, 101 insertions(+), 86 deletions(-) diff --git a/documents/en/team/index.md b/documents/en/team/index.md index a4ea3d7ea..fde65c809 100644 --- a/documents/en/team/index.md +++ b/documents/en/team/index.md @@ -3,10 +3,10 @@ title: Contributors description: Thanks to everyone who contributed to this project. translation: source: documents/team/index.md - source_hash: 0bee5b2fa7c792b86fec134255c2063143fdbbb4cada2277d792a67247f078fa - translated_at: '2026-05-26T10:20:47.220977+00:00' + source_hash: d57d71c8f25587ebd62a051a3d996f535b2be153012ac592658ba2011489ce45 + translated_at: '2026-06-07T02:11:01.601674+00:00' engine: anthropic - token_count: 449 + token_count: 961 --- # Contributors @@ -18,19 +18,21 @@ Thank you to everyone who has contributed to this project! Whether through code, |:----:|------|-------------| | 🎨 | Interface Design | UI/UX design, visual optimization | | 🖼️ | Illustrations | Tutorial diagrams, architecture diagrams, flowcharts | -| 🐛 | Bug Reports | Reporting errors via Issues, WeChat, QQ, email, or other channels | -| 📝 | Content Suggestions | Providing content improvement suggestions, learning path optimization | +| 🐛 | Bug Reports | Reporting errors via Issues, WeChat, QQ, email, etc. | +| 📝 | Content Suggestions | Providing content improvement ideas, learning path optimization | | 🔍 | Review | Content review, technical proofreading | -| 🌍 | Translation | English translation or translation into other languages | +| 🌍 | Translation | English translation or other language translations | | 💡 | Code Examples | Providing code examples or improvement suggestions | | 📢 | Promotion | Helping to spread the word about the project | ## Contributor List -::: info About Anonymous Contributors +::: info About anonymous contributors Some contributors have chosen to remain anonymous or use pseudonyms for privacy reasons. We are equally grateful to them. ::: +The main maintainers are as follows: +
Charliechen @@ -41,11 +43,16 @@ Some contributors have chosen to remain anonymous or use pseudonyms for privacy

Lead writer of the tutorial, resource collection and development, project architecture design, and basic daily maintenance

+ + +A huge thank you to these friends for their PRs and various forms of support! Your help is making this project better and taking it further! (A deep 90-degree bow!) + +
Doll-Attire
Doll-Attire -

UI Design Guidance and Feedback

+

UI Design Guidance & Feedback

🎨 📝

Guided the UI design of this documentation site and provided important suggestions for improving the reading experience

@@ -56,25 +63,30 @@ Some contributors have chosen to remain anonymous or use pseudonyms for privacy YukunJ PR

Content Fixes

🐛 💡

-

Fixed typos in the shared_ptr and variant chapters, improving content accuracy

+

Fixed typos in the shared pointer and variant chapters, improving content accuracy

+ + +The valuable feedback from these folks is equally worth recording. It is your feedback that allows this project to identify real issues and pushes us to improve! (Another deep 90-degree bow!) + +
Leon19960120
Leon19960120 Issue

Bug Reports

🐛

-

Reported 404 pages and text display issues

+

Reported 404 page issues and text display anomalies

REvolution2
REvolution2 Issue -

Bug Reports · Content Suggestions

+

Bug Reports & Content Suggestions

🐛 📝

-

Raised questions and discussions about C++ character literal types

+

Raised questions and discussions regarding C++ character literal types

@@ -83,7 +95,7 @@ Some contributors have chosen to remain anonymous or use pseudonyms for privacy L-Super Issue

Bug Reports

🐛 💡

-

Reported expected example compilation failure

+

Reported compilation failure in the expected example

@@ -92,8 +104,8 @@ Some contributors have chosen to remain anonymous or use pseudonyms for privacy We welcome contributions in all forms! These include, but are not limited to: -- **Submitting Issues**: Finding errors or inaccuracies -- **Content Suggestions**: Topics you would like to see added or improvement suggestions +- **Submitting Issues**: Spotting errors or inaccuracies +- **Content Suggestions**: Topics you would like to see added or improvement ideas - **Interface Design**: UI/UX optimization proposals for the documentation site - **Illustrations**: Creating diagrams and flowcharts for the tutorial - **Translation**: Helping to translate tutorial content diff --git a/documents/en/vol2-modern-features/ch01-smart-pointers/03-shared-ptr.md b/documents/en/vol2-modern-features/ch01-smart-pointers/03-shared-ptr.md index eb820ea69..420ce47ef 100644 --- a/documents/en/vol2-modern-features/ch01-smart-pointers/03-shared-ptr.md +++ b/documents/en/vol2-modern-features/ch01-smart-pointers/03-shared-ptr.md @@ -26,16 +26,16 @@ related: - 自定义删除器 translation: source: documents/vol2-modern-features/ch01-smart-pointers/03-shared-ptr.md - source_hash: a799d925944b2310bf0dc27f58be8797d7ae93684134663a67d3d65f21ff1ffc - translated_at: '2026-05-26T11:21:17.720531+00:00' + source_hash: 9114fe8009c863522c68fe9a08d3b76affb519a89d94d0a0cae1eafc03b0146d + translated_at: '2026-06-07T02:13:44.058182+00:00' engine: anthropic - token_count: 4052 + token_count: 4051 --- # A Deep Dive into shared_ptr: Shared Ownership and Reference Counting -In the previous article, we discussed `unique_ptr`—a zero-overhead smart pointer with exclusive ownership. But in the real world, resources aren't always "exclusively owned." Sometimes, an object genuinely needs to be held and managed jointly by multiple modules—such as a configuration object read by multiple subsystems, a network connection shared across tasks, or a cache entry accessed by multiple consumers. In these cases, the "exclusive" semantics of `unique_ptr` fall short. +In the previous article, we discussed `unique_ptr`—the zero-overhead smart pointer for exclusive ownership. But in the real world, resources aren't always exclusively owned by a single master. Sometimes, an object genuinely needs to be held and managed jointly by multiple modules—such as a configuration object read by multiple subsystems, a network connection shared across tasks, or a cache entry accessed by multiple consumers. In these cases, the "exclusive" semantics of `unique_ptr` fall short. -`std::shared_ptr` is designed for exactly this scenario. Its core idea is **reference counting**: each additional `shared_ptr` pointing to the object increments the count; each destruction decrements it; when the count reaches zero, the object is automatically destroyed. It sounds simple and elegant, but the underlying implementation details—control blocks, atomic operations, and memory allocation strategies—are far more complex than one might imagine. +`std::shared_ptr` is designed for exactly this scenario. Its core idea is **reference counting**: every time a new `shared_ptr` points to the object, the count increments by one; every time one is removed, it decrements by one; when the count reaches zero, the object is automatically destroyed. It sounds simple and elegant, but the underlying implementation details—control blocks, atomic operations, and memory allocation strategies—are far more complex than one might imagine. ## Shared Ownership: Semantics and Costs @@ -83,15 +83,15 @@ use_count: 1 Disconnected from 192.168.1.1:8080 ``` -This looks great. But shared ownership isn't free—every copy and destruction of a `shared_ptr` requires updating the reference count, and this count must be thread-safe (using atomic operations). Furthermore, `shared_ptr` internally maintains a control block to store the reference count and other metadata. These overheads become very noticeable in scenarios where `shared_ptr` instances are frequently created and destroyed. +This looks great. But shared ownership isn't free—every copy and destruction of a `shared_ptr` requires updating the reference count, and that count must be thread-safe (via atomic operations). Furthermore, `shared_ptr` internally maintains a control block to store the reference count and other metadata. These overheads become very noticeable in scenarios where `shared_ptr` instances are frequently created and destroyed. -Our recommendation is to use `unique_ptr` whenever possible, and only resort to `shared_ptr` when shared ownership is genuinely needed. `shared_ptr` should not become an excuse for being "too lazy to think about ownership." +Our advice is to use `unique_ptr` whenever possible, and only resort to `shared_ptr` when shared ownership is genuinely needed. `shared_ptr` should not become an excuse for being too lazy to think about ownership. -## The Control Block: Internal Structure of shared_ptr +## The Control Block: The Internal Structure of shared_ptr To understand the performance characteristics of `shared_ptr`, we must first understand its internal structure. A `shared_ptr` actually contains two pointers: one to the managed object, and another to the control block. -The control block is a heap-allocated data structure containing the strong reference count (the number of `shared_ptr` instances), the weak reference count (the number of `weak_ptr` instances), a custom deleter (if any), and a custom allocator (if any). When you create a `shared_ptr` using `std::make_shared`, the object and the control block are placed in the same memory block (a single allocation); when created using `std::shared_ptr(new T)`, the object and the control block are two separate allocations. +The control block is a heap-allocated data structure that contains the strong reference count (the number of `shared_ptr` instances), the weak reference count (the number of `weak_ptr` instances), a custom deleter (if provided), and a custom allocator (if provided). When you create a `shared_ptr` using `std::make_shared`, the object and the control block are placed in a single memory block (one allocation); when created using `std::shared_ptr(new T)`, the object and the control block are two separate allocations. Let's use a simplified diagram to understand this: @@ -121,13 +121,13 @@ std::cout << "sizeof(shared_ptr): " << sizeof(p1) << "\n"; // 16 (64-bit) std::cout << "sizeof(unique_ptr): " << sizeof(std::unique_ptr) << "\n"; // 8 ``` -⚠️ `make_shared` also has a lesser-known drawback: because the object and the control block share the same memory block, when all `shared_ptr` instances are destroyed (strong reference count reaches zero), the object is destructed, but the control block's memory is not immediately freed—the entire memory block is only reclaimed when all `weak_ptr` instances are also destroyed (weak reference count reaches zero). If the object is large and `weak_ptr` instances are still alive, this can result in higher memory usage than expected. If you anticipate `weak_ptr` instances living for a long time, consider using `std::shared_ptr(new T)` to allocate the object's memory independently from the control block, so that the object's memory can be freed immediately when the strong reference count reaches zero. +> ⚠️ `make_shared` also has a lesser-known drawback: because the object and the control block share the same memory block, when all `shared_ptr` instances are destroyed (strong reference count reaches zero), the object is destructed, but the control block's memory is not immediately freed—the entire memory block is only reclaimed when all `weak_ptr` instances are also destroyed (weak reference count reaches zero). If the object is large and `weak_ptr` instances are still alive, this can result in higher memory usage than expected. If you anticipate long-lived `weak_ptr` instances, consider using `std::shared_ptr(new T)` to allocate the object's memory independently from the control block, so that the object's memory can be freed immediately when the strong reference count reaches zero. ## Atomic Operations on Reference Counts and Thread Safety -`shared_ptr` uses atomic operations for its reference count to ensure thread safety. This means that in a multithreaded environment, you can safely copy and destroy the `shared_ptr` instances themselves (the incrementing and decrementing of the reference count is atomic), but **access to the managed object is not protected**—if multiple threads simultaneously read and write to the object itself, you still need to provide your own locking. +`shared_ptr` uses atomic operations for its reference counts to ensure thread safety. This means that in a multithreaded environment, you can safely copy and destroy the `shared_ptr` instances themselves (the incrementing and decrementing of the reference count is atomic), but **access to the managed object is not protected**—if multiple threads are simultaneously reading from and writing to the object itself, you still need to provide your own locking. -This is a common misconception: many people think that `shared_ptr` provides "thread safety for the object," but it actually only guarantees "thread safety for the reference count." We can use cppreference's description to understand this precisely: the control block of a `shared_ptr` is thread-safe—multiple threads can simultaneously operate on different `shared_ptr` instances (even if they point to the same object) without external synchronization. However, the same `shared_ptr` instance cannot be read and written simultaneously by multiple threads (locking is required). Concurrent access to the managed object must be made safe by you. +This is a common misconception: many people assume that `shared_ptr` provides "thread safety for the object," but it actually only guarantees "thread safety for the reference count." We can use cppreference's description to understand this precisely: the control block of a `shared_ptr` is thread-safe—multiple threads can simultaneously operate on different `shared_ptr` instances (even if they point to the same object) without external synchronization. However, the same `shared_ptr` instance cannot be read from and written to simultaneously by multiple threads (locking is required). Concurrent access to the managed object must be made safe by you. ```cpp #include @@ -154,7 +154,7 @@ void demo_thread_safety() { } ``` -From a performance perspective, every copy or destruction of a `shared_ptr` incurs an atomic operation (typically `fetch_add` or `fetch_sub`). On a single-core system, the overhead of atomic operations is small (it might just be a special CPU instruction), but on multi-core systems, it triggers cache coherence protocol overhead (cache line bouncing). If your code frequently creates and destroys `shared_ptr` instances (for example, in a hot loop), this overhead can become very significant. You can verify the overhead difference between single-threaded and multi-threaded scenarios through `code/volumn_codes/vol2/ch01-smart-pointers/verify_shared_ptr_performance.cpp`. +From a performance perspective, every copy or destruction of a `shared_ptr` incurs an atomic operation (typically `fetch_add` or `fetch_sub`). On a single-core system, the overhead of an atomic operation is very small (it might just be a special CPU instruction), but on a multi-core system, it triggers cache coherence protocol overhead (cache line bouncing). If your code frequently creates and destroys `shared_ptr` instances (for example, in a hot loop), this overhead can become very significant. You can verify the overhead difference between single-threaded and multi-threaded scenarios through `code/volumn_codes/vol2/ch01-smart-pointers/verify_shared_ptr_performance.cpp`. The logic when decrementing the reference count is particularly worth noting. When `fetch_sub` returns 1 (meaning this is the last `shared_ptr`), the object needs to be destroyed. Mainstream implementations (like GNU libstdc++) use `memory_order_acq_rel` to ensure that all previous write operations are visible to the destruction code, and insert an `acquire` fence before destruction. These memory barriers have little overhead on x86 (x86 inherently has strong memory ordering), but on weakly-ordered architectures like ARM, they can cause pipeline flushes. @@ -166,13 +166,13 @@ Let's do an intuitive comparison, putting the overheads of `shared_ptr`, `unique |-----------|-------------|------------|------------| | Object size | 8B (64-bit) | 8B | 16B | | Extra heap allocation | None | None | Control block (24-32B+) | -| Copy overhead | 8B copy | Not copyable | Atomic fetch_add | +| Copy overhead | 8B copy | Non-copyable | Atomic fetch_add | | Destruction overhead | None | delete | Atomic fetch_sub + possible delete | | Thread safety | None | None | Reference count safe, object unsafe | From this table, we can clearly see that `shared_ptr` is heavier than `unique_ptr` in every dimension. This isn't to say that `shared_ptr` is bad—it's the correct design choice in scenarios requiring shared ownership—but you should use it only when shared ownership is genuinely needed, rather than "using `shared_ptr` everywhere for convenience." -In real-world projects, we've seen codebases that manage almost all objects with `shared_ptr`, resulting in reference counts flying everywhere, unoptimizable performance, and frequent circular reference issues. A better approach is to clarify ownership relationships during the design phase: manage most resources with `unique_ptr`, use `shared_ptr` only in the few places where sharing is truly needed, and pass non-owning access via references (`T&`) or raw pointers (`T*`, which don't hold ownership). +In real-world projects, we've seen plenty of codebases that manage almost all objects with `shared_ptr`. The result is reference counts flying everywhere, performance that can't be optimized, and frequent circular reference issues. A better approach is to clarify ownership relationships during the design phase: manage most resources with `unique_ptr`, use `shared_ptr` only in the few places where sharing is truly necessary, and pass non-owning access via references (`T&`) or raw pointers (`T*`, which don't hold ownership). ## Aliasing Constructor: A Powerful, Lesser-Known Feature @@ -183,7 +183,7 @@ template shared_ptr(const shared_ptr& r, T* ptr) noexcept; ``` -This constructor creates a new `shared_ptr` that shares the ownership of `r` (i.e., its reference count is shared with `r`), but `get()` returns `ptr` instead of `r.get()`. Simply put: **it lets you hold a "part" of the same object without needing to manage that part's lifetime separately.** +This constructor creates a new `shared_ptr` that shares the ownership of `r` (i.e., its reference count is shared with `r`), but `get()` returns `ptr` instead of `r.get()`. Simply put: **it lets you hold a "part" of the same object without needing to manage that part's lifetime separately**. The most common use case is accessing a member of an object: @@ -208,7 +208,7 @@ void connect(const std::shared_ptr& host) { This feature is particularly useful when implementing "smart pointers to container elements"—for example, if you want to return a `shared_ptr` pointing to a specific element in a `vector`, but you don't want the caller to hold a `shared_ptr` to the entire `vector`. Through the aliasing constructor, you can return a `shared_ptr` that only exposes the element type, while the underlying lifetime is still managed by the container's `shared_ptr`. -## enable_shared_from_this: Obtaining a shared_ptr in Member Functions +## enable_shared_from_this: Obtaining a shared_ptr in a Member Function Sometimes, an object's member function needs to return a `shared_ptr` pointing to itself. The most intuitive approach, `shared_ptr(this)`, is a fatal error—it creates a new control block, causing the object to be deleted twice. The correct approach is to inherit from `std::enable_shared_from_this` and call `shared_from_this()`: @@ -247,35 +247,35 @@ void session_demo() { } ``` -⚠️ Using `enable_shared_from_this` has a prerequisite: the object must already be managed by a `shared_ptr`. If you create the object on the stack or manage it with a raw pointer, calling `shared_from_this()` leads to undefined behavior. Additionally, you cannot call `shared_from_this()` in the constructor—because at that point, the `shared_ptr` has not finished being constructed. +> ⚠️ Using `shared_from_this()` has a prerequisite: the object must already be managed by a `shared_ptr`. If you create the object on the stack or manage it with a raw pointer, calling `shared_from_this()` leads to undefined behavior. Additionally, you cannot call `shared_from_this()` in the constructor—because at that point, the `shared_ptr` has not finished being constructed yet. ## Common Misuses and Pitfalls -Before diving into embedded trade-offs, let's inventory a few common misuse patterns of `shared_ptr`. We've fallen into these "pitfalls" more than once ourselves, and we hope readers can avoid them in advance. +Before diving into embedded trade-offs, let's take stock of a few common `shared_ptr` misuse patterns. We've fallen into these "pitfalls" ourselves more than once, and we hope readers can avoid them in advance. **Misuse 1: Creating a second control block with `shared_ptr(this)`**. This is the most fatal error. If you write `return std::shared_ptr(this)` in a member function of an object already managed by a `shared_ptr`, the compiler creates a brand-new control block with a reference count starting at 1. The result is two independent control blocks managing the same object—when both `shared_ptr` instances are destroyed, the object gets deleted twice. The correct approach is to inherit from `enable_shared_from_this` and call `shared_from_this()`. -**Misuse 2: Exposing ownership intent with `shared_ptr` in interfaces**. If you write a function `void process(std::shared_ptr w)`, the signature itself implies "I want to share ownership with you." But often, the function just wants to use the object without needing to hold it. In this scenario, passing `const Widget&` or `Widget*` is more appropriate—it doesn't imply ownership, and it avoids the overhead of reference counting. +**Misuse 2: Exposing shared ownership intent in interfaces with `shared_ptr`**. If you write a function `void process(std::shared_ptr w)`, the signature itself implies "I want to share ownership with you." But often, the function just wants to use the object without needing to hold it. In this scenario, passing a `const Widget&` or `Widget*` is more appropriate—it implies no ownership and incurs no reference counting overhead. -**Misuse 3: Using `shared_ptr` to manage objects that "don't need sharing"**. Some teams use `shared_ptr` to manage all heap objects just to save effort—"shared_ptr can handle anything anyway." This leads to blurred ownership semantics (everyone holds it means no one is responsible), degraded performance (atomic operations everywhere), and increased risk of circular references. Our experience is: **90% of objects should be managed by `unique_ptr`, and only the 10% that truly need sharing should use `shared_ptr`**. +**Misuse 3: Using `shared_ptr` to manage objects that "don't need sharing"**. Some teams use `shared_ptr` to manage all heap objects just to save effort—"after all, shared_ptr can manage anything." This leads to blurred ownership semantics (if everyone holds it, no one is responsible), degraded performance (atomic operations everywhere), and increased risk of circular references. Our experience is: **90% of objects should be managed by `unique_ptr`, and only the 10% that truly need sharing should use `shared_ptr`**. -**Misuse 4: Ignoring the difference between `make_shared` and `new`**. `make_shared` merges the object and the control block into a single allocation, but this also means the object's destruction and the control block's release don't happen at the same time—when all `shared_ptr` instances are destroyed, the object is destructed, but if `weak_ptr` instances are still alive, the entire memory block (including the space occupied by the object) won't be freed until all `weak_ptr` instances are also destroyed. For large objects, this can lead to a situation where "no one is using it, but the memory isn't returned." If you expect `weak_ptr` instances to live for a long time, using `shared_ptr(new T)` to separate the object and control block allocations might be more appropriate. +**Misuse 4: Ignoring the difference between `make_shared` and `new`**. `make_shared` merges the object and the control block into a single allocation, but this also means the object's destruction and the control block's release don't happen at the same time—when all `shared_ptr` instances are destroyed, the object is destructed, but if `weak_ptr` instances are still alive, the entire memory block (including the space occupied by the object) won't be freed until all `weak_ptr` instances are also destroyed. For large objects, this can lead to a phenomenon where "no one is using it anymore, but the memory isn't returned." If you expect long-lived `weak_ptr` instances, using `shared_ptr(new T)` to allocate the object and the control block separately might be more appropriate. ## Systemic Consequences of shared_ptr Abuse -We're dedicating a separate section to this simply because we ourselves used to be abusers... +We've dedicated a separate section to this topic, quite simply because we ourselves were once abusers... -Earlier, we went through common misuse patterns of `shared_ptr` one by one, but the severity of the problem goes far beyond "a mistake somewhere." When `shared_ptr` is systematically abused in a codebase, it brings a **chronic poison at the architectural level**—not the kind of acute error that fails to compile, but a progressive decay that makes the codebase gradually unmaintainable, unreasonably complex, and unoptimizable. We've seen more than one project fall into this quagmire because "all objects are managed by `shared_ptr`," and fixing it often requires large-scale refactoring. +Earlier, we went through common `shared_ptr` misuse patterns one by one, but the severity of the problem goes far beyond "a mistake in some place." When `shared_ptr` is systematically abused in a codebase, it brings a **chronic poison at the architectural level**—not the kind of acute error that fails to compile, but a progressive decay that makes the codebase gradually unmaintainable, unreasonably complex, and unoptimizable. We've seen more than one project fall into this quagmire because "all objects are managed by `shared_ptr`," and fixing it often requires a large-scale refactoring. ### Collapse of the Ownership Model -In a healthy design, every object should have a clear owner—"who created it, who destroys it, whose responsibility is the lifetime"—these questions should be answered clearly during the design phase. But when you use `shared_ptr` everywhere, the answer to these questions becomes "who knows, it'll naturally be destroyed when the reference count reaches zero." It sounds convenient, but the price is that you lose control over the object's lifetime: you can't guarantee the object is alive at any specific moment (because other holders might release it at any time), and you can't guarantee the object is destroyed at any specific moment (because unknown holders might still be referencing it). This "nobody is responsible" state is exactly the same problem caused by an overabundance of global variables. +In a healthy design, every object should have a clear owner—"who created it, who destroys it, and whose decision determines its lifetime"—these questions should be answered clearly during the design phase. But when you use `shared_ptr` everywhere, the answer to these questions becomes "who knows, it'll naturally be destroyed when the reference count reaches zero." It sounds convenient, but the cost is that you lose control over the object's lifetime: you can't guarantee the object is alive at any specific moment (because other holders might release it at any time), and you can't guarantee the object is destroyed at any specific moment (because unknown holders might still be referencing it). This "nobody is responsible" state is remarkably similar to the problems caused by an overabundance of global variables. -In his C++Now talk, Sean Parent aptly compared abusing `shared_ptr` to **implicit global variables**—any code holding a `shared_ptr` participates in the object's lifetime management, which is strikingly similar to the characteristic of global variables where "anywhere can access it, anywhere can extend its lifetime." A more practical problem is that once your public interface returns a `shared_ptr`, all callers are forced to use `shared_ptr`, even if they just want to temporarily borrow the object. You deprive callers of the right to choose their ownership model—a better approach is to return `unique_ptr` (callers can freely `std::move` it into a `shared_ptr`) or a raw pointer/reference (for non-owning access). +In his C++Now talk, Sean Parent aptly compared abusing `shared_ptr` to **implicit global variables**—any code holding a `shared_ptr` participates in the object's lifetime management, a characteristic strikingly similar to global variables where "anywhere can access it, anywhere can extend its lifetime." A more practical problem is that once your public interface returns a `shared_ptr`, all callers are forced to use `shared_ptr`, even if they just want to temporarily borrow the object. You've deprived callers of the right to choose their ownership model—a better approach is to return a `unique_ptr` (callers can freely `std::move` it into a `shared_ptr`) or a raw pointer/reference (for non-owning access). ### Cache Line Contention Under Multithreading -This problem doesn't appear at all in single-threaded code, but it becomes very glaring in multithreaded scenarios. The control block of a `shared_ptr` stores the strong reference count and the weak reference count. These two atomic counters are in the same control block and likely share the same cache line (typically 64 bytes). When multiple threads frequently copy and destroy `shared_ptr` instances pointing to the **same object**, every atomic modification to the reference count by each thread causes that cache line to bounce back and forth between different cores—even if these threads are operating on their own independent `shared_ptr` instances, as long as they point to the same object, they compete for the same control block's cache line. +This problem doesn't appear at all in single-threaded code, but it becomes very glaring in multithreaded scenarios. The control block of a `shared_ptr` stores both the strong and weak reference counts. These two atomic counters are typically in the same control block and likely share the same cache line (usually 64 bytes). When multiple threads frequently copy and destroy `shared_ptr` instances pointing to **the same object**, every atomic modification to the reference count by each thread causes that cache line to bounce back and forth between different cores—even if these threads are operating on their own independent `shared_ptr` instances, as long as they point to the same object, they compete for the cache line of the same control block. Talking isn't enough; let's run a test. The following benchmark program (`code/volumn_codes/vol2/ch01-smart-pointers/verify_cache_contention.cpp`) builds a thread-safe producer-consumer queue, passing messages via raw pointers and `shared_ptr` respectively. The test environment is our Windows WSL2 Arch Linux, AMD Ryzen 7 5800H (14 threads), GCC 15.2, compiled with `-O2` in Release mode. The results are as follows: @@ -284,33 +284,33 @@ Talking isn't enough; let's run a test. The following benchmark program (`code/v | Raw pointer | 10,000 | ~30 ms | Baseline | | `shared_ptr` | 10,000 | ~35 ms | **+15-20%** | -The 15-20% overhead might be even more significant in real-world applications, because our test used a mutex-protected queue, and the mutex overhead masks some of the `shared_ptr` overhead. In lock-free queues or higher-concurrency scenarios (like the 8-thread setup in the original test), the overhead of `shared_ptr` becomes even more pronounced. The source of this overhead is clear: every `shared_ptr` copy atomically increments the reference count, and every destruction atomically decrements it—in scenarios where multiple threads simultaneously operate on the same control block, these atomic operations trigger cache line contention. This can be ignored in low-concurrency, low-throughput scenarios, but must be carefully considered on high-concurrency hot paths. +A 15-20% overhead might be even more significant in real-world applications, because our test used a mutex-protected queue, and the mutex overhead masks some of the `shared_ptr` overhead. In lock-free queues or higher-concurrency scenarios (like the 8-thread setup in the original test), the overhead of `shared_ptr` becomes even more pronounced. The source of this overhead is clear: every `shared_ptr` copy atomically increments the reference count, and every destruction atomically decrements it—in scenarios where multiple threads simultaneously operate on the same control block, these atomic operations trigger cache line contention. This can be ignored in low-concurrency, low-throughput scenarios, but must be carefully considered on high-concurrency hot paths. ### Circular References: Silent Memory Leaks -When an object leaks due to a circular reference, you won't get any error messages—the reference count of the `shared_ptr` will never reach zero, and the object just quietly sits on the heap occupying memory. No crashes, no assertion failures, no logs telling you "hey, this object leaked." You might only notice the problem when memory usage keeps growing, and then you need tools like Valgrind or AddressSanitizer to pinpoint the leak. What's worse is that circular references are often not simple loops between two objects, but complex dependency graphs involving multiple objects—A holds B, B holds C, and C holds A—tracking the reference chain in such cases is a very painful endeavor. +When an object leaks due to a circular reference, you won't get any error messages—the reference count of the `shared_ptr` will never reach zero, and the object just quietly sits on the heap taking up memory. No crashes, no assertion failures, no logs telling you "hey, this object leaked." You might only notice the problem when memory usage keeps growing, and then you need tools like Valgrind or AddressSanitizer to pinpoint the leak. What's worse is that circular references are often not simple loops between two objects, but complex dependency graphs involving multiple objects—A holds B, B holds C, and C holds A again—tracking the reference chain in such cases is a very painful endeavor. -In contrast, the exclusive ownership model of `unique_ptr` makes circular references impossible at compile time (you cannot construct a valid exclusive ownership cycle), which is a huge advantage at the design level. If you find yourself needing to extensively use `weak_ptr` to break circular references, that in itself is a strong signal: there's a problem with your ownership model design, and you should re-examine the dependencies between objects rather than patching things up everywhere with `weak_ptr`. +In contrast, the exclusive ownership model of `unique_ptr` makes circular references impossible at compile time (you cannot construct a valid exclusive ownership cycle), which is a huge advantage at the design level. If you find yourself needing to use `weak_ptr` extensively to break circular references, that in itself is a strong signal: there's a problem with your ownership model design, and you should re-examine the dependencies between objects rather than patching things up everywhere with `weak_ptr`. ### Ownership Inversion: A Ticking Time Bomb in Callbacks -This problem is particularly common in asynchronous programming, and the bugs it causes are extremely difficult to track down. Suppose object A holds a Timer, and the Timer's callback captures A's `shared_ptr` via `shared_from_this()`. When A is reset on the main thread, the Timer thread ironically becomes A's sole holder—A's lifetime gets "inverted" onto the Timer thread. If the Timer's destructor needs to join its own thread (`std::jthread` will do exactly this), it triggers a `std::system_error`: a thread attempting to join itself, which is undefined behavior. The root cause of this type of bug is that `shared_ptr` lets you "be too lazy to think about ownership"—you thought you released A, but the callback is still secretly holding onto it in the shadows. The correct approach is to clarify lifetime constraints during the design phase: if A's destruction depends on the Timer thread finishing, then A must be destroyed before the Timer, using `unique_ptr`'s exclusive semantics to express this constraint. +This problem is particularly common in asynchronous programming, and the bugs it causes are extremely difficult to track down. Suppose object A holds a Timer, and the Timer's callback captures A's `shared_ptr` via a `shared_from_this()`. When A is reset on the main thread, the Timer thread ironically becomes A's sole holder—A's lifetime has been "inverted" onto the Timer thread. If the Timer's destructor needs to join the thread it resides on (`std::jthread` will do exactly this), it triggers a `std::system_error`: a thread attempting to join itself, which is undefined behavior. The root cause of this type of bug lies in `shared_ptr` letting you "be too lazy to think about ownership"—you thought you released A, but the callback is still secretly holding onto it in the shadows. The correct approach is to clarify lifetime constraints during the design phase: if A's destruction depends on the Timer thread finishing, then A must be destroyed before the Timer, using the exclusive semantics of `unique_ptr` to express this constraint. ### Uncertain Destruction Timing and Real-Time Hazards -When you drop a `shared_ptr`, you can't be sure whether it's the last one—the object might be destroyed in this drop, or it might continue living because other holders still exist. This means the timing of the destructor call is **unpredictable**, and the destruction order is **undefined**. In real-time systems, this is especially dangerous: if you drop a `shared_ptr` in an audio callback, an interrupt service routine, or any code path with real-time requirements, and it happens to be the last holder, the triggered destructor could bring unacceptable latency—heap deallocation, file I/O, log writing, these are all non-deterministic, time-consuming operations. Timur Doumler proposed a clever `ReleasePool` approach when discussing C++ audio development: periodically clean up `shared_ptr` instances that might need destruction on a low-priority thread, ensuring that destructors are never triggered on real-time threads. But ultimately, if you had used `unique_ptr` with explicit lifetime management during the design phase, you wouldn't need this workaround at all. +When you drop a `shared_ptr`, you can't be sure whether it's the last one—the object might be destroyed in this drop, or it might continue living because other holders still exist. This means the timing of the destructor call is **unpredictable**, and the destruction order is **undefined**. In real-time systems, this is especially dangerous: if you drop a `shared_ptr` in an audio callback, an interrupt service routine, or any code path with real-time requirements, and it happens to be the last holder, the triggered destructor could bring unacceptable latency—heap deallocation, file I/O, and log writing are all non-deterministic, time-consuming operations. Timur Doumler proposed a clever `ReleasePool` approach when discussing C++ audio development: periodically clean up `shared_ptr` instances that might need destruction on a low-priority thread, ensuring that destructors are never triggered on real-time threads. But ultimately, if you had used `unique_ptr` with explicit lifetime management during the design phase, you wouldn't need this workaround at all. ## Practical Selection Guide: When to Use shared_ptr -Before discussing embedded trade-offs, let's do a practical, decision-oriented analysis. Many people hesitate between `unique_ptr` and `shared_ptr`, but the judgment criterion is simple—ask yourself one question: **Does this object need to be jointly owned by multiple independent modules?** +Before discussing embedded trade-offs, let's do a practical, decision-oriented analysis. Many people hesitate between `unique_ptr` and `shared_ptr`, but the judgment criterion is actually very simple—ask yourself one question: **Does this object need to be jointly owned by multiple independent modules?** -If the answer is "no"—the object's lifetime is determined by one clear "owner," and other modules just temporarily borrow it—then use `unique_ptr` + raw pointer/reference passing. This covers the vast majority of scenarios. +If the answer is "no"—the object's lifetime is determined by a clear "owner," and other modules just temporarily borrow it—then use `unique_ptr` + raw pointers/references for passing. This covers the vast majority of scenarios. If the answer is "yes"—multiple modules genuinely need to independently decide "I'm still using this object," and no single module can claim "I'm the sole owner"—then use `shared_ptr`. Typical use cases for `shared_ptr` include: shared modules in a plugin system (multiple components might depend on the same plugin instance simultaneously, and none can unload it prematurely), shared state in asynchronous callback chains (multiple futures/callbacks need to keep the state alive until they complete), and shared nodes in trees or graphs (multiple parent nodes referencing the same child node). -Typical scenarios where you should not use `shared_ptr` include: passing function parameters (passing by reference is enough), sole owners of objects (use `unique_ptr`), and simple caches (use `weak_ptr` to observe, `shared_ptr` to hold). +Typical scenarios where you should *not* use `shared_ptr` include: passing function arguments (passing by reference is enough), the sole owner of an object (use `unique_ptr`), and simple caches (use `weak_ptr` to observe, and `shared_ptr` to hold). Let's look at a specific design decision example—implementing a simple task scheduler: @@ -372,29 +372,29 @@ private: }; ``` -The first version uses `unique_ptr`—after a task is submitted, ownership belongs to the scheduler, simple and clear. The second version uses `shared_ptr`—allowing multiple schedulers or external code to hold a reference to the same task, and the task is only destroyed when the last holder goes away. Which one to choose depends on your design needs, not "which one is more convenient." +The first version uses `unique_ptr`—once a task is submitted, ownership belongs to the scheduler, simple and clear. The second version uses `shared_ptr`—allowing multiple schedulers or external code to hold a reference to the same task, and the task is only destroyed when the last holder goes away. Which one to choose depends on your design needs, not "which one is more convenient." ## Embedded Trade-offs: Memory Overhead and ISR Considerations Using `shared_ptr` in embedded scenarios requires extra caution. Let's analyze the reasons one by one. -First is the **memory overhead**. On a 32-bit MCU, a `shared_ptr` object takes up 8 bytes (two pointers), and the control block takes at least 16-24 bytes (depending on the implementation). If you use `make_shared`, the object and the control block together might occupy `sizeof(T) + 24+` bytes. For an MCU with only a few tens of KB of RAM, this overhead becomes very noticeable when the number of objects is large. Let's do the specific math: suppose your MCU has 64KB of RAM, and you need to manage 50 peripheral handles, each handle object being 16 bytes itself. Managing them with `unique_ptr` costs a total of `50 * (8 + 16) = 1200` bytes; managing them with `shared_ptr` + `make_shared` costs a total of `50 * (16 + 16 + 24) = 2800` bytes—that's 1,600 extra bytes, accounting for 2.4% of the total RAM. On MCUs with even tighter memory (like the STM32F103 with only 20KB of RAM), this number becomes even more glaring. +First is the **memory overhead**. On a 32-bit MCU, a `shared_ptr` object takes up 8 bytes (two pointers), and the control block takes at least 16-24 bytes (depending on the implementation). If you use `make_shared`, the object and the control block together might occupy `sizeof(T) + 24+` bytes. For an MCU with only a few dozen KB of RAM, this overhead becomes very noticeable when the number of objects is large. Let's do the specific math: suppose your MCU has 64KB of RAM, and you need to manage 50 peripheral handles, with each handle object itself being 16 bytes. Managing them with `unique_ptr` costs a total of `50 * (8 + 16) = 1200` bytes; managing them with `shared_ptr` + `make_shared` costs a total of `50 * (16 + 16 + 24) = 2800` bytes—an extra 1600 bytes, accounting for 2.4% of the total RAM. On MCUs with even tighter memory (like the STM32F103 with only 20KB of RAM), this number becomes even more glaring. -Second is **heap allocation**. The control block needs to be allocated on the heap, and many embedded systems either have the heap disabled or have very limited heap space. Frequent heap allocation leads to memory fragmentation, ultimately causing allocation failures. If your system runs for a long time (embedded devices typically run year-round), the fragmentation problem will only get worse. One possible mitigation is to use `std::allocate_shared` with a custom allocator (such as a memory pool allocator), moving the control block's allocation from the system heap to a pre-allocated memory pool. +Second is **heap allocation**. The control block needs to be allocated on the heap, and many embedded systems either have the heap disabled or have very limited heap space. Frequent heap allocation leads to memory fragmentation, ultimately resulting in allocation failures. If your system runs for a long time (embedded devices typically run year-round), the fragmentation problem will only get worse. One possible mitigation is to use `std::allocate_shared` with a custom allocator (such as a memory pool allocator), moving the control block's allocation from the system heap to a pre-allocated memory pool. -Third is **atomic operations**. The atomic increment/decrement of the reference count on a single-core MCU might degrade into disabling interrupts (depending on the toolchain's implementation of `std::atomic`), which affects interrupt response times. Using `shared_ptr` in an ISR is a terrible idea—not only because of heap operations, but also because atomic operations might disable interrupts. If your system has strict real-time requirements (for example, a control loop must complete within 100us), any indeterminate delay in an ISR is unacceptable. +Third is **atomic operations**. The atomic increment/decrement of the reference count on a single-core MCU might degrade into interrupt-disabling operations (depending on the toolchain's implementation of `std::atomic`), which affects interrupt response times. Using `shared_ptr` in an ISR is a terrible idea—not only because of heap operations, but also because atomic operations might disable interrupts. If your system has strict real-time requirements (for example, a control loop must complete within 100us), any uncertain latency in an ISR is unacceptable. -Our recommendation is to prioritize `unique_ptr` or directly use RAII wrapper classes in embedded systems. If shared semantics are truly needed, consider intrusive reference counting—placing the reference count inside the object itself to avoid extra heap allocations. In a single-threaded environment, the reference count in an intrusive solution can be a plain `uint32_t`, requiring no atomic operations and having extremely low overhead. We will discuss this topic in detail in the article on "Custom Deleters and Intrusive Reference Counting." +Our advice is to prioritize `unique_ptr` or directly use RAII wrapper classes in embedded systems. If shared semantics are truly needed, consider intrusive reference counting—putting the reference count inside the object itself to avoid extra heap allocations. In a single-threaded environment, the reference count in an intrusive scheme can be a plain `uint32_t`, requiring no atomic operations and having extremely low overhead. We will discuss this topic in detail in the article on "Custom Deleters and Intrusive Reference Counting." ## Summary -`shared_ptr` implements shared ownership semantics through reference counting, complementing the exclusive semantics of `unique_ptr`. The key to understanding it lies in the control block mechanism—each `shared_ptr` instance holds two pointers (to the object and to the control block), and the atomic reference count in the control block guarantees safety under multithreading, but it also brings non-negligible performance overhead. +`shared_ptr` implements shared ownership semantics through reference counting, complementing the exclusive semantics of `unique_ptr`. The key to understanding it lies in the control block mechanism—each `shared_ptr` instance holds two pointers (to the object and to the control block), and the atomic reference counts in the control block guarantee safety in multithreaded environments, but they also bring non-negligible performance overhead. -`make_shared` optimizes performance and memory locality through a single allocation, and should be the preferred way to create `shared_ptr`. The aliasing constructor and `enable_shared_from_this` are two lesser-known but highly useful advanced features. In embedded scenarios, the memory overhead, heap allocation, and atomic operation costs of `shared_ptr` need to be carefully weighed—in most cases, `unique_ptr` or intrusive solutions are better choices. +`make_shared` optimizes performance and memory locality through a single allocation, and should be the preferred way to create a `shared_ptr`. The aliasing constructor and `enable_shared_from_this` are two advanced features that are not well-known but are very useful. In embedded scenarios, the memory overhead, heap allocation, and atomic operation costs of `shared_ptr` need to be carefully weighed—in most cases, `unique_ptr` or intrusive approaches are better choices. -In the next article, we will discuss `weak_ptr`—the partner of `shared_ptr`, specifically designed to solve the thorny problem of circular references. +In the next article, we will discuss `weak_ptr`—the partner of `shared_ptr`, specifically designed to solve the tricky problem of circular references. -## References +## Reference Resources - [cppreference: std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) - [cppreference: std::make_shared](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared) diff --git a/documents/en/vol2-modern-features/ch04-type-safety/03-variant.md b/documents/en/vol2-modern-features/ch04-type-safety/03-variant.md index 504924e64..ee9a9bb84 100644 --- a/documents/en/vol2-modern-features/ch04-type-safety/03-variant.md +++ b/documents/en/vol2-modern-features/ch04-type-safety/03-variant.md @@ -1,7 +1,7 @@ --- title: 'std::variant: A Type-Safe Union' -description: Replacing `union` with `variant`, combined with `visit` to achieve type-safe - polymorphism +description: Using `variant` instead of `union`, combined with `visit` to achieve + type-safe polymorphism chapter: 4 order: 3 tags: @@ -23,16 +23,16 @@ related: - 错误处理的现代方式 translation: source: documents/vol2-modern-features/ch04-type-safety/03-variant.md - source_hash: e0e6da66d9e52036bc0b120363c90d0ca098ae09434f9c330dd9baa77d4d52b1 - translated_at: '2026-05-26T11:28:09.977892+00:00' + source_hash: 81d99b49b224001e9f5a0f0432eec42cd6eef679ea5fe985c106aa4b669e6733 + translated_at: '2026-06-07T02:14:12.511358+00:00' engine: anthropic - token_count: 2858 + token_count: 2916 --- # std::variant: A Type-Safe Union ## Introduction -`std::variant` (introduced in C++17) is the modern replacement for `union`. The core problem it solves is how to guarantee type safety under the constraint of "holding exactly one of several types at any given time." Unlike a bare `union`, `variant` knows which type it currently holds, checks this when you access the value, and correctly manages the lifetime of the held object. In this chapter, we start from the pain points of `union` and work through the mechanisms and usage of `variant` step by step. +`std::variant` (introduced in C++17) is the modern replacement for `union`. The core problem it solves is how to guarantee type safety under the constraint of "holding exactly one of several types at any given time." Unlike a bare `union`, `variant` knows which type it currently holds, performs checks when you access the value, and correctly manages the lifetime of the held object. In this chapter, we start from the pain points of `union` and work our way through the mechanisms and usage of `variant`. ## Step 1 — The Fatal Flaws of union @@ -74,7 +74,7 @@ Frankly, every time we write code like this, it feels like walking a tightrope ### Construction and Assignment -`std::variant` can hold a value of **exactly one** of the types in `Types...` at any given time. When default-constructed, it constructs the first alternative type (unless you use `std::monostate` as a placeholder): +A `std::variant` can hold a value of **exactly one** of the types in `Types...` at any given time. When default-constructed, it constructs the first alternative type (unless you use `std::monostate` as a placeholder): ```cpp #include @@ -124,7 +124,7 @@ Our recommended approach is: if you only need to check the type, use `std::holds ## Step 3 — std::visit and the Visitor Pattern -`std::visit` is the core access mechanism for `variant`. It accepts a callable (the visitor) and one or more `variant` objects, dispatching the call based on the type currently held by the `variant`. This is safer than `switch-case` because the compiler checks whether you have handled all alternative types. +`std::visit` is the core access mechanism for `variant`. It accepts a callable object (a visitor) and one or more `variant` objects, dispatching the call based on the type currently held by the `variant`. This is safer than `switch-case` because the compiler checks whether you have handled all alternative types. ### Simple visit with a lambda @@ -136,11 +136,11 @@ std::visit([](auto&& arg) { }, v); ``` -Here, `auto&&` is a forwarding reference, and `visit` instantiates this lambda based on the type currently held by `v`. When you need to perform the same operation on all types, this approach is very concise. +Here, `auto&&` is a forwarding reference, and `visit` instantiates this lambda based on the type currently held by `v`. When you only need to perform the same operation on all types, this approach is very concise. ### Overload sets: Handling different types -A more common scenario is that different types require different handling logic. In this case, we need an "overload set"—a callable object with a corresponding overload for each alternative type. There is a classic trick in C++17 to achieve this: +A more common scenario is where different types require different handling logic. In this case, we need an "overload set"—a callable object with a corresponding overload for each alternative type. There is a classic trick in C++17 to achieve this: ```cpp // 重载集合工具(C++17 惯用法) @@ -154,7 +154,7 @@ template Overloaded(Ts...) -> Overloaded; ``` -This `Overloaded` "inherits" the `operator()` of multiple lambdas together, forming a callable object with overloads for multiple types. In use: +This `Overloaded` "inherits" the `operator()` of multiple lambdas together, forming a callable object with overloads for multiple types. Usage looks like this: ```cpp std::variant v = 3.14; @@ -166,11 +166,11 @@ std::visit(Overloaded{ }, v); ``` -The compiler checks whether your `Overloaded` covers all alternative types of the `variant`. If you miss handling a certain type, the compiler will error out directly—this is the embodiment of compile-time type safety. In C++20, you don't even need to write `Overloaded` by hand—the standard library directly supports the visit pattern with multiple lambdas (though the formal support mechanism is still evolving). +The compiler checks whether your `Overloaded` covers all alternative types of the `variant`. If you miss handling a certain type, the compiler will directly report an error—this is the embodiment of compile-time type safety. In C++20, you don't even need to write `Overloaded` by hand—the standard library directly supports the visit pattern with multiple lambdas (though the formal support mechanism is still evolving). ### visit with return values -The visitor in `visit` can also return values. The return types of all lambdas must be compatible (convertible to a common type): +A visitor in `visit` can also return values. The return types of all lambdas must be compatible (convertible to a common type): ```cpp std::variant v = 42; @@ -256,11 +256,11 @@ for (const auto& s : shapes) { } ``` -The advantages of the `variant` approach are: value semantics (no need for `new`/`delete`), contiguous memory (stored directly in the `vector`, which is cache-friendly), and compile-time type checking (all branches of the `visit` are determined at compile time). But it also has a cost: every time you add a new shape, you must modify the `variant` definition of the `Shape`—which is inflexible in certain scenarios. If your type hierarchy is "open" (third parties can extend it with new types), virtual functions remain the better choice. +The advantage of the `variant` approach lies in: value semantics (no need for `new`/`delete`), contiguous memory (stored directly in the `vector`, which is cache-friendly), and compile-time type checking (all branches of `visit` are determined at compile time). But it comes with a cost: every time you add a new shape, you must modify the `variant` definition of the `Shape`—which is inflexible in certain scenarios. If your type hierarchy is "open" (third parties can extend it with new types), virtual functions remain the better choice. ## Step 5 — Exception Safety and valueless_by_exception -`variant` has a rather special state called `valueless_by_exception`. When `variant` is in the process of switching types (for example, during assignment or `emplace`), if the constructor of the new type throws an exception and the old value has already been destroyed, the `variant` enters this "valueless" state. +`variant` has a rather special state called `valueless_by_exception`. When a `variant` is switching types (for example, during assignment or `emplace`), if the constructor of the new type throws an exception while the old value has already been destroyed, the `variant` enters this "valueless" state. ```cpp struct ThrowingType { @@ -276,9 +276,9 @@ try { } ``` -In this state, `std::visit` throws `std::bad_variant_access`, and `std::get` also throws an exception. So if your code has `variant` that might encounter this situation, it's best to check before accessing. +In this state, `std::visit` throws `std::bad_variant_access`, and `std::get` also throws an exception. So if `variant` in your code might encounter this situation, it's best to check before accessing. -⚠️ In practice, `valueless_by_exception` rarely appears during normal use. It is only triggered in the specific scenario where "constructing a new value throws an exception." If the constructors of all your alternative types are `noexcept` (or you don't use exceptions), you don't need to worry about this state at all. +⚠️ In practice, `valueless_by_exception` rarely appears during normal usage. It is only triggered in the specific scenario where "constructing a new value throws an exception." If the constructors of all your alternative types are `noexcept` (or you don't use exceptions), you don't need to worry about this state at all. ## Practical Application — Message Type System @@ -359,13 +359,13 @@ private: }; ``` -The beauty of this code is: if you add a new message type (such as `FileTransfer`), the compiler will error out directly at the `visit` call in `Overloaded`—you must add a corresponding overload in `handle`. This ability to have the compiler find all the places you need to modify when adding a new type is one of the biggest advantages of `variant` over `switch-case` or virtual functions. +The beauty of this code is: if you add a new message type (such as `FileTransfer`), the compiler will immediately report an error at the `visit` call site in `Overloaded`—you must add a corresponding overload in `handle`. This ability to "have the compiler find all the places you need to modify when adding a new type" is one of the biggest advantages of `variant` over `switch-case` or virtual functions. ## Practical Application — Configuration Values and AST Nodes ### Configuration Values -Configuration systems often need to store different types of values: integers, floats, strings, and booleans. `variant` is a natural fit: +Configuration systems often need to store different types of values: integers, floating-point numbers, strings, and booleans. `variant` is a natural fit: ```cpp using ConfigValue = std::variant; @@ -434,29 +434,32 @@ struct UnaryExpr { ## Memory Layout and Performance Considerations -The size of `variant` equals the size of the largest alternative type plus a small metadata field (used to record the index of the currently held type). This means that even if you are currently only holding a `int`, the `variant` is still at least as large as `sizeof(std::string) + sizeof(size_t)`. +The size of a `variant` equals the size of the "largest alternative type" plus a small metadata field (used to record the index of the currently held type). This means that even if you are currently only holding an `int`, the `variant` is still at least as large as `sizeof(std::string) + sizeof(size_t)`. ```cpp std::cout << "sizeof(variant): " << sizeof(std::variant) << "\n"; -// 典型输出:32(64 位平台上,string 占 32 字节,int 和 double 各 8 字节) +// 典型输出:40(64 位平台上,string 占 32 字节,int 占 4 字节, double 占 8 字节) std::cout << "sizeof(string): " << sizeof(std::string) << "\n"; // 典型输出:32 ``` -This size is completely acceptable for most applications. However, in extremely memory-constrained embedded scenarios, you may need to evaluate whether it's worth using `variant` to replace a hand-written `union` + `enum` tag scheme. The type safety benefits brought by `variant` usually far outweigh the overhead of a few bytes of memory. +> As a brief aside, you can read about the size of `int` at this [website](https://en.cppreference.com/cpp/language/types). Simply put, `int` is guaranteed to be at least 16 bits, or 2 bytes, though it is uniformly 4 bytes on other platforms. Of course, don't memorize this as rote knowledge. +> You can refer to this [example](https://godbolt.org/z/sbvEMW56G) provided by instructor [YukunJ](https://github.com/YukunJ) + +This size is completely acceptable for most applications. However, in extremely memory-constrained embedded scenarios, you may need to evaluate whether it is worth using `variant` to replace a hand-written `union` + `enum` tag scheme. The type safety benefits brought by `variant` usually far outweigh the overhead of a few bytes of memory. ## Summary -`std::variant` is one of the most important type safety tools in C++17. It solves three core problems of the bare `union`: not knowing what type it currently holds (solved via an internal tag), not managing object lifetimes (automatically calling constructors/destructors), and not supporting non-trivial types (no restrictions whatsoever). +`std::variant` is one of the most important type safety tools in C++17. It solves three core problems of a bare `union`: not knowing what type it currently holds (solved via an internal tag), not managing object lifecycles (automatically calling constructors/destructors), and not supporting non-trivial types (no restrictions whatsoever). -`std::visit` is the core access mechanism for `variant`, and combined with the `Overloaded` idiom, it enables type-safe pattern matching. When your type set is finite and known (message types, configuration values, AST nodes, etc.), `variant` is more efficient and safer than virtual functions. But if the type set is open (third parties can extend it), virtual functions remain the more appropriate choice. +`std::visit` is the core access mechanism for `variant`, and combined with the `Overloaded` idiom, it enables type-safe pattern matching. When your set of types is finite and known (message types, configuration values, AST nodes, etc.), `variant` is more efficient and safer than virtual functions. But if the type set is open (third parties can extend it), virtual functions remain the more appropriate choice. -`valueless_by_exception` is a state you need to be aware of but usually don't need to worry about—it only occurs in the extreme scenario where constructing a new value throws an exception. Simply knowing this state exists is enough; there's no need to be overly defensive about it in practical code. +`valueless_by_exception` is a state you need to be aware of but usually don't need to worry about—it only occurs in the extreme scenario where constructing a new value throws an exception. Simply knowing that this state exists is enough; there is no need to be overly defensive about it in actual code. -The `std::optional` we will discuss next can be seen as a special case of `variant`—when your "type set" has only two possibilities ("has a value" and "does not have a value"), `optional` is the more concise choice. +The `std::optional` we will discuss next can be seen as a special case of `variant`—when your "set of types" has only two possibilities ("has a value" and "does not have a value"), `optional` is the more concise choice. -## References +## Reference Resources - [cppreference: std::variant](https://en.cppreference.com/w/cpp/utility/variant) - [cppreference: std::visit](https://en.cppreference.com/w/cpp/utility/variant/visit)