结合正则表达式和范围会导致内存问题

Combining regex and ranges causes memory issues

我想对 textregex 的所有子匹配构建一个视图。以下是定义此类视图的两种方法:

    char const text[] = "The IP addresses are: 192.168.0.25 and 127.0.0.1";
    std::regex regex{R"((\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}))"};

    auto sub_matches_view = 
        std::ranges::subrange(
            std::cregex_iterator{std::ranges::begin(text), std::ranges::end(text), regex},
            std::cregex_iterator{}
        ) |
        std::views::join;

    auto sub_matches_sv_view = 
        std::ranges::subrange(
            std::cregex_iterator{std::ranges::begin(text), std::ranges::end(text), regex},
            std::cregex_iterator{}
        ) |
        std::views::join |
        std::views::transform([](std::csub_match const& sub_match) -> std::string_view { return {sub_match.first, sub_match.second}; });

下面是上述范围的用法示例:

for(auto const& sub_match : sub_matches_view) {
    std::cout << std::string_view{sub_match.first, sub_match.second} << std::endl; // #1
}

for(auto const& sv : sub_matches_sv_view) {
    std::cout << sv << std::endl; // #2
}

Loop #1 工作没有问题 - 打印结果是正确的。但是,根据 Address Sanitizer,loop #2 会导致堆释放后使用问题。 事实上,只是循环 sub_matches_sv_view 而根本不访问元素也会导致这个问题。 Here 是 Compiler Explorer 上的代码,也是 Address Sanitizer 的输出。

我不知道我的错误在哪里。 textregex 永远不会超出范围,我没有看到任何可能在其生命周期之外访问的迭代器。 std::csub_match 对象将迭代器 (.first.second) 保存到 text 中,所以我认为在构造 std::string_view 之后它不需要自己保持活动状态std::views::transform.

我知道还有许多其他方法可以迭代正则表达式匹配,但我对导致我的程序内存错误的原因特别感兴趣,我不需要解决此问题的方法。

问题是 std::regex_iterator 以及它隐藏的事实。


那个类型基本上是这样的:

class regex_iterator {
    vector<match> matches;

public:
    auto operator*() const -> vector<match> const& { return matches; }
};

这意味着,例如,即使此迭代器的引用类型是 T const&,如果您有同一迭代器的两个副本,它们实际上会为您提供对不同对象的引用。

现在,join_view<R>::iterator基本上是这样的:

class iterator {
    // the iterator into the range we're joining
    iterator_t<R> outer;

    // an iterator into *outer that we're iterating over
    iterator_t<range_reference_t<R>> inner;
};

其中,对于 regex_iterator,大致如下所示:

class iterator {
    // the regex matches
    vector<match> outer;

    // the current match
    match* inner;
};

现在,当你复制这个迭代器时会发生什么?副本的inner仍然指的是原件的outer!这些实际上并不像您期望的那样独立。这意味着如果原来的超出范围,我们就会有一个悬垂的迭代器!

这就是您在这里看到的:transform_view 最终复制迭代器(当然允许这样做),现在您有一个悬空的迭代器(libc++ 的实现改为移动,这就是为什么它恰好有效 in this case as 康桓瑋 )。但是我们可以在没有 transform 的情况下重现同样的问题,只要我们复制迭代器并销毁原始的。例如:

#include <ranges>
#include <regex>
#include <iostream>
#include <optional>

int main() {
    std::string_view text = "The IP addresses are: 192.168.0.25 and 127.0.0.1";
    std::regex regex{R"((\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}))"};

    auto a =  std::ranges::subrange(
            std::cregex_iterator(std::ranges::begin(text), std::ranges::end(text), regex),
            std::cregex_iterator{}
        );

    auto b = a | std::views::join;

    std::optional i = b.begin();
    std::cout << std::string_view((*i)->first, (*i)->second) << '\n'; // fine

    auto j = *i;
    i.reset();
    std::cout << std::string_view(j->first, j->second) << '\n'; // boom
}

我不确定这个问题的解决方案是什么样的,但原因是 std::regex_iterator 而不是 views::joinviews::transform.