C++23 `print` 是否检查写入是否成功进入流?

dig*_*evo -5 c++ io error-handling fmt c++23

我想知道标准委员会是否已经修复了臭名昭著的Hello, world! 漏洞。我主要谈论新<print>库(尚未在任何编译器中使用)。

{fmt}库(它启发了标准库)尚未修复此问题。显然,它在输出时不会抛出任何异常/dev/full(从 v9.1.0 开始)。因此,使用 CI/O 函数(例如std::fflush错误处理)仍然是一件事。

下面的程序注意到错误并返回失败代码(因此没有错误):

#include <exception>
#include <cstdio>
#include <cstdlib>
#include <fmt/core.h>


int main()
{
    fmt::println( stdout, "Hello, world!" );
    if ( std::fflush( stdout ) != 0 || std::ferror( stdout ) != 0 ) [[unlikely]]
    {
        return EXIT_FAILURE;
    }
}
Run Code Online (Sandbox Code Playgroud)

但这在 C++23 中可能吗?

#include <print>
#include <exception>
#include <cstdio>
#include <cstdlib>


int main()
{
    try
    {
        std::println( stdout, "Hello, world!" );
    }
    catch ( const std::exception& ex )
    {
        return EXIT_FAILURE;
    }
}
Run Code Online (Sandbox Code Playgroud)

对于那些不知道“Hello World”错误的人,下面的程序(Rust 中)会出现恐慌并输出一条有用的错误消息:

#include <exception>
#include <cstdio>
#include <cstdlib>
#include <fmt/core.h>


int main()
{
    fmt::println( stdout, "Hello, world!" );
    if ( std::fflush( stdout ) != 0 || std::ferror( stdout ) != 0 ) [[unlikely]]
    {
        return EXIT_FAILURE;
    }
}
Run Code Online (Sandbox Code Playgroud)
#include <print>
#include <exception>
#include <cstdio>
#include <cstdlib>


int main()
{
    try
    {
        std::println( stdout, "Hello, world!" );
    }
    catch ( const std::exception& ex )
    {
        return EXIT_FAILURE;
    }
}
Run Code Online (Sandbox Code Playgroud)

相反,C++ 标准iostreams以及其他一些语言(C、Ruby、Java、Node.js、Haskell 等)默认情况下不会报告任何失败,即使在程序关闭文件流时也是如此。另一方面,其他一些(Python3、Bash、Rust、C# 等)确实报告了错误。

Ran*_*its 6

该函数的文档表明,如果写入流失败(以及其他失败的其他异常),std::println它将抛出。std::system_error当然,std::println成功写入流,失败通常发生在稍后将流实际写入文件系统时。

在 C++ 环境中,如果您需要保证数据确实到达磁盘,您有时需要使用类似的方法std::flush来检查是否没有发生错误。您可以争论这是否方便,但这遵循这样的逻辑:如果您不需要该功能,则不应该有任何开销。这是一个功能,而不是一个错误。

如果您需要这种保证,请编写一个小包装器,该包装器使用RAII技术在出现错误时抛出异常。是关于析构函数中的释放与提交语义以及何时引入析构函数是一个好主意的很好的讨论。

示例代码

#include <iostream>

struct SafeFile {
    SafeFile(const std::string& filename)
        : fp_(fopen(filename.c_str(), "w"))
        , nuncaught_(std::uncaught_exceptions()) {
        if (fp_ == nullptr)
            throw std::runtime_error("Failed to open file");
    }

    ~SafeFile() noexcept(false) {
        fflush(fp_);
        if (ferror(fp_) and nuncaught_ == std::uncaught_exceptions()) {
            fclose(fp_);
            throw std::runtime_error("Failed to flush data");
        }
        fclose(fp_);
    }

    auto operator*() {
        return fp_;
    }

    FILE *fp_{nullptr};
    int nuncaught_{};
};

int main()
{
    try {
        SafeFile fp("/dev/urandom");
        fprintf(*fp, "Hello, world!");
    }
    catch ( const std::exception& ex )
    {
        std::cout << "Caught the exception" << std::endl;
        return EXIT_FAILURE;
    }
}
Run Code Online (Sandbox Code Playgroud)

输出

Caught the exception
Run Code Online (Sandbox Code Playgroud)

  • @digito_evo 那里没有问题。如果要验证flush是否成功,就必须验证flush是否成功。 (3认同)
  • @digito_evo:是的。这就是所谓的“不要为不使用的东西付费”。您可以对一个流执行多个输出,然后在最后刷新它们。这样,您只需支付一次冲水费用。如果它总是冲洗,你就不能这样做。 (3认同)
  • @digito_evo:“*std 库需要对文件流负责,如果无法在程序关闭时刷新它们,则返回失败代码。*”这样做的成本不为零。它将要求每个“print”执行只有*某些*“print”语句需要执行的操作。C++ 是一种通常期望“你”对非零成本的事情负责的语言。就是这样。这不是一个错误;而是一个错误。这是一个功能。 (3认同)
  • @digito_evo:“*它付出了代价。*”但**不是在每个打印语句中**。*这才是重点。“解决”这个问题的唯一方法是让它在每次打印语句时支付该费用。这违反了“不为不使用的东西付费”的规则。API 不*知道*任何特定的“print”语句是否需要执行刷新。因此,做这样的flush是一个你不知道用户是否想要或者需要的成本。所以你让他们在他们想做的时候做。 (3认同)
  • 您可能不同意结果,但正如其他人所说,没有问题需要解决。这只是语言/库工作方式的一个特性。 (2认同)