C++26:std::indirect
摘要
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)。
相似文章
PImpl惯用法与C++26 std::indirect类型
解释C++中的PImpl惯用法,以及即将推出的C++26 std::indirect类型如何简化其实现,减少样板代码和手动内存管理。
C++26:减少未定义行为
C++26 引入了减少未定义行为的更改,特别是使删除指向不完整类型的指针成为格式错误,从而提高了程序安全性。
C++26:更多函数包装器
C++26 引入了两个新的函数包装器:std::copyable_function(提供了可复制且 const 正确的 std::function 替代品)和 std::function_ref(一个非拥有、可调用的引用,具有引用语义)。
C++26:标准库强化
C++26 引入了标准化的库强化机制,用于在运行时捕获常见的未定义行为(如越界访问)。基于 Google 的生产经验,此举仅带来 0.30% 的性能开销,同时将段错误减少了 30%。
C++26: #embed
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.