自定义可管道视图适配器带Lambda捕获时与join管道编译失败
自定义split_when视图适配器带捕获Lambda编译失败问题排查
问题概述
自定义的可管道split_when视图适配器,无捕获Lambda版本可正常运行,甚至能配合std::views::join工作,但使用带捕获Lambda时编译失败,报错提示operator|不匹配,类型不兼容。
正常运行示例
无捕获Lambda版本
auto main() -> int { auto v = std::vector<int>{1, 2, 3, 4, 5}; auto split = v | so::views::splitWhen([](int n) { return n % 2 == 0; }); for (auto range : split) { for (auto e : range) { std::cout << e << ' '; } std::cout << '\n'; } }
输出:
1 2 3 4 5
配合std::views::join版本
auto main() -> int { auto v = std::vector<int>{1, 2, 3, 4, 5}; auto split = v | so::views::splitWhen([](int n) { return n % 2 == 0; }) | std::views::join; for (auto e : split) { std::cout << e << ' '; } }
输出:
1 2 3 4 5
带捕获Lambda的错误示例
auto main() -> int { auto v = std::vector<int>{1, 2, 3, 4, 5}; auto divisor = 2; auto split = v | so::views::splitWhen([divisor](int n) { return divisor % 2 == 0; }) | std::views::join; for (auto e : split) { std::cout << e << ' '; } }
编译错误信息
error: no match for 'operator|' (operand types are 'so::views::SplitWhenView<std::ranges::ref_view<std::vector<int> >, main()::<lambda(int)> >' and 'const std::ranges::views::_Join')
note: 'std::ranges::views::__adaptor::_RangeAdaptorClosure' is not a base of 'so::views::SplitWhenView<std::ranges::ref_view<std::vector<int> >, main()::<lambda(int)> >'
完整实现代码
自定义bind_back实现
namespace so { template <class ConstFn, class... Args> constexpr auto bind_back(ConstFn fn, Args&& ... args) { using F = ConstFn; if constexpr (std::is_pointer_v<F> or std::is_member_pointer_v<F>) static_assert(fn != nullptr); return [fn, ... bound_args( std::forward<Args>(args))]<class... T> ( T &&... call_args ) { return std::invoke(fn, std::forward<T>(call_args)..., bound_args...); }; } }
P2387辅助工厂
namespace so::views { template <typename F> class closure : public std::ranges::range_adaptor_closure<closure<F>> { F f; public: constexpr closure(F f) : f(f) { } template <std::ranges::viewable_range R> requires std::invocable<F const&, R> constexpr decltype(auto) operator()(R&& r) const { return f(std::forward<R>(r)); } }; template <typename F> class adaptor { F f; public: constexpr adaptor(F f) : f(f) { } template <typename... Args> constexpr decltype(auto) operator()(Args&&... args) const { if constexpr (std::invocable<F const&, Args...>) { return f(std::forward<Args>(args)...); } else { return closure(bind_back(f, std::forward<Args>(args)...)); } } }; }
SplitWhenView及迭代器实现
namespace so::views { template <std::ranges::input_range View, std::predicate<std::ranges::range_value_t<View>> F> requires std::ranges::view<View> class SplitWhenView : public std::ranges::view_interface<SplitWhenView<View, F>> { View base_; F predicate_; public: SplitWhenView() = default; SplitWhenView(View base, F predicate) : base_(std::move(base)), predicate_(std::move(predicate)) {} auto base() { return base_; } template <std::input_iterator It, class Predicate> class SplitWhenIterator; using iterator = SplitWhenIterator<std::ranges::iterator_t<View>, F*>; auto begin() { return SplitWhenIterator(std::ranges::begin(base_), std::ranges::end(base_), &predicate_); } auto end() { return SplitWhenIterator(std::ranges::end(base_), std::ranges::end(base_), &predicate_); } }; template <std::ranges::input_range View, std::predicate<std::ranges::range_value_t<View>> F> requires std::ranges::view<View> template <std::input_iterator It, class Predicate> class SplitWhenView<View, F>::SplitWhenIterator { It current_, end_; Predicate predicate_; public: using value_type = std::ranges::subrange<It>; using difference_type = std::ranges::range_difference_t<View>; SplitWhenIterator() = default; SplitWhenIterator(It begin, It end, Predicate func) : current_{begin}, end_{end}, predicate_{func} {} auto operator++() -> SplitWhenIterator& { current_ = std::ranges::find_if(current_, end_, *predicate_); if (current_ != end_) ++current_; return *this; } auto operator++(int) -> auto { if constexpr (std::forward_iterator<It>) { auto tmp = *this; ++*this; return tmp; } else ++*this; } auto operator*() const -> std::ranges::subrange<It> { auto next = std::ranges::find_if(current_, end_, *predicate_); if (next != end_) return {current_, std::next(next)}; return {current_, next}; } auto operator<=>(const SplitWhenIterator& rhs) const -> auto { return std::tie(current_, end_) <=> std::tie(rhs.current_, rhs.end_); } auto operator==(const SplitWhenIterator& rhs) const -> bool { return std::tie(current_, end_) == std::tie(rhs.current_, rhs.end_); } }; template <std::ranges::range R, std::predicate<std::ranges::range_value_t<R>> F> SplitWhenView(R&&, F) -> SplitWhenView<std::views::all_t<R>, F>; }
splitWhen适配器定义
namespace so::views { inline constexpr adaptor splitWhen = []<std::ranges::viewable_range Range, std::predicate<std::ranges::range_value_t<Range>> Predicate>( Range&& range, Predicate&& predicate ) { return SplitWhenView(std::forward<Range>(range), std::forward<Predicate>(predicate)); }; }
问题原因分析
核心问题出在SplitWhenIterator的设计:
- 原实现中,迭代器直接存储了谓词
predicate_的副本。对于无捕获Lambda(无状态),所有副本行为等价,迭代器的operator==仅比较current_和end_不会触发问题;但带捕获Lambda是有状态的,不同副本的谓词可能行为不同,此时operator==忽略predicate_的比较,违反了C++迭代器的equality_comparable概念要求(相等的迭代器必须有一致的解引用/递增行为)。 - 由于迭代器不满足
input_iterator概念,导致SplitWhenView无法满足std::ranges::input_range,而std::views::join要求输入的范围必须是input_range,因此编译器找不到匹配的operator|重载。
解决方法
修改迭代器的设计,让其存储指向SplitWhenView中谓词的指针,而非副本,这样所有迭代器共享同一个谓词实例,无需比较谓词即可满足迭代器的相等性要求:
具体修改步骤
- 调整SplitWhenIterator的模板参数和成员:
- 将迭代器的模板参数
Predicate改为指针类型(对应SplitWhenView中F的指针) - 成员变量
predicate_改为指针类型,不再存储副本
- 将迭代器的模板参数
- 修改SplitWhenView的iterator类型定义:
using iterator = SplitWhenIterator<std::ranges::iterator_t<View>, F*>; - 修改SplitWhenView的begin/end方法:
传递谓词的地址给迭代器:auto begin() { return SplitWhenIterator(std::ranges::begin(base_), std::ranges::end(base_), &predicate_); } auto end() { return SplitWhenIterator(std::ranges::end(base_), std::ranges::end(base_), &predicate_); } - 修改迭代器中谓词的调用方式:
解引用指针后调用谓词,例如:current_ = std::ranges::find_if(current_, end_, *predicate_);
修改后的代码既避免了带捕获Lambda的复制问题,又满足了迭代器的概念要求,带捕获Lambda的场景即可正常编译运行。
内容的提问来源于stack exchange,提问作者Fureeish
相关产品推荐
相关产品推荐

