在 C++ 中读取文件并处理可能的错误的便携式方法

Posted

技术标签:

【中文标题】在 C++ 中读取文件并处理可能的错误的便携式方法【英文标题】:Portable way to read a file in C++ and handle possible errors 【发布时间】:2017-08-23 13:09:39 【问题描述】:

我想做一个简单的事情:从文件中读取第一行,并在没有这样的文件、没有读取文件的权限等情况下进行适当的错误报告。

我考虑了以下选项:

std::ifstream。不幸的是,没有可移植的方法来报告系统错误。其他一些答案建议在阅读失败后检查errno,但标准不保证errno 是由iostreams 库中的任何函数设置的。 C 风格fopen/fread/fclose。这可行,但不如带有std::getline 的iostreams 方便。我正在寻找 C++ 解决方案。

有没有什么方法可以使用 C++14 和 boost 来实现这一点?

【问题讨论】:

是的,有一种方法可以“使用 C++14 完成此任务”。实现你自己的std::streambuf 子类,然后用它来构造一个std::istream。你得到一个普通的流,你的实现可以使用操作系统调用来打开/读取文件,并报告实际的操作系统错误。 好吧,如果一个人的目标是捕获底层操作系统错误,并且仍然有一个普通的std::istream,那么这是唯一的方法。不幸的是,C++ 并没有以拥有大量魔术按钮而闻名,人们只需按下这些按钮即可获得他们正在寻找的结果。许多事情需要大量工作才能正确、正确地实施。 @ArtemyVysotsky 这完全不合适,因为它引入了 TOCTTOU 问题。 @OlegAndriyanov 我相信更广泛使用的术语是“比赛条件”(我不得不谷歌 TOCTTOU 才能得到它) 关于“C 做得更好”。 C 标准严格要求三个与 errno 相关的宏:EDOM、EILSEQ 和 ERANGE,并且不保证任何 I/O 函数将 errno 设置为特定的任何内容。 【参考方案1】:

免责声明:我是 AFIO 的作者。但您正在寻找的正是https://ned14.github.io/afio/,它是结合了 2015 年 8 月 Boost 同行评审反馈的 v2 库。See the list of features here。

我当然会警告说这是一个 alpha 质量库,您不应该在生产代码中使用它。但是,已经有不少人这样做了。

如何使用AFIO解决OP的问题:

请注意,AFIO 是一个非常低级的库,因此您必须键入更多代码才能实现与 iostream 相同的功能,另一方面,您不会获得内存分配、不会抛出异常、不会出现不可预测的延迟峰值:

  // Try to read first line from file at path, returning no string if file does not exist,
  // throwing exception for any other error
  optional<std::string> read_first_line(filesystem::path path)
  
    using namespace AFIO_V2_NAMESPACE;
    // The result<T> is from WG21 P0762, it looks quite like an `expected<T, std::error_code>` object
    // See Outcome v2 at https://ned14.github.io/outcome/ and https://lists.boost.org/boost-announce/2017/06/0510.php

    // Open for reading the file at path using a null handle as the base
    result<file_handle> _fh = file(, path);
    // If fh represents failure ...
    if(!_fh)
    
      // Fetch the error code
      std::error_code ec = _fh.error();
      // Did we fail due to file not found?
      // It is *very* important to note that ec contains the *original* error code which could
      // be POSIX, or Win32 or NT kernel error code domains. However we can always compare,
      // via 100% C++ 11 STL, any error code to a generic error *condition* for equivalence
      // So this comparison will work as expected irrespective of original error code.
      if(ec == std::errc::no_such_file_or_directory)
      
        // Return empty optional
        return ;
      
      std::cerr << "Opening file " << path << " failed with " << ec.message() << std::endl;
    
    // If errored, result<T>.value() throws an error code failure as if `throw std::system_error(fh.error());`
    // Otherwise unpack the value containing the valid file_handle
    file_handle fh(std::move(_fh.value()));
    // Configure the scatter buffers for the read, ideally aligned to a page boundary for DMA
    alignas(4096) char buffer[4096];
    // There is actually a faster to type shortcut for this, but I thought best to spell it out
    file_handle::buffer_type reqs[] = buffer, sizeof(buffer);
    // Do a blocking read from offset 0 possibly filling the scatter buffers passed in
    file_handle::io_result<file_handle::buffers_type> _buffers_read = read(fh, reqs, 0);
    if(!_buffers_read)
    
      std::error_code ec = _fh.error();
      std::cerr << "Reading the file " << path << " failed with " << ec.message() << std::endl;
    
    // Same as before, either throw any error or unpack the value returned
    file_handle::buffers_type buffers_read(_buffers_read.value());
    // Note that buffers returned by AFIO read() may be completely different to buffers submitted
    // This lets us skip unnecessary memory copying

    // Make a string view of the first buffer returned
    string_view v(buffers_read[0].data, buffers_read[0].len);
    // Sub view that view with the first line
    string_view line(v.substr(0, v.find_first_of('\n')));
    // Return a string copying the first line from the file, or all 4096 bytes read if no newline found.
    return std::string(line);
  

