如何高效获取non-sized range的最后n个元素?(C++20/23或Range-v3)
问题示例代码
#include <range/v3/all.hpp> namespace views3 = ranges::views; int main() { std::string_view sv = "Hello world! Goodbye world!"; auto notBang = [](char ch) { return ch != '!'; }; auto input = std::views::take_while(sv, notBang); static_assert(!std::ranges::sized_range<decltype(input)>); static_assert(!std::ranges::common_range<decltype(input)>); static_assert(std::ranges::contiguous_range<decltype(input)>); auto v1 = input | views3::take_last(3); // 编译错误 std::cout << (v1 | ranges::to<std::string>) << "\n"; // 期望输出:rld auto v2 = input | views3::drop_last(3); // 正常工作 std::cout << (v2 | ranges::to<std::string>) << "\n"; // 输出:Hello wo }
问题分析
上述代码里的input是**连续(contiguous)、非公共(non-common)、非 Sized(non-sized)**的range。Range-v3的take_last(n)没法处理这类range,因为它的实现逻辑是构造时计算ranges::begin(input) + (ranges::size(input) - n),而非 Sized range没有ranges::size()方法。
但drop_last(n)可以正常处理非 Sized range——它通过维护两个间隔n个元素的迭代器,同步向前遍历。当drop_last遍历完成后,两个迭代器之间的范围恰好就是take_last(n)想要的结果,但没法直接获取这个范围。
解决方案
1. Range-v3 反转两次实现(适用于双向迭代器范畴以上的range)
如果你的range支持双向遍历,可以通过反转range取前n个元素再反转回来的方式实现,逻辑简单直观:
auto v1 = input | views3::reverse | views3::take(3) | views3::reverse; std::cout << (v1 | ranges::to<std::string>) << "\n"; // 输出:rld
这种方法时间复杂度O(N),空间复杂度仅迭代器开销(O(1)),对于连续range来说反转迭代器的额外开销可以忽略。
2. 手动实现适配非 Sized range 的 take_last
参考drop_last的双迭代器思路,我们可以实现一个通用的take_last,支持所有输入迭代器范畴的range:
template <std::ranges::input_range R> requires (!std::ranges::sized_range<R>) auto take_last(R&& r, std::size_t n) { using namespace ranges; if (n == 0) return views::empty<typename range_value_t<R>>; auto it = begin(r); auto lead = it; // 先让领先迭代器前进n步 for (std::size_t i = 0; i < n && lead != end(r); ++i) { ++lead; } // 同步前进两个迭代器,直到领先迭代器到达末尾 while (lead != end(r)) { ++it; ++lead; } // 返回从当前迭代器到末尾的子range return views::subrange(it, end(r)); }
调用方式:
auto v1 = take_last(input, 3); std::cout << (v1 | ranges::to<std::string>) << "\n"; // 输出:rld
这个实现和drop_last逻辑一致,时间复杂度O(N),空间复杂度O(1),适配所有非 Sized 的输入range。
3. C++23 标准库原生支持
如果你的编译器支持C++23,直接使用标准库的std::views::take_last即可——它已经适配了非 Sized range(只要range属于双向迭代器范畴以上):
auto v1 = input | std::views::take_last(3);
补充说明
Range-v3的take_last确实存在这个局限性,依赖range的size()方法导致无法处理非 Sized range。对比它仅66行的简化实现和drop_last的373行完整实现,这更像是早期的设计妥协,而非刻意的功能限制。
内容的提问来源于stack exchange,提问作者Quuxplusone

