C++23范围适配器为何要求可调用对象满足copy_constructible?相关设计考量探究
咱们先从实际遇到的问题说起:在C++20的范围适配器里,像transform_view、filter_view这类组件,内部用copyable-box<F>来存储传入的可调用对象,这就要求可调用对象F必须满足copy_constructible约束。
比如下面这段代码就会编译失败——因为MoveOnlyFun持有std::unique_ptr(仅移动类型),没法被复制:
#include <ranges> #include <memory> struct MoveOnlyFun { std::unique_ptr<int> x; MoveOnlyFun(int x) : x(std::make_unique<int>(x)) { } int operator()(int y) const { return *x + y; } }; int main() { // 编译错误:MoveOnlyFun不可复制,不符合transform_view的要求 auto r = std::views::iota(0, 5) | std::views::transform(MoveOnlyFun(1)); }
这时候你肯定会疑惑:视图本身并没有强制要求具备copy_constructible特性,为什么要对内部的可调用对象提这个要求?为什么不用moveable-box来替代copyable-box呢?
核心设计考量
1. 迭代器的可复制语义依赖
范围适配器的迭代器是其核心组成部分,而C++标准中,绝大多数范围操作(比如range-based for循环、标准算法对范围的遍历)都依赖迭代器的可复制性——毕竟遍历过程中会多次复制迭代器。
如果适配器用moveable-box存储仅移动的可调用对象,那么迭代器在复制时就会遇到问题:迭代器要么需要持有可调用对象的引用(但这会带来生命周期悬垂的风险),要么只能移动而不能复制,这就违反了多数迭代器的基本语义要求(除输入迭代器外,标准要求前向迭代器及以上必须可复制)。
2. 视图的“轻量、可复用”定位
C++标准中,视图的设计初衷是轻量的、无所有权的、可多次复用的——它只是对底层范围的“观察”,不应该持有独占资源。如果允许仅移动的可调用对象进入视图,视图本身就会变成仅移动类型,失去了“可复用、可复制”的特性,违背了视图的设计定位。
3. 早期设计的简化性
C20引入范围特性时,为了降低设计和实现的复杂度,优先选择了更简单的可复制语义。支持仅移动可调用对象需要解决一系列边缘问题:比如迭代器的移动语义与复制语义的兼容、视图的状态管理等,这些在C20的时间窗口内没有完全落地。
现有解决方案:P2494R0提案
针对这个问题,近期的P2494R0提案给出了详尽的解决方案。它调整了范围适配器的内部存储机制,允许适配器根据可调用对象的特性自动选择copyable-box或moveable-box存储,同时优化了迭代器的行为,在不破坏现有代码兼容性的前提下,支持仅移动可调用对象的使用。
内容的提问来源于stack exchange,提问作者康桓瑋