【讨论】:

您能否提供一个使用 AFIO 从文件中读取一行的代码示例?这看起来像我需要的,但我还没有找到任何代码示例。 @OlegAndriyanov 这是个好主意。几分钟后回来。 @OlegAndriyanov 添加的使用代码示例。如果您有任何问题,请告诉我。 这看起来像是围绕操作系统调用的非常好的包装器。是否有将 AFIO 纳入 boost 的计划?此外,IMO 最好在 AFIO 中实现 std::streambuf(正如 @SamVarshavchik 建议的那样)以重用格式化 iostream 的 IO 操作的所有功能,但具有可移植的错误处理。还是超出了图书馆的范围? @OlegAndriyanov 我希望在 2019 年返回 AFIO 进行第二次 Boost 同行评审。我在 Coroutines TS、Ranges TS、Expected 和 Span 提案以及 C++ 20 中的其他一些内容上受阻。无需添加 streambuf 支持,AFIO 已经使用 Ranges TS。所以用 Ranges 说 i/o,一切都很好。 Eric 在ericniebler.com/2017/08/17/… 上写了一篇关于此的博客文章,他使用 libuv 而不是 AFIO,因为它更广为人知。但是 AFIO比 libuv 更高效,WG21 目前正在为 iostreams v2 的 AFIO 微笑【参考方案2】:

boost-users 邮件列表中的人指出,boost.beast 库具有独立于操作系统的 API,用于基本文件 IO包括正确的错误处理。开箱即用的file concept 有三种实现:POSIX、stdio 和 win32。这些实现支持 RAII(销毁时自动关闭)和移动语义。 POSIX 文件模型自动处理EINTR 错误。基本上,这对于可移植地逐块读取文件是足够且方便的,例如,显式处理文件不存在的情况:

using namespace boost::beast;
using namespace boost::system;

file f;
error_code ec;
f.open("/path/to/file", file_mode::read, ec);
if(ec == errc::no_such_file_or_directory) 
    // ...
 else 
    // ...

【讨论】:

【参考方案3】:

最好的办法是包装 Boost WinAPI 和/或 POSIX API。

“天真的”C++ 标准库的东西(花里胡哨)不会让你走得太远:

Live On Coliru

#include <iostream>
#include <fstream>
#include <vector>

template <typename Out>
Out read_file(std::string const& path, Out out) 
    std::ifstream s;
    s.exceptions(std::ios::badbit | std::ios::eofbit | std::ios::failbit);
    s.open(path, std::ios::binary);

    return out = std::copy(std::istreambuf_iterator<char>s, , out);


void test(std::string const& spec) try 
    std::vector<char> data;
    read_file(spec, back_inserter(data));

    std::cout << spec << ": " << data.size() << " bytes read\n";
 catch(std::ios_base::failure const& f) 
    std::cout << spec << ": " << f.what() << " code " << f.code() << " (" << f.code().message() << ")\n";
 catch(std::exception const& e) 
    std::cout << spec << ": " << e.what() << "\n";
;

int main() 

    test("main.cpp");
    test("nonexistent.cpp");


打印...:

