Search code examples
c++error-handlingiofmtc++23

Does C++23 `print` check to see if the write successfully made it into the stream?


I want to know whether or not the standard committee has fixed the infamous Hello, world! bug. I'm primarily talking about the new <print> library (not yet available in any of the compilers).

The {fmt} library (which has inspired the standard library) has not fixed this. Apparently, it does not throw any exceptions when outputting to /dev/full (as of v9.1.0). So the use of C I/O functions like std::fflush for error handling is still a thing.

The below program notices the error and returns a failure code (thus not buggy):

#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;
    }
}

But is this possible in 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;
    }
}

For those unaware of the "Hello World" bug, the below program (in Rust) panics and outputs a useful error message:

fn main()
{
    println!( "Hello, world!" );
}
./main > /dev/full 
thread 'main' panicked at 'failed printing to stdout: No space left on device (os error 28)', library/std/src/io/stdio.rs:1008:9

Conversely, C++ standard iostreams along with some other languages (C, Ruby, Java, Node.js, Haskell, etc) don't report any failure by default even at program shutdown when the program closes the file streams. On the other hand, some others (Python3, Bash, Rust, C#, etc) do report the error.


Solution

  • The documentation for the std::println function indicates that it will throw the std::system_error if it fails writing to the stream (and other exceptions for other failures). Of course, std::println successfully writes to the stream, and the failure typically occurs later when the stream is actually written to the filesystem.

    In a C++ environment, if you need to guarantee that data actually hits the disk, you will at some point need to use something like std::flush and check that no error has occurred. You can argue whether this is convenient or not, but this follows from the logic that there should not be any overhead if you don't need the feature. This is a feature, not a bug.

    If you need this guarantee, write a small wrapper that uses the RAII technique to throw an exception if there is an error. Here is a good discussion about release versus commit semantics in destructors and when it could be a good idea to throw in a destructor.

    Sample Code

    #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;
        }
    }
    

    Output

    Caught the exception