C++26:更多函数包装器
摘要
C++26 引入了两个新的函数包装器:std::copyable_function(提供了可复制且 const 正确的 std::function 替代品)和 std::function_ref(一个非拥有、可调用的引用,具有引用语义)。
<p><a href="https://lobste.rs/s/wqeqgc/c_26_more_function_wrappers">评论</a></p>
查看缓存全文
缓存时间: 2026/05/20 16:29
# C++26:更多函数包装器 来源:https://www.sandordargo.com/blog/2026/05/20/cpp26-copyable-function
C++26 继续填补我们在类型擦除的可调用包装器故事中的空白。自 C++11 以来我们已有 `std::function`,自 C++23 以来有 `std::move_only_function`,但仍有一些缺失的部分。现在我们迎来了两个新成员:`std::copyable_function` 和 `std::function_ref`。
## `std::function` 有什么问题?
`std::function` 一直为我们服务得很好,但它有两个众所周知的问题。首先,它会显著增加二进制大小 (https://www.sandordargo.com/blog/2023/04/05/binary-size-and-templates)。其次,也是更根本的问题,它存在一个**常量正确性缺陷**。它的 `operator()` 被声明为 `const`,但它可以调用存储的可调用对象的非 `const` `operator()`:
```
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// https://godbolt.org/z/9YxcW5eqK
#include <iostream>
#include <functional>
struct Counter {
int counter = 0;
void operator()() {
++counter;
std::cout << "modifying state: " << counter << '\n';
}
};
int main() {
const std::function<void()> f = Counter{};
f(); // OK (!)
}
```
这个缺陷根植于原始设计,无法在不破坏 ABI 的情况下修复。C++23 引入了 `std::move_only_function` (https://www.sandordargo.com/blog/2024/01/31/cpp23-likes-to-move-it) (P0288R9 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0288r9.html)),它修复了常量正确性问题,并增加了对 cv/ref/noexcept 限定符的支持。但正如其名,它不可复制。我们仍需一个既可复制又**常量正确**的包装器。
## `std::copyable_function`
P2548R6 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2548r6.pdf) 恰恰填补了这一空白。其设计紧跟 `std::move_only_function`,增加了拷贝构造函数和拷贝赋值运算符,并要求存储的可调用对象是可拷贝构造的。与 `std::function` 的关键区别在于:签名中的限定符直接控制 `operator()` 如何声明:
```
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// https://godbolt.org/z/orxPK9Gn9
#include <iostream>
#include <functional>
struct Counter {
int count = 0;
int operator()() { return ++count; } // non-const
};
int main() {
// copyable_function 意味着 operator() 是非 const 的
std::copyable_function<int()> f = Counter{};
f(); // OK,非 const 调用
// 如果你想要 const,需要显式声明:
// std::copyable_function<int() const> g = Counter{}; // 错误!
// Counter::operator() 没有 const 限定
std::copyable_function<int() const> h = []{ return 42; }; // OK
}
```
和 `move_only_function` 一样,它支持完整的限定符组合——`const`、`noexcept`、左值/右值引用限定符,以及它们的任意组合。这比 `std::function` 有了显著改进,后者不支持这些限定符。
`copyable_function` 可以隐式转换为 `move_only_function`,反之则不行。和 `move_only_function` 一样,它省略了很少使用的 `target()` 和 `target_type()` 成员函数。
## `std::function_ref`
P0792R14 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p0792r14.html) 添加了一种不同类型的包装器:一个**非拥有的、类型擦除的可调用引用**。可以把它理解为可调用对象的 `std::string_view`。
```
1 2 3 4 5 6 7 8 9
// 使用函数指针——限制太大,不支持带捕获的 lambda:
payload retry(size_t times, payload(*action)());
// 使用模板——可行但膨胀二进制,必须在头文件中:
template<typename F>
payload retry(size_t times, F&& action);
// 使用 function_ref——轻量级,非拥有,无分配:
payload retry(size_t times, std::function_ref<payload()> action);
```
它具有**引用语义**:没有默认构造函数,没有 `operator bool`,也没有 `nullptr` 比较。`function_ref` 始终指向一个有效的可调用对象。每个特化都是可平凡复制的,意味着它可以按寄存器传递。禁止从非函数类型赋值,以防止悬挂引用——因为 `function_ref` 不拥有可调用对象,赋值一个临时对象将是 bug。
它支持 `const` 和 `noexcept` 限定符,但不支持引用限定符,因为引用类型上的引用限定符没有意义。后续论文 P3961R1 (https://isocpp.org/files/papers/P3961R1.html) 修复了从另一个 `function_ref` 构造时的双重间接问题,并允许将 `noexcept` 限定的 `function_ref` 赋值给非 `noexcept` 的 `function_ref`——正如你对普通函数指针的预期。
## 选择正确的包装器
现在有四种可调用包装器可供选择,以下是一个快速指南:
- **`function_ref`**:用于回调参数。非拥有,零开销。
- **`move_only_function`**:用于存储不需要复制的可调用对象。任务队列,延迟执行。
- **`copyable_function`**:用于存储需要副本的可调用对象。现代 `std::function` 替代品。
- **`std::function`**:遗留。新代码中避免使用。
## 结论
C++26 完成了类型擦除的可调用包装器的图景。`std::copyable_function` 提供了 `std::function` 从一开始就应该有的样子:一个常量语义正确的可复制包装器。`std::function_ref` 填补了非拥有场景的空白,为回调参数提供了一种轻量级、零分配的替代方案。与 `std::move_only_function` 一起,新代码中再也没有理由使用 `std::function` 了。
## 更深入的联系
如果你喜欢这篇文章,请:
- 点赞,
- 订阅我的通讯 (https://sandor-dargo.kit.com/e19f29b0a1)
- 并在 Twitter 上与我联系 (https://twitter.com/SandorDargo)!
相似文章
C++26 中 std::format 的改进
C++26 标准对 std::format 库进行了多项改进,包括直接指针格式化、路径格式化、constexpr 支持,以及为 std::println 新增的空行重载。
C语言中在C++中仍然无法工作的构造——以及一些已发生变化的构造
一篇更新经典调查的博文,关于C语言中在C++中无法工作的构造,涵盖了C++20和C23标准中影响兼容性的变化。
C++26:标准库强化
C++26 引入了标准化的库强化机制,用于在运行时捕获常见的未定义行为(如越界访问)。基于 Google 的生产经验,此举仅带来 0.30% 的性能开销,同时将段错误减少了 30%。
C++ 标准库在过去十五年间一直在自我撤步,证据公开
一份详细的目录,列出了从 C++11 到 C++26 期间被正式弃用、非正式不推荐或由于 ABI 约束实际上已损坏但无法修复的 C++ 标准库特性。文章指出,C++ 委员会推出一系列替代品来替换其自身特性的模式始终如一,其中包含一个基准测试,显示 Rust 和 C++ 标准库容器之间的 P99 延迟差异高达 58 倍。
C++ 编译器何时可以反虚拟化调用?
探讨 C++ 编译器何时可以对虚函数调用进行去虚拟化,涵盖已知动态类型和 final 关键字等情况,并在 GCC、Clang、MSVC 和 ICC 之间进行比较。