Search code examples
c++fmt

Why does std::format() throw at runtime for incorrect format specifiers?


Recently I discovered that the following code compiles on a few major compilers, and then throws at runtime:

std::cout << std::format("{:*<{}}", 10, "Hello") << std::endl;
terminate called after throwing an instance of 'std::format_error'
  what():  format error: argument used for width or precision must be a non-negative integer

It throws because "10" should come after "Hello", not before.

But the obvious question is: Why isn't it failing at compile-time? My understanding was that these arguments would be type-checked at compile-time, and obviously a const char* can't be used as a width specifier. Why is this not a compile error?

If you don't understand why this is confusing, please know that the first argument of std::format() is of type std::format_string<...>. This type takes a string literal/string view at compile time (due to its consteval constructor), and at compile-time it reads the contents of the given string to see if the format arguments match the format string. Therefore, a call of std::format("{}"); is guaranteed not to compile, since the string "{}" is read at compile-time as a format specifier, but the type lists show that no arguments were passed, so what would be put in that space?


Solution

  • TL;DR: Placeholders inside format specifiers are a known shortcoming of compile time validation in C++20. It's being fixed with C++26.

    Original answer:

    Disclaimer: this is an idea I came up with after looking into it just now, I'm in no way sure about this.

    I think there is an issue here with how the compile time validation works for your case.

    Normally, compile time checking of, say, std::format("{:d}", "hi!") works, because it calls std::formatter<const char*>::parse("{:d}"), which is constexpr, and will throw because it sees that ":d" doesn't fit the type of the formatter.

    You pass this format string: "{:*<{}}" with arg types int then const char*. So we get the following parse calls:

    • std::formatter<int>::parse("{:*<{}}") - which is the place I'm a bit fuzzy on. From the interface of parse_context I'm going to guess they simply store the ID of the next argument, so they can use it to fetch the desired width when it's time to actually format things. But I don't see the parse_context providing a way to actually check what the type of the next argument is. So the parsing just succeeds, because for all it knows the next argument could be an int.

    • std::formatter<const char*>::parse("{}") - parsing the format string of the "inner" placeholder. Nothing wrong here. (Edit 3: I'm not sure this parse call actually happens. It's probably the responsibility of the outermost parse call to process its whole format specifier including nested placeholders.)

    And with that, the parsing has succeeded and the error will be found only at runtime, when actually reading the argument for the width from the stored arg ID.

    Edit: It looks like they actually addressed this exact issue in C++26 by adding check_dynamic_spec to the parse_context. With that, it should be possible to check placeholders within format specs as well, I'd imagine.

    Edit 2: Here is the paper that introduced these new methods, and provides a very similar motivating example to yours.