5 ms·
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
by jstimpfle 3y ago
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.
- repsilat 3y agoSometimes the print statement is in untested sanity-checking error-case branches that don't have test coverage ("json parsing failed" or whatever). It's pretty annoying when those things throw, and not too uncommon. Another case in C++ is if the value is templated. You don't always get test coverage for all the types, and a compile error is nice if the thing can't be handled. "Type coverage" is pretty useful. Not a huge deal here I agree, but nice nonetheless.
- catskul2 3y ago> 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.
- deleted 3y ago[deleted]
- jstimpfle 3y agoTake 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.
- tialaramex 3y ago> 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.
- jstimpfle 3y agoThanks 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.)
- mort96 3y agoI've definitely written bugs where the format specifier is wrong. Maybe you used %lu because a type happened to be unsigned long on your system, then you compile it for another system and it's subtly wrong because the type you're printing is typedef'd to unsigned long long there. Printing an uint32_t? I hope you're using the PRIu32 macro, otherwise that's also a bug.
- jstimpfle 3y agoYes, this is annoying. Integer promotions can be annoying in general. I'm often working with fixed size types, and still find myself using %d and %ld instead of PRIu32 etc most of the time, because it's simply easier to type. If unsure about portability issues it can help to cast the argument instead. But realistically it isn't much of an issue, printfs seem to be about 0.5% of my code, and >> 90% of them are purely diagnostic. I don't even recall the last time I had an issue there. I remember the compiler warning me a few times about mismatching integer sizes though.
- throwaway2037 3y agoI agree. Layers of legacy typedefs in the Win32 API always catch me off guard. Any large source base with lots of typedefs, it can be tricky to printf() without issue.
- qalmakka 3y agoThis. I have corrected uncountable lines of code where people just used "%d" for everything, potentially opening a massive can of worms. `inttypes.h` is what you should be using 99% of the time, but that makes for very ugly format strings, so basically nobody uses that. Otherwise you should cast all of your integer params to (long long int) and use %lld, which sucks.
- tialaramex 3y ago> 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 void msg(char *msg, char *x, int k) { printf("%hhn%s%d\n", x, msg, k); } So, how about it? I mean, I have code where that works exactly as expected, so I can "know it works" according to you, but I also have code where that blows up immediately, because it's complete nonsense. Which according to you shouldn't happen, but there it is.
- jstimpfle 3y agoI mean yes, I should have been more restrictive in my statement, but I'm sure you notice how we're veering more into the hypothetical / into programming language geek land. I had to look up %hhn because I've never used it. (Have used %n years ago but noticed it's a fancy and unergonomic way to code anyway. In the few locations where printed characters have to be counted, just consider the return value of the format call!) And btw. how is this a problem related to missing type checks with varargs? The only problem I see is that we don't know that those pointers are not null / the char-pointer doesn't point to a zero-terminated string. In other words, just the basic C level of type (un)safety.
- xeyownt 3y agoTotally agree, given example is total nonsense. Most issues with printf could be reported by static analysis, and modern compiler reports them as warnings, which in my book must be immediately converted to errors. All other weird usages should be either banned, or reviewed carefully, but they are very rare. Also, std::iostream is ridden with bugs as well. Try to print out hex / dec in a consistent way is plain "impossible". Everytime you print an int, you should in fact systematically specify the padding, alignment and mode (hex/dec) otherwise you can't know for sure what you are outputting.
- qalmakka 3y agoiostream _sucks_, I had to implement an iostream for zlib + DEFLATE in order to play ball with an iostream-based library, and I had to sweat blood and tears in order to make it work right when a simple loop and a bit of templated code would have worked wonders compared to that sheer insanity of `gptr`, `pubsync`, ... The moment you notice that they've methods called "pubXXX" that call a protected "XXX" on the `basic_streambuf` class is the moment your soul leaves your body. IOStreams is superbad, and thankfully <format> removes half of its usages which were based on spamming `stringstream`s everywhere (stringstream is also very, very bad). They also inspired Java's InputStream nonsense which ruined the existence of an innumerable number of developers in the last 30 years.
- gpderetta 3y agoEvery time I have to sprintf a string_view I die a little bit inside.
- gmadsen 3y agofor basic things, sure. it is much much worse than this when you deal with different encodings for an application that needs to format and print things
- deleted 3y ago[deleted]
- chlorion 3y agoprintf and similar stdio string formatting functions have caused a non-trivial amount of security exploits. If the compiler can statically check that your format string is valid and type checks, you can prevent this entire class of exploits. https://en.wikipedia.org/wiki/Printf#Vulnerabilities https://en.wikipedia.org/wiki/Printf#Vulnerabilities
- usefulcat 3y ago> you have to run a single time to know it works A common use case for cstdio/iostreams/std::format is logging. It's not at all uncommon to have many, many log statements that are rarely executed because they're mainly for debugging and therefore disabled most of the time. There you go, lots of rarely used and even more rarely 'tested' formatting constructs. I don't want things to start blowing up just because I enabled some logging, so I'm going to do what I can to let the compiler find as many problems as possible at compile time.
- pjmlp 3y agoThere are several CVEs that thank their success story to printf's format string.