非可拷贝迭代器搭配范围适配器的编译问题与解决方案问询
问题描述
传统C17的InputIterator迭代器要求可拷贝,但C20的std::input_iterator新概念并未强制此要求。然而,当使用自定义的非可拷贝输入迭代器实现范围,并尝试通过std::views::drop(1)这类范围适配器处理时,会出现编译错误,提示迭代器不可拷贝——看起来std::ranges::input_range对迭代器施加了可拷贝的约束。
最小示例代码
#include <iterator> #include <ranges> #include <iostream> class InputIterator { public: using difference_type = std::ptrdiff_t; using value_type = int; // 仅支持移动 InputIterator(InputIterator&&) = default; InputIterator& operator=(InputIterator&&) = default; // 删除拷贝构造与赋值 InputIterator(const InputIterator&) = delete; InputIterator& operator=(const InputIterator&) = delete; int operator*() const { return *p_; } InputIterator& operator++() { ++p_; return *this; } void operator++(int) { ++*this; } // 示例所需的额外成员(非input_iterator概念强制要求) InputIterator() = default; InputIterator(const int* p) : p_(p) {} bool operator==(const InputIterator& other) const = default; bool operator!=(const InputIterator& other) const = default; private: const int* p_ = {}; }; static_assert(std::input_iterator<InputIterator>); class InputView : public std::ranges::view_interface<InputView> { public: InputIterator begin() const { return {array}; }; // RVO避免拷贝 InputIterator end() const { return {array + 4}; }; // RVO避免拷贝 private: int array[4] = {1, 2, 3, 4}; }; int main() { // 正常编译运行 InputView view; // 正常编译运行 for (int x : view) { std::cout << x << " "; } // 编译失败 for (int x : view | std::views::drop(1)) { std::cout << x << " "; } // 编译失败 static_assert(std::ranges::input_range<InputView>); }
编译错误信息
[...] note: because type constraint 'sentinel_for<InputIterator, __range_iter_t<InputView &> >' was not satisfied: note: because 'InputIterator' does not satisfy 'semiregular' note: because 'InputIterator' does not satisfy 'copyable' note: because 'InputIterator' does not satisfy 'copy_constructible' [...]
核心疑问
- 为何
input_range要求底层input_iterator可拷贝,而input_iterator本身无此要求? - 编写仅支持
input_range而非input_iterator的泛型算法是否为不良实践? - 实际中是否存在不可拷贝的
input_iterator?
解答
是否能让范围适配器适配非可拷贝迭代器?
可以,但需要调整范围的迭代器与哨位设计:
标准库范围适配器依赖的std::ranges::input_range要求哨位满足sentinel_for约束。当迭代器与哨位是同一类型时,sentinel_for<I, I>会额外要求I满足semiregular(即可拷贝、可默认构造);但如果将迭代器和哨位拆分为不同类型,仅需保证哨位满足semiregular,迭代器只需符合input_iterator(无需可拷贝)。
修改示例的思路:自定义一个独立的Sentinel类型作为范围的哨位,让InputIterator只负责迭代逻辑,这样就能满足input_range的约束,适配范围适配器。
疑问解答
input_range与input_iterator约束差异的原因:std::input_iterator仅关注迭代器本身的移动、递增、取值等核心能力,不需要可拷贝;但input_range的约束包含sentinel_for<iterator_t<R>, sentinel_t<R>>——当迭代器和哨位是同一类型时,sentinel_for要求该类型是semiregular(即可拷贝),这是为了保证哨位能被安全复制、传递。如果拆分迭代器和哨位类型,迭代器就不需要可拷贝。仅支持
input_range的泛型算法是否为不良实践:
不是。C20引入范围概念的核心目的就是推动基于范围的编程模型,它比传统的迭代器编程更友好,代码更简洁易读。只要算法符合input_range的约束,就是符合现代C规范的实践,无需刻意兼容旧的迭代器接口。实际中的非可拷贝输入迭代器:
存在。比如封装独占资源(如文件句柄、网络连接、自定义输入流)的迭代器——这类迭代器持有资源的唯一所有权,拷贝会导致资源冲突或数据混乱,因此只能设计为仅可移动、不可拷贝。
内容的提问来源于Stack Exchange,提问作者Boris Dalstein