main.cpp: 823 bytes read
nonexistent.cpp: basic_ios::clear: iostream error code iostream:1 (iostream error)

    当然,您可以通过阅读 &lt;filesystem&gt; 添加更多诊断信息,但是 如前所述,这很容易受到竞争的影响(取决于您的应用程序,这些甚至可能会引发安全漏洞,所以只需说“不”)。

    使用boost::filesystem::ifstream 不会改变引发的异常

    更糟糕的是,使用 Boost IOstream 无法引发任何错误:

    template <typename Out>
    Out read_file(std::string const& path, Out out) 
        namespace io = boost::iostreams;
        io::stream<io::file_source> s;
        s.exceptions(std::ios::badbit | std::ios::eofbit | std::ios::failbit);
        s.open(path, std::ios::binary);
    
        return out = std::copy(std::istreambuf_iterator<char>s, , out);
    
    

    打印愉快:

    main.cpp: 956 bytes read
    nonexistent.cpp: 0 bytes read
    

    Live On Coliru

【讨论】:

这些例子有什么用?证明他们没有做我想做的事?我不认为手动 API 包装是一个好主意,因为这个任务很容易用任何现代语言在几行中解决。甚至 C 也比 C++ 做得更好。 信息就是一切。如果你知道,你可以在你的问题中展示它。另外,I share the rant,但这不是一个有用的答案。 (这甚至是一种可疑的态度)。 感谢分享。 boost 包装 ifstreamofstream 并处理正确的路径,因此您在两个平台上都有 unicode 老实说,我认为这个答案提出的研究可能是问题的一部分。因为它不是,所以我在这里将其作为一项工作来展示,以避免其他人不得不尝试同样的事情。这些部分并没有很好的记录(这就是问题如此相关的原因)。【参考方案4】:
#include <iostream>
#include <fstream>
#include <string>
#include <system_error>

using namespace std;

int
main()

    ifstream f("testfile.txt");
    if (!f.good()) 
        error_code e(errno, system_category());
        cerr << e.message();
        //...
    
    // ...

ISO C++ 标准:

标题的内容 “赛尔诺” 与 POSIX 标头相同 “错误号.h” , 除了那个 错误号 将 被定义为宏。 [ 笔记: 目的是与 POSIX 标准保持紧密一致。 - 结尾 笔记 ] 独立 错误号 应为每个线程提供值。

【讨论】:

检查 errno 不像我说的那样可移植。 如果您认为fopen 设置了errnoifstream::open 不设置,请使用fopen。但请注意,ISO C 在fopen 部分中没有提及errno(将其与fgetposfsetposftell、...进行比较)。另请注意,ISO C 准确地知道 errno 的 3 个错误代码,而 ISO C++ 提到所有 POSIX 错误代码并将它们映射到 errc 我很确定 iostreams 不保证 errno 将在失败时保留。确实,可以看出它很可能会被覆盖。 这段代码也使用 system_category 而不是 generic_category 这是错误的。【参考方案5】:

检查此代码:

uSTL 是 C++ 标准库的部分实现,专注于 减少用户可执行文件的内存占用。

https://github.com/msharov/ustl/blob/master/fstream.cc

【讨论】:

我们为什么要在这个问题的上下文中检查该代码? 那么你应该在论证中说明原因/方式。这是一个仅链接的答案,显示很少或根本没有努力。 (我查看了代码,它当然无助于正确和/或可移植地处理错误情况)。项目负责人A word:“Windows 不受支持,也不会受到支持。我们都应该尽自己的一份力量阻止 Windows 的存在。” windows支持问题无关紧要,我只点fstream实现。 哇。所以,你没有读过这个问题。事实上,甚至不是第一个词。标题。

以上是关于在 C++ 中读取文件并处理可能的错误的便携式方法的主要内容,如果未能解决你的问题,请参考以下文章

PySpark 读取不存在文件时的错误处理

Swift-错误处理(Error Handling)(十六)

eXeScope

在 C++ 中为结构读取二进制文件的运行时错误

在运行时检测 C++ 中的堆碎片的便携式方法?

将十六进制转换为从文件 C++ 读取的 ASCII 的正确方法