-5

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.

user229044
  • 232,980
  • 40
  • 330
  • 338
digito_evo
  • 3,216
  • 2
  • 14
  • 42
  • **Comments have been [moved to chat](https://chat.stackoverflow.com/rooms/253710/discussion-on-question-by-digito-evo-does-c23-print-check-to-see-if-the-writ); please do not continue the discussion here.** Before posting a comment below this one, please review the [purposes of comments](/help/privileges/comment). Comments that do not request clarification or suggest improvements usually belong as an [answer](/help/how-to-answer), on [meta], or in [chat]. Comments continuing discussion may be removed. – Samuel Liew May 18 '23 at 01:02

1 Answers1

6

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
RandomBits
  • 4,194
  • 1
  • 17
  • 30
  • `fclose(fp_)` is not called if `ferror(fp_)`. Not sure, fushing maybe is not needed on fclose and the last one returns an error. – 273K May 17 '23 at 01:22
  • @273K Yes, I should probably call `fclose` first, but I am unsure of the details here. – RandomBits May 17 '23 at 02:20
  • OK. But does this mean the C++23 standard has not fixed the issue? Maybe we have to wait and see. – digito_evo May 17 '23 at 09:41
  • 2
    You may not agree with the results, but as others have said, there is no issue to fix. This is just a feature of how the language / library works. – RandomBits May 17 '23 at 11:08
  • 3
    @digito_evo there is no issue there. If you want to verify the flush is successful, you have to verify the flush is successful. – spectras May 17 '23 at 12:15
  • @RandomBits So not flushing to the out stream (before/after calling `std::exit`) and returning a success code is a feature? – digito_evo May 17 '23 at 13:07
  • 3
    @digito_evo: Yes. It's called "don't pay for what you don't use." You can perform multiple outputs to a stream, then flush them at the end. This way, you only pay for the flushing cost once. You can't do that if it always flushes. – Nicol Bolas May 17 '23 at 13:38
  • Is this really a good idea to `throw` in a destructor? I mean, [this may be source of various kinds of more-or-less-known problems](https://stackoverflow.com/questions/130117/if-you-shouldnt-throw-exceptions-in-a-destructor-how-do-you-handle-errors-in-i)... – Erel May 17 '23 at 14:42
  • @Erel Yes, throwing in a destructor has serious caveats and as a general principal should be avoided. Using a destructor to implement `commit` semantics, however, is an exception to the rule and can be done safely if you are careful to ensure there is not another exception in flight as done in the above code. See this [answer](https://stackoverflow.com/a/4098662/1202808) to the question you linked. – RandomBits May 17 '23 at 16:25
  • 1
    @NicolBolas "*It's called "don't pay for what you don't use."*" I am literally paying the cost of automatic flushing at program shutdown. For what reason I should not get the failure code if it doesn't go well?! – digito_evo May 17 '23 at 17:06
  • @NicolBolas I think you miss to recognize the problem here. The std library needs to take responsibility for the file streams and return a failure code if it can't flush them at program shutdown. Similar to what other languages do. Take `spdlog` as an example. It automatically calls `spdlog::shutdown()` after the execution of `std::exit` and its exit handlers which flushes and destructs the loggers and file handles safely. In other words, it is kinda based on RAII. – digito_evo May 17 '23 at 17:07
  • 3
    @digito_evo: "*The std library needs to take responsibility for the file streams and return a failure code if it can't flush them at program shutdown.*" The cost of doing that is non-zero. It would require every `print` to do something that only *some* `print` statements need to do. C++ is a language that generally expects *you* to take responsibility for things that have non-zero costs. That's just how it is. It is not a bug; it's a feature. – Nicol Bolas May 17 '23 at 17:12
  • @NicolBolas Again. I understood that yesterday with the help of some of the comments. So that's fine. What I'm saying now is that the library/runtime does a flush at the end. It pays the cost. But the program does not report the failure as shown in the question. That's an apparent bug. Anyway. I guess it cannot be fixed without introducing API-breaking changes to the language which probably means it's not going to be fixed during the next decade. – digito_evo May 17 '23 at 17:18
  • 3
    @digito_evo: "*It pays the cost.*" But **not at every print statement**. *That*'s the point. The only way to "fix" that would be to make it pay that cost at every print statement. Which violates the "don't pay for what you don't use" rule. The API does not *know* if any particular `print` statement needs to do a flush. Therefore, doing such a flush is a cost that you don't know if the user wants or needs. So you make them do it when they want to. – Nicol Bolas May 17 '23 at 17:27
  • @NicolBolas "*But not at every print statement*" Ok? I'm talking about when a program terminates. There is always some flushing done at that stage (which means **cost**). Why should it not report anything? – digito_evo May 17 '23 at 17:35
  • 1
    @digito_evo: "*Why should it not report anything?*" Because it would override the return value of `main`. If my `main` function says that it terminated with a bad error code, that code should not be overwritten by some other problem. Even if my `main` function says that it terminated OK, then that's what ought to be reported to the outside world. It's not for the library to decide to go behind my back and decide whether my program executed correctly. – Nicol Bolas May 17 '23 at 17:40
  • @NicolBolas I see. With that said, still returning a success code seems to me like the program is lying. I could personally choose to have an exception thrown by the library at that stage to make the program call the `std::termainate` so that it does not hide the problem. This at least seems like a more sane way of dealing with such an error. – digito_evo May 17 '23 at 17:48
  • @digito_evo I you found this answer and discussion useful, can you accept it? – RandomBits May 18 '23 at 01:37
  • @RandomBits Yes, however, I need to ask a few questions first. – digito_evo May 18 '23 at 13:23
  • @RandomBits So as I understand now, the `std::print` function throws a `std::system_error` when it fails to write to a stream, and it has nothing to do with flushing that stream to the output device (i.e. console, disk, etc). I still need to manually do a `std::fflush` and then check `std::ferror()` to see whether the flushing was successful or not. Is this all true? – digito_evo May 18 '23 at 13:27
  • @digito_evo Yes, exactly. And, all that boiler plate code can be abstracted into a class that handles it automagically. – RandomBits May 18 '23 at 13:35
  • The answer is still not edited, `SafeFile` opens and owns a C file stream and doesn't close it if `fflush()` fails causing a resource leak. – 273K May 18 '23 at 13:58
  • @273K Updated the code based on your feedback. – RandomBits May 18 '23 at 14:57
  • @RandomBits Your code uses a function `std::uncaught_exception()` that was removed from C++20. Please use `std::uncaught_exceptions()` instead. GCC warns about it too. – digito_evo May 18 '23 at 17:17
  • 1
    @digito_evo Thanks for catching that. I updated the code. – RandomBits May 18 '23 at 19:57