我已经使用范围有一段时间了,每次使用过滤时,我都觉得它可以轻松地用普通的旧
if
和 continue
语句替换。考虑这些代码片段:
for (auto value : values | ranges::views::filtered([](auto value) { /* ... */ })) {
// ... do something with value
}
for (auto value : values) {
if (/* ... */) continue;
// ... do something with value
}
对我来说,第二个选项看起来更具可读性。此外,第一个选项可能会涉及一些额外的调用开销。那么过滤有哪些优点呢?是否有一些关于我何时应该选择其中之一的指南?
第一个版本是 C++ 笨拙的 lambda 语法的受害者。如果您已经有一个现成的谓词或使用 lambda 库来表示短 lambda,再加上命名空间别名,那么看起来会更好看。
namespace srv = std::ranges::views;
for (auto const& value : values | srv::filter(&Widget::is_visible)) {
// ...
}
现在,它的优点是其意图非常明确,而不仅仅是像早期的 continue 那样只是一种模式。您可能会从优化构建中的两个版本中获得非常相似的程序集。
这感觉像是一个基于意见的问题,但是让我给你和我在 JavaScript 或任何其他带有集合/序列操作库的语言中问这个问题时给出的相同的观点:
(几乎)您对奇特视图和范围所做的所有操作都可以通过调用
reduce
来完成(它在 C++ 中有很多名称,来自 C++23 的std::ranges::fold_left
,来自 C++17 的 std::reduce
)和
std::accumulate
)。用
reduce
能做的一切都可以用 for 循环来完成。
当您执行单个操作时,几乎总是有一个算法。如果没有,你可以自己写一个。当循环中有多个操作时,事情会变得更加复杂。在这种情况下,范围通常会变得更具可读性、更具声明性,并且其意图更清晰(即自记录代码)。
我会尝试举一个例子来强调这一点:
auto input = get_vector<int>();
std::vector<widget_type> output{};
output.reserve(input.size());
for (auto item : input) {
if (item % 2 == 0) continue;
auto widget = get_widget_by_index(item + 1);
if (widget.hidden()) continue;
output.emplace_back(std::move(widget));
}
output.shrink_to_fit();
很容易阅读,但与此相比:
auto output = views::all(get_vector<int>()) // views::all is to allow the immediate use of temporaries without lifetime issues
| views::filter([](auto i) { return i % 2 != 0; })
| views::transform([](auto i) { return i + 1 })
| views::transform(&get_widget_by_index)
| views::filter(&widget_type::hidden)
| ranges::to<std::vector>();
continue
)更容易阅读,与旧的 C++ 标准兼容,并且非常接近后面的实际执行。第一个版本很可能会优化为非常接近的二进制代码,但在我看来,它的语法过于复杂,并且涉及后面模板类的大量编译时优化。
第一个版本的优势为零。你的想法很好,随着最近的 C++ 功能,人们往往会过度思考并导致迭代膨胀。 编辑:评论指出我对临时过滤列表分配的看法是错误的。因此,这两个代码最终可能会产生完全相同的指令。然而,第二个版本更简单、更易读并且与旧的 C++ 标准兼容且没有缺点的论点仍然成立。 您还有一个略有不同的版本:
for (auto value : values) {
if (!...) {
//Do stuff here
}
}
我倾向于更喜欢那个,因为控制流在视觉上比
continue
更明显。但这完全是个人品味问题。