C++26:std::indirect

Lobsters Hottest 工具

摘要

C++26 引入了 std::indirect,这是一个词汇类型,为堆分配对象提供值语义和常量传播,是类成员中比 std::unique_ptr 更安全的替代方案。

<p><a href="https://lobste.rs/s/k4ri7d/c_26_std_indirect">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/12 08:34

# C++26: std::indirect Source: https://www.sandordargo.com/blog/2026/08/12/cpp26-indirect C++26 在 `<memory>` 中增加了两个新的词汇类型,由 P3019R14 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3019r14.html) 引入(Coe、Peacock、Parent)。从摘要中可以看到: > 类模板 `indirect` 为动态分配的对象赋予类似值的语义。一个 `indirect` 可以持有类 `T` 的对象。拷贝 `indirect` 会拷贝对象 `T`。当 `indirect` 通过 const 访问路径访问时,常量性会传播到所拥有的对象。 > 类模板 `polymorphic` 为动态分配的对象赋予类似值的语义。一个 `polymorphic` 可以持有从 `T` 公开派生的类的对象。拷贝 `polymorphic` 会拷贝派生类型的对象。当 `polymorphic` 通过 const 访问路径访问时,常量性会传播到所拥有的对象。 如你所见,这两种类型在精神上非常接近。它们曾经是两个独立的提案——P1950 用于 `indirect`,P0201 用于 `polymorphic`——后来合并为一份论文。同样,我原本计划在一篇文章中同时涵盖两者,但它变得足够长,我决定拆分。这篇文章介绍 `std::indirect`;下一篇文章将介绍 `std::polymorphic`。 ## `unique_ptr` 的问题 当 `std::unique_ptr` 用作值类型类的成员时,它有两个根本问题。 **首先,它破坏了 const 传播。** `unique_ptr::operator*() const` 返回非 const 的 `T&`。一个 `const` 对象可以修改其间接存储的成员: ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // https://godbolt.org/z/P7zdodhsd struct Settings { int volume = 50; bool muted = false; }; class Player { std::unique_ptr<Settings> settings_; public: Player() : settings_(std::make_unique<Settings>()) {} void mute() const { settings_->muted = true; // 编译通过——通过 const 修改了! } }; const Player p; p.mute(); // const 正确性被破坏了 ``` **其次,它删除了拷贝操作。** 如果 `Car` 应该是可拷贝的,你必须自己编写全部五个特殊成员函数。这就是每个 C++ 开发者都熟知的繁琐的“五法则”(https://www.sandordargo.com/blog/2024/07/31/rule-of-5-once-again)样板代码。 ## `std::indirect`——堆分配对象的值语义 `std::indirect` 就像是为复合类成员而不是所有权转移而设计的 `std::unique_ptr`。它拥有一个堆分配的 `T`,并提供深拷贝、const 传播、基于值的比较和哈希——所有这些你期望从值类型中得到的东西。 ``` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // https://godbolt.org/z/ePxb8E9Ko struct Settings { int volume = 50; bool muted = false; bool operator==(const Settings&) const = default; auto operator<=>(const Settings&) const = default; }; class Player { std::indirect<Settings> settings_; public: Player() : settings_(std::in_place) {} void mute() { settings_->muted = true; } void set_volume(int v) { settings_->volume = v; } int volume() const { return settings_->volume; } bool is_muted() const { return settings_->muted; } const Settings& settings() const { return *settings_; } // 所有特殊成员函数都由编译器生成。 // 拷贝会深拷贝 Settings。移动会转移它。 }; ``` 注意,现在 `mute()` 和 `set_volume()` 是非 const 的——理应如此。如果你试图让它们成为 `const`,编译器会阻止你:`indirect::operator->() const` 返回 `const Settings*`,所以在 `const` 方法中 `settings_->muted = true` 是编译错误: ``` 1 2 error: assignment of member 'Settings::muted' in read-only object settings_->muted = true; // 这无法编译! ``` 来自 `unique_ptr` 版本的确切 bug 在结构上不可能出现。让我们看看 `indirect` 还给了我们什么。 ### Const 传播 与 `unique_ptr` 不同,`indirect::operator*() const` 返回 `const T&`。当你有一个 `const Player` 时,`settings_->` 给你一个 `const Settings&`,因此尝试修改任何成员都是编译错误。这就是成员子对象的行为方式,而 `indirect` 只是将其扩展到堆上。 ### 深拷贝 拷贝一个 `indirect` 会拷贝所拥有的 `T`。你的类变得可拷贝,而无需编写一行样板代码: ``` 1 2 3 4 5 6 7 8 9 10 11 // https://godbolt.org/z/z6aEE4oP4 Player a; a.set_volume(80); a.mute(); Player b = a; // 深拷贝 Settings b.set_volume(30); assert(a.volume() == 80); // a 保持不变 assert(b.volume() == 30); // b 有它自己的副本 ``` 对于 `unique_ptr`,这需要手写拷贝构造函数。 ### 基于值的比较 如果 `T` 支持 `==` 和 `<=>`,那么 `indirect` 也支持——通过比较所拥有的对象,而不是指针: ``` 1 2 3 4 5 6 7 8 // https://godbolt.org/z/znMWdPvjr Player a; Player b; assert(a.settings() == b.settings()); // 真——两者都有 volume=50, muted=false a.set_volume(80); assert(a.settings() != b.settings()); // 真——现在 volume 不同了 ``` ### 无值状态 `indirect` 在设计上没有 null 或空状态。没有默认的 `operator bool()`,也没有 `has_value()`。一个 `indirect` 总是拥有一个对象——除了被移动之后。在这种情况下,`valueless_after_move()` 返回 `true`,访问该对象是未定义行为。如果你需要可空的间接访问,请使用 `std::optional<std::indirect<T>>`。 ### 何时使用它 当你出于结构性原因需要堆分配,但又希望你的类表现得像一个值时,`std::indirect` 是正确的工具: - **PIMPL**:`indirect` 取代了通常的 `unique_ptr`——不再需要手写拷贝/移动/析构函数。Marius Bancila 有一篇详细的演练文章 (https://mariusbancila.ro/blog/2026/07/23/the-pimpl-idiom-and-the-cpp26-stdindirect-type/) 介绍了这一点。 - **递归类型**:`struct Node { int value; std::indirect<Node> next; };` 可以直接工作。 - **大成员**:将大成员移到堆上以缩小 `sizeof(YourClass)`,同时保持值语义。 ## 结论 `std::indirect` 填补了自 C++11 引入移动语义和智能指针以来一直存在的空白。`unique_ptr` 解决了所有权问题,但它从未解决间接存储对象的*值语义*。有了 `indirect`,PIMPL 实现摆脱了样板代码,复合类获得了正确的 const 传播,深拷贝、比较和哈希都无需编写任何特殊成员函数即可工作。 在下一篇文章中,我们将看看它的兄弟 `std::polymorphic`,它将同样的想法扩展到类层次结构——为你提供具有值语义的多态容器,且无需 `clone()` 方法。 ## 深入连接 如果你喜欢这篇文章,请 - 点赞, - 订阅我的 newsletter (https://sandor-dargo.kit.com/e19f29b0a1) - 让我们在 Twitter (https://twitter.com/SandorDargo) 上联系! - 如果你正在准备 C++ 量化/交易面试,请查看 GetCracked (https://www.getcracked.io/?via=sandor)。

相似文章

C++26:减少未定义行为

Lobsters Hottest

C++26 引入了减少未定义行为的更改,特别是使删除指向不完整类型的指针成为格式错误,从而提高了程序安全性。

C++26:更多函数包装器

Lobsters Hottest

C++26 引入了两个新的函数包装器:std::copyable_function(提供了可复制且 const 正确的 std::function 替代品)和 std::function_ref(一个非拥有、可调用的引用,具有引用语义)。

C++26:标准库强化

Lobsters Hottest

C++26 引入了标准化的库强化机制,用于在运行时捕获常见的未定义行为(如越界访问)。基于 Google 的生产经验,此举仅带来 0.30% 的性能开销,同时将段错误减少了 30%。

C++26: #embed

Lobsters Hottest

C++26 introduces the #embed preprocessor directive, allowing binary files to be embedded directly at compile time without external tools, simplifying resource inclusion in C++ programs.