So:

  int main() {
      printf("hello, world!\n");
      return 0;
    }
C knew it all along :)

... Except the fact that printf/scanf use variadics, and the only reason why it stopped being a constant source of crash is the fact that compilers started recognizing it and validating format strings/complaining when you pass a non-literal string as a format.

is instead 100% typesafe. If you pass the wrong stuff it won't compile, and as {fmt} shows you can even validate formats at compile time using just `constexpr` and no compiler support.

As always, people making more fuss around it than necessary. Code calling printf() with a constant format string literal is this class of code that you have to run a single time to know it works. Many C++ programmers have always been using printf() in preference to e.g. std::cout because of ergonomics. And they were right.

It's hard to take people seriously that try to talk it down for being a pragmatic solution that's been around for probably 30-40 years.

> is this class of code that you have to run a single time to know it works

Not sure what you're trying to say here.

Take as an example printf("%d\n", foo->x);. Assuming it compiled but assuming no further context, what could break here at run-time? foo could be NULL. And the type of foo->x could be not an integer.

Let's assume you run the code once and observe that it works. What can you conclude? 1) foo was not NULL at least one time. Unfortunately, we don't know about all the other times. 2) foo->x is indeed an integer and the printf() format is always going to be fine -- it matches the arguments correctly. It's a bit like a delayed type check.

A lot of code is like that. Furthermore, a lot of that code -- if the structure is good -- will already have been tested after the program has been started up. Or it can be easily tested during development by running it just once.

I'm not saying it's impossible to write a bad printf line and never test it, only to have it fail years later in production. It's absolutely possible and it has happened to me. Lessons learned.

I'll even go as far as saying that it's easy to have errors slip on refactors if there aren't good tests in place. But people are writing untyped Python or Javascript programs, sometimes significant ones. Writing in those is like every line was a printf()!

But many people will through great troubles to achieve an abstract goal of type safety, accepting pessimisations on other axes even when it is ultimately a bad tradeoff. People also like to bring up issues like this on HN like it's the end of the world, when it's not nearly as big of an issue most of the time.

Another similar example like that are void pointers as callback context. It is possible to get it wrong, it absolutely happens. But from a pragmatic and ergonomic standpoint I still prefer them to e.g. abstract classes in a lot of cases due to being a good tradeoff when taking all axes into account.

> I'm not saying it's impossible to write a bad printf line and never test it, only to have it fail years later in production. It's absolutely possible and it has happened to me. Lessons learned.

A modern compile time type checked formatter would have prevented this mistake, you are deliberately choosing to use poor tools and calling this "pragmatism" because it sounds better than admitting you're bad at this and you don't even want to improve.

In fact C++ even shipped a pair of functions here. There's a compile time type checked formatter std::format, which almost everybody should use almost always (and which is what std::println calls), and there's also a runtime type checked formatter std::vformat, for those few cases where you absolutely can't know the format string until the last moment. That is a hell of a thing, if you need one of those I have to admit nobody else has one today with equal ergonomics.

Thanks for the ad hominem, but let's put that into perspective.

My current project is a GUI prototype based on plain Win32/Direct3D/Direct2D/DirectWrite. It currently clocks in at just under 6 KLOC. These are all the format calls in there (used git grep):

        fatal_f("Failed to CreateBuffer(): %lx", err);
        fatal_f("Failed to Map() buffer");
        fatal_f("Failed to compile shader!");
        fatal_f("Failed to CreateBuffer(): %lx", err);
        fatal_f("Failed to create blend state");
        fatal_f("OOM");
        fatal_f("Failed to register Window class");
        fatal_f("Failed to CreateWindow()");
        fatal_f("%s failed: error code %lx", what, hr);
        msg_f("Shader compile error messages: %s", errors->GetBufferPointer());
        msg_f("Failed to compile shader but there are no error messages. "
        msg_f("HELLO: %d times clicked", count);
        msg_f("Click %s", item->name.buf);
        msg_f("Init text controller %p", this);
        msg_f("DELETE");
        msg_f("Refcount is now %d", m_refcount);
        msg_f("Refcount is now %d", m_refcount);
        vfprintf(stderr, fmt, ap);
        fprintf(stderr, "\n");
        fprintf(stderr, "FATAL ERROR: ");
        vfprintf(stderr, fmt, ap);
        fprintf(stderr, "\n");
        snprintf(utext, sizeof utext, "Hello %d", ui->update_count);
        snprintf(filepath, sizeof filepath, "%s%s",
        int r = vsnprintf(m_buffer, sizeof m_buffer, fmt, ap);
        int r = vsnprintf(text_store, sizeof text_store, fmt, ap);
        snprintf(svg_filepath, sizeof svg_filepath, "%s", filepath);
That's theory and practice for you. The real world is a bit more nuanced.

Meanwhile I have 100 other, more significant problems to worry about than printf type safety. For example, how to get rid of the RAII based refcounting that I introduced but it wasn't exactly an improvement to my architecture.

But thanks for the suggestion to use std::format in that set of cases and std::vformat in these other situations. I'll put those on my stack of C++ features to work through when I have time for things like that. (Let's hope that when I get there, those aren't already superseded by something safer).

(Update: uh, std::format returns std::string. Won't use.)

> std::format returns std::string. Won't use

just use `std::format_to_t` then and format to whatever your heart desires, without ever allocating once:

    std::array buf{};

    std::format_to_n(data(buf), size(buf), "hello {}", 44);
I've used `fmt` on *embedded* devices and it was never a performance issue, not even once (it's even arguably _faster_ than printf).

(OT: technically speaking, in C++ you shouldn't call `vfprintf` or other C library functions without prefixing them with `std::`, but that's a crusade I'm bound to lose - albeit `import std` will help a lot)

I noticed std::format and std::print aren't even available with my pretty up-to-date compilers (testing Debian bookworm gcc/clang right now). There is only https://github.com/fmtlib/fmt but it doesn't seem prepackaged for me. Have you actually used std::format_to_n? Did you go through the trouble of downloading it or are you using C++ package managers?

I'm often getting the impression that these "you're a fool using these well-known but insecure libraries. Better use this new shiny thing because it's safe" discussions are a bit removed from reality.

But I'm asking in earnest. Please also check out my benchmark in the sibling thread where I compared stringstream with stdio/snprintf build performance. Would in fact love to compare std::format_to_n, but can't be arsed to put in more time to get it running right now.