C++20部分ranges视图与std::copy不兼容问题排查
std::views::take时std::copy编译失败,换成drop_while就正常? 问题重现
我写了一段处理vector的代码,用filter+transform+take(2)生成视图后,想把结果复制到新容器里,结果编译报错。但把take(2)换成drop_while就完全正常,明明都是视图操作,为啥复制会失败?
代码示例:
#include <ranges> #include <vector> #include <iostream> #include <unordered_map> using namespace std; int main() { // std::views::drop_while([](const int val){return val < 9;}) | vector v{1,2,3,4,5,6,7,4,8,9}; std::ranges::take_view res = v | std::views::filter([](const int val){return val %2 == 0;}) | std::views::transform([](const int val){return val*3;}) | std::views::take(2); decltype(v) filtered; std::copy(res.begin(),res.end(),std::back_inserter(filtered)); for (const auto val : filtered) { std::cout << "val: " << val << '\n'; } return 0; }
编译错误(翻译后):
main.cpp: 在函数‘int main()’中:
main.cpp:23:14: 错误:没有匹配的函数调用‘copy(std::counted_iterator<...>, std::ranges::take_view<...>::_Sentinel, std::back_insert_iterator<std::vector>)’
23 | std::copy(res.begin(),res.end(),std::back_inserter(filtered));
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
从/usr/include/c++/11/bits/char_traits.h:39包含的文件中:
...(中间包含路径省略)...
/usr/include/c++/11/bits/stl_algobase.h:611:5: 注意:候选:‘template<class _II, class _OI> constexpr _OI std::copy(_II, _II, _OI)’
611 | copy(_II __first, _II __last, _OI __result)
| ^~~~
/usr/include/c++/11/bits/stl_algobase.h:611:5: 注意: 模板参数推导/替换失败:
main.cpp:23:14: 注意: 推导的参数‘_II’类型冲突(‘std::counted_iterator<...>’ 和 ‘std::ranges::take_view<...>::_Sentinel’)
23 | std::copy(res.begin(),res.end(),std::back_inserter(filtered));
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
...(其他候选省略)...
问题原因
核心矛盾是传统std::copy要求前两个参数必须是相同类型的迭代器,但std::take_view的设计特殊:
res.begin()返回的是std::counted_iterator类型(自带剩余元素计数的迭代器)res.end()返回的是std::default_sentinel_t(一个空的哨位类型,没有迭代器的行为)
这俩类型完全不匹配,导致std::copy的模板参数_II无法同时推导两个类型,直接触发编译错误。
而std::drop_while_view的begin()和end()返回的是同一种迭代器类型(都是适配后的底层容器迭代器),所以std::copy能顺利完成模板推导,编译通过。
修复方案
有两种简单可行的解决方式:
1. 使用C++20范围版本的std::ranges::copy(推荐)
这是专门为C++20范围视图设计的算法,天然支持迭代器和哨位类型不同的场景,代码也更简洁:
std::ranges::copy(res, std::back_inserter(filtered));
不用手动传begin()和end(),直接把整个视图传进去就行,完全符合范围库的设计意图。
2. 显式指定std::copy的模板参数
如果你非要用传统的std::copy,可以手动指定迭代器类型,跳过自动推导:
std::copy<decltype(res.begin())>(res.begin(), res.end(), std::back_inserter(filtered));
不过这种方式代码冗余,不如范围版优雅,不推荐。
另外可以把res的声明改成auto,让代码更简洁(这不是解决编译错误的关键,但能优化代码):
auto res = v | std::views::filter([](const int val){return val %2 == 0;}) | std::views::transform([](const int val){return val*3;}) | std::views::take(2);
内容的提问来源于stack exchange,提问作者Deborah C

