在C++中,split_view和lazy_split_view有什么区别?

7

我已经阅读了最新的草案,其中添加了 lazy_split_view

但后来我意识到,split_view 被重命名为 lazy_split_view,而 split_view 也被更新了。

libstdc++ 最近也通过使用 GCC Trunk 版本 https://godbolt.org/z/9qG5T9n5h 实现了这一功能。

我有一个简单的程序,在这个程序中展示了两个视图的用法,但我看不出它们之间的区别:

#include <iostream>
#include <ranges>

int main(){

    std::string str { "one two three  four" };

    for (auto word : str | std::views::split(' ')) {
        for (char ch : word)
            std::cout << ch;
        std::cout << '.';
    }

    std::cout << '\n';

    for (auto word : str | std::views::lazy_split(' ')) {
        for (char ch : word)
            std::cout << ch;
        std::cout << '.';
    }

}

输出:

one.two.three..four.
one.two.three..four.

直到我使用std::span<const char>来作为两个视图的时候,才注意到它们之间的差异。

在第一个视图中:std::views::split

for (std::span<const char> word : str | std::views::split(' '))

编译器已接受我的代码。

而在第二个示例中: std::views::lazy_split

for (std::span<const char> word : str | std::views::lazy_split(' ')) 

抛出编译错误。

我知道这两者之间会有区别,但是我不容易发现它们。 这是C++20中的缺陷报告还是C++23中的新功能(带有变化),或者两者都有?


2
通常情况下,惰性求值会在需要时按需发生。换句话说,与创建该值时相比,惰性计算仅在读取该值时进行计算。 - Timo
1个回答

8
我查看了相关文件(Barry Revzin的P2210R2),split_view已更名为lazy_split_view。新的split_view与旧版本不同,它提供了一种不同的结果类型,可以保留源范围的类别。
例如,我们的字符串str是一个连续的范围,因此split将产生一个连续的子范围。以前它只会给你一个前向范围。如果您尝试进行多次操作或获取底层存储的地址,则可能会出现问题。
从论文的例子来看:
std::string str = "1.2.3.4";
auto ints = str 
    | std::views::split('.')
    | std::views::transform([](auto v){
        int i = 0;
        std::from_chars(v.data(), v.data() + v.size(), i);
        return i;
    });

现在可以工作,但是

std::string str = "1.2.3.4";
auto ints = str 
    | std::views::lazy_split('.')
    | std::views::transform([](auto v){
        int i = 0;
        // v.data() doesn't exist
        std::from_chars(v.data(), v.data() + v.size(), i);
        return i;
    });

由于范围v仅为前向范围,因此无法提供data()成员,所以不会工作。

原始答案

我曾认为split也必须是惰性的(惰性是ranges提案的卖点之一),因此我进行了一个小实验

struct CallCount{
    int i = 0;

    auto operator()(auto c) {
        i++;
        return c;
    }

    ~CallCount(){
        if (i > 0) // there are a lot of copies made when the range is constructed
            std::cout << "number of calls: " << i << "\n";
    }
};


int main() {
    
    std::string str = "1 3 5 7 9 1";

    std::cout << "split_view:\n";

    for (auto word : str | std::views::transform(CallCount{}) | std::views::split(' ') | std::views::take(2)) {
    }

    std::cout << "lazy_split_view:\n";

    for (auto word : str | std::views::transform(CallCount{}) | std::views::lazy_split(' ') | std::views::take(2)) {
    }    
}

这段代码会打印输出(注意,transform 操作会作用于字符串中的每个字符):

split_view:
number of calls: 6
lazy_split_view:
number of calls: 4

那么会发生什么?

实际上,这两种观点都很懒惰。但是它们的懒惰程度有所不同。我在split之前放置的transform只是计算调用次数。结果表明,split急切地计算出下一个项目,而lazy_split在遇到当前项目后立即停止。

您可以看到字符串str由标记其字符索引(从1开始)的数字组成。 take(2)应该在我们在str中看到“3”后停止循环。确实,lazy_split在“3”后的空格处停止,但split在“5”后的空格处停止。

这基本上意味着split热切地获取其下一个项目而不是惰性的。这种差异可能大多数时候都不重要,但它可能会影响对性能至关重要的代码。

我不知道这是否是更改的原因(我没有读过论文)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接