首页 > 解决方案 > 读取一些数据后两次提升限制给出了错误的结果

问题描述

这个案例描述起来很复杂,我只展示一些代码片段

ifstream ifs = xxx; // total length 1200 bytes
// do something that read 200 bytes from ifs;
filtering_istream limitedIn(restrict(ifs, 0/*off*/, 1000/*len*/));
// do something that read 700 bytes from limitedIn; we still got 300 bytes to read
filtering_istream limitedIn2(restrict(limitedIn, 0/*off*/, 300/*len*/)); 
// but now I can only read 100 bytes from limitedIn2 because when restrict limitedIn, it called limitedIn.seek(0, cur) which in turn calls ifs.seek(0, cur) which return 900 and updated limitedIn _pos to 900

有什么简单的方法可以避免这个问题吗?如果我能够在 ifstream 周围获得一个流包装器,seek即使 ifstream 返回 200,它也会返回 0,事情会变得简单得多。

工作代码示例

    ofstream out(R"(C:\Users\nick\AppData\Local\Temp\bugTest)", ios_base::binary);
    char* content = new char[1200];
    out.write(content, 1200);
    out.flush();
    out.close();
    ifstream in(R"(C:\Users\nick\AppData\Local\Temp\bugTest)", ios_base::binary);
    in.read(content, 200);
    filtering_istream limitedIn(restrict(in, 0, 1000));
    limitedIn.read(content, 700);
    filtering_istream limitedIn2(restrict(limitedIn, 0, 300));
    std::cout << limitedIn2.rdbuf()->sgetn(content, 300); // print 100
    delete []content;

标签: c++boost

解决方案


我创建了一个更详细的独立示例,可以更好地显示正在发生的事情:

住在神螺栓上

#include <boost/iostreams/filtering_stream.hpp>
#include <boost/iostreams/restrict.hpp>
#include <fstream>
#include <fmt/ranges.h>
#include <span>

int main() {
    constexpr int capacity = 12;
    std::array<char, capacity> content{'a','b','c','d','e','f','g','h','i','j','k','l'};
    {
        std::ofstream out(R"(bugTest)", std::ios::binary);
        out.write(content.data(), content.size());
        out.flush();
    }
    using boost::iostreams::restrict;
    using boost::iostreams::filtering_istream;

    auto read_n = [](auto&& is, size_t n) {
        std::array<char, capacity> content{0};
        bool ok(is.read(content.data(), n));
        fmt::print("{} read {} bytes out of {}: {}\n",
                ok?"successful":"failed",
                is.gcount(), n,
                std::span(content.data(), is.gcount()));
    };

    std::ifstream in("bugTest", std::ios::binary);
    read_n(in, 2);

    filtering_istream limitedIn(restrict(in, 0, 10));
    read_n(limitedIn, 7);
    read_n(filtering_istream(restrict(limitedIn, 0, 3)), 3);
}

印刷

successful read 2 bytes out of 2: {'a', 'b'}
successful read 7 bytes out of 7: {'c', 'd', 'e', 'f', 'g', 'h', 'i'}
failed read 1 bytes out of 3: {'j'}

设备表现得“更好”吗?

概念上的问题似乎是一个的重复限制。也许您可以使用设备

住在神螺栓上

#include <boost/iostreams/filtering_stream.hpp>
#include <boost/iostreams/device/file.hpp>
#include <boost/iostreams/restrict.hpp>
#include <fstream>
#include <fmt/ranges.h>
#include <span>

int main() {
    constexpr int capacity = 12;
    std::array<char, capacity> content{'a','b','c','d','e','f','g','h','i','j','k','l'};
    {
        std::ofstream out(R"(bugTest)", std::ios::binary);
        out.write(content.data(), content.size());
        out.flush();
    }
    using boost::iostreams::restrict;
    using boost::iostreams::filtering_istream;
    using boost::iostreams::is_device;

    auto read_n = [](auto&& str_or_dev, size_t n) {
        std::array<char, capacity> content{0};
        bool ok = true;
        size_t count = 0;
        if constexpr (is_device<std::decay_t<decltype(str_or_dev)>>::value) {
            if (auto actual = str_or_dev.read(content.data(), n); actual != -1) {
                ok = true;
                count = actual;
            } else {
                ok = false;
                count = 0;
            }
        } else {
            ok = str_or_dev.good();
            count = str_or_dev.gcount();
        }

        fmt::print("{} read {} bytes out of {}: {}\n",
                ok?"successful":"failed", count, n,
                std::span(content.data(), count));
    };

    boost::iostreams::file_source in("bugTest", std::ios::binary);
    read_n(in, 2);

    auto limitedIn(restrict(in, 0, 10));
    read_n(limitedIn, 7);
    read_n(restrict(limitedIn, 0, 3), 3);
}

确实

  • 简化代码
  • 可能会提高性能
  • 有所作为(因为最终阅读被认为是“成功的”)
  • 没有区别(在某种意义上,窗口计算异常仍然存在)

现在怎么办

到目前为止,看起来我们正处于简单的库错误/未记录的限制领域。

我会让你知道无论如何我认为输出令人惊讶,我希望第二次阅读可以重新开始'a',尤其是在设备公式中。那是因为offset = 0对我来说意味着绝对的寻求。

让我们在每次读取时检测相对于限制的实际流位置:

auto pos = [](auto&& str_or_dev) {
    return str_or_dev.seek(0, std::ios::cur);
};

进而:

auto read_n = [pos](auto&& str_or_dev, size_t n) {
    fmt::print("read at #: {}\n", pos(str_or_dev));
    // ...

这会带来更多的洞察力!它打印

read at #: 0    
successful read 2 bytes out of 2: {'a', 'b'}    
read at #: 2    
successful read 7 bytes out of 7: {'c', 'd', 'e', 'f', 'g', 'h', 'i'}    
read at #: 9    
failed read 0 bytes out of 3: {}

哇。我们了解到:

  • 实际上,即使相对于限制,我们也不会从“9”开始。所以我认为第二次阅读应该从逻辑上开始的期望'a'似乎成立
  • 仅在读取之前观察位置会导致最后一次读取返回-1,这在某种程度上比返回 1 个字符更有意义

事实上,查看相关的构造函数,我们可以推断出restriction底层源是在原点的硬假设:

template<typename Device>
restricted_indirect_device<Device>::restricted_indirect_device
    (param_type dev, stream_offset off, stream_offset len)
    : device_adapter<Device>(dev), beg_(off), pos_(off), 
      end_(len != -1 ? off + len : -1)
{
    if (len < -1 || off < 0)
        boost::throw_exception(BOOST_IOSTREAMS_FAILURE("bad offset"));
    iostreams::skip(this->component(), off);
}

是无条件的skip,不观察 的流位置dev。现在,在您的情况下,您认为这并不重要,因为off它始终为 0,但流位置不是。

反例:

在每次读取之前寻找底层设备确实会消除所有意外影响:

in.seek(0, std::ios::beg);
read_n(in, 2);

auto limitedIn(restrict(in, 0, 10));

in.seek(0, std::ios::beg);
read_n(limitedIn, 7);

in.seek(0, std::ios::beg);
read_n(restrict(limitedIn, 0, 3), 3);

打印

read at #: 0    
successful read 2 bytes out of 2: {'a', 'b'}
read at #: 0    
successful read 7 bytes out of 7: {'a', 'b', 'c', 'd', 'e', 'f', 'g'}
read at #: 0
successful read 3 bytes out of 3: {'a', 'b', 'c'}

当然,这不是你想要的,但它有助于理解它的作用,所以你知道该说些什么来得到我们想要的。

现在,通过寻求受限的开始,我们将解决这个问题:https ://godbolt.org/z/r11zo8 。现在最后一个read_n触发 a bad_seek,但令人惊讶的是,仅在第二行中:

str_or_dev.seek(0, std::ios::beg);
str_or_dev.seek(0, std::ios::cur);

更糟糕的是,两次寻求开始限制的行为突然与寻求底层设备为0的行为相同?!

str_or_dev.seek(0, std::ios::beg);
str_or_dev.seek(0, std::ios::beg);

在 Godbolt 上看到它

successful read 2 bytes out of 2: {'a', 'b'}
successful read 7 bytes out of 7: {'a', 'b', 'c', 'd', 'e', 'f', 'g'}
successful read 3 bytes out of 3: {'a', 'b', 'c'}

错误报告时间

可悲的是,这意味着可能是时候提交错误报告了。当然,您可以自己避免使用嵌套限制,但这在您的实际代码库(可能包括通用流)中可能不太容易做到。

有许多快速而肮脏的技巧可以使限制从底层组件“实现”其位置(如在上面的顿悟中,我们发现“仅在读取之前观察位置会导致最后一次读取返回-1

我不会建议其中任何一个,因为如果您有超过 2 个级别的限制,它们也会崩溃 - 可能被一些更具适应性的层隐藏。

这是需要在库中修复的东西。至少文档可能会声明底层组件在创建受限设备或过滤器时处于起始位置。


推荐阅读