Moving printf into the modern age using C++17

Ever since c++11 introduce variadic templates, I started seeing people implement some "safe printf" examples. They are pretty cool, but none of them attempted to actually implement the printf fully with all of its quirks. Instead, they all pretty much do the same thing:

  1. Use variadic templates to verify the sanity of the parameters
  2. Delegate the actual formatting to the libc printf

I think we can do better...


Originally, I had implemented this with C++11's variadic templates. It was doable, but honestly, the code was not pretty at all. I had to use a bunch of SFINAE, and template specializations. None the less, I was pretty pleased with the results.

Now that C++17 has been standardized, I figured I would refactor that code to see how much better I can make it. The new features did not disappoint :-).

If we skip over convenience wrappers, the entry point of the code is the Printf function. Which looks like this:

template <class Context, class... Ts>
int Printf(Context &ctx, const char *format, const Ts &... ts) {

    assert(format);

    if constexpr (sizeof...(ts) > 0) {
        while (*format != '\0') {
            if (*format == '%') {
                // %[flag][width][.precision][length]char

                // this recurses into get_width -> get_precision -> get_length -> process_format
                return detail::get_flags(ctx, format, ts...);
            } else {
                ctx.write(*format);
            }

            ++format;
        }

        // clean up any trailing stuff
        return Printf(ctx, format + 1, ts...);
    } else {
        for (; *format; ++format) {
            if (*format != '%' || *++format == '%') {
                ctx.write(*format);
                continue;
            }

            detail::ThrowError("Bad Format");
        }

        // this will usually null terminate the string
        ctx.done();

        // return the amount of bytes that should have been written if there was sufficient space
        return ctx.written;
    }
}

Starting with C++17, we have if constexpr, so we don't need to use specializations to special case not having any arguments. Sweet!

The idea behind the code is pretty simple (would be simpler if we had for constexpr, but that's a different discussion).

We decide If we have any arguments, if we do, then we loop through the format string searching for a '%' character. For each character we see, if it isn't a '%' we output it, so far, pretty simple. However, when we do see it, that's when the interesting stuff happens. Because there is no compile time looping over the string, things are implemented recusively, which I'll go into more detail later.

So, we've seen a '%', we may or may not have some flags, so we call detail::get_flags.

template <class Context, class... Ts>
int get_flags(Context &ctx, const char *format, const Ts &... ts) {

    Flags f = {0, 0, 0, 0, 0, 0};
    bool done = false;

    // skip past the % char
    ++format;

    while (!done) {

        char ch = *format++;

        switch (ch) {
        case '-':
            // justify, overrides padding
            f.justify = 1;
            f.padding = 0;
            break;
        case '+':
            // sign, overrides space
            f.sign = 1;
            f.space = 0;
            break;
        case ' ':
            if (!f.sign) {
                f.space = 1;
            }
            break;
        case '#':
            f.prefix = 1;
            break;
        case '0':
            if (!f.justify) {
                f.padding = 1;
            }
            break;
        default:
            done = true;
            --format;
        }
    }

    return get_width(ctx, format, f, ts...);
}

This function looks pretty much like how you would implement it in plain C, consuming the flag characters in a loop, and updating a flags variable as it goes. Really the only standout is that we pass along the calculated flags along with the template arguments when we are done. After flags come the width!

template <class Context, class T, class... Ts>
int get_width(Context &ctx, const char *format, Flags flags, const T &arg, const Ts &... ts) {

    int width = 0;

    if (*format == '*') {
        ++format;
        // pull an int off the stack for processing
        width = formatted_integer<long int>(arg);
        if constexpr (sizeof...(ts) > 0) {
            return get_precision(ctx, format, flags, width, ts...);
        }
        ThrowError("Internal Error");
    } else {
        char *endptr;
        width = strtol(format, &endptr, 10);
        format = endptr;

        return get_precision(ctx, format, flags, width, arg, ts...);
    }
}

Width is a lttle more interesting because it may or may not consume a parameter. if constexpr to the rescue again! Originally, I had to use some specializations for this as well, but now in C++17, it is much simpler. If we need to pluck a parameter off the pack, we use formatted_integer which looks like this:

template <class R, class T>
constexpr R formatted_integer([[maybe_unused]] T n) {
    if constexpr(std::is_integral<T>::value) {
        return static_cast<R>(n);
    }
    ThrowError("Non-Integer Argument For Integer Format");
}

All it is basically doing is checking that the argument is in fact an integer and static_cast's it to the type we wanted; nothing too special.

Once we've consumed the width, we do the same with precision, which looks very similar.

template <class Context, class T, class... Ts>
int get_precision(Context &ctx, const char *format, Flags flags, long int width, const T &arg, const Ts &... ts) {

    // default to non-existant
    long int p = -1;

    if (*format == '.') {

        ++format;
        if (*format == '*') {
            ++format;
            // pull an int off the stack for processing
            p = formatted_integer<long int>(arg);
            if constexpr (sizeof...(ts) > 0) {
                return get_modifier(ctx, format, flags, width, p, ts...);
            }
            ThrowError("Internal Error");
        } else {
            char *endptr;
            p = strtol(format, &endptr, 10);
            format = endptr;
            return get_modifier(ctx, format, flags, width, p, arg, ts...);
        }
    }

    return get_modifier(ctx, format, flags, width, p, arg, ts...);
}

We're almost there! After the precision comes any "length modifiers" such as "l" or "ll".

template <class Context, class T, class... Ts>
int get_modifier(Context &ctx, const char *format, Flags flags, long int width, long int precision, const T &arg, const Ts &... ts) {

    Modifiers modifier = Modifiers::MOD_NONE;

    switch (*format) {
    case 'h':
        modifier = Modifiers::MOD_SHORT;
        ++format;
        if (*format == 'h') {
            modifier = Modifiers::MOD_CHAR;
            ++format;
        }
        break;
    case 'l':
        modifier = Modifiers::MOD_LONG;
        ++format;
        if (*format == 'l') {
            modifier = Modifiers::MOD_LONG_LONG;
            ++format;
        }
        break;
    case 'L':
        modifier = Modifiers::MOD_LONG_DOUBLE;
        ++format;
        break;
    case 'j':
        modifier = Modifiers::MOD_INTMAX_T;
        ++format;
        break;
    case 'z':
        modifier = Modifiers::MOD_SIZE_T;
        ++format;
        break;
    case 't':
        modifier = Modifiers::MOD_PTRDIFF_T;
        ++format;
        break;
    default:
        break;
    }

    return process_format(ctx, format, flags, width, precision, modifier, arg, ts...);
}

And Finally, we actually process the thing we want to format! I'm going to highly abbreviate this function because it is quite long. You are best served just looking at the source for this one:

template <class Context, class T, class... Ts>
int process_format(Context &ctx, const char *format, Flags flags, long int width, long int precision, Modifiers modifier, const T &arg, const Ts &... ts) {

    // ... snip ...
    const char *s_ptr;

    char ch = *format;
    switch (ch) {
        // ... snip ...

    case 's':
        s_ptr = formatted_string(arg);
        if (!s_ptr) {
            s_ptr = "(null)";
        }
        output_string('s', s_ptr, precision, width, flags, strlen(s_ptr), ctx);
        return Printf(ctx, format + 1, ts...);

        // ... snip ...
    }

    return Printf(ctx, format + 1, ts...);
}

Now that we've collected the flags, width, precision, and modifier, we can finally process the argument itself. We have a big switch statement to determine what the argument should be, and we use functions like formatter_string to enforce correctness. We write the string to the destination using output_string. And finally, we recursively call the `Printf`` function passing the portion of the format that is left along with any remaining arguments. Then the whole thing starts again.

So to illustrate things a bit, if we had a call like this:

cxx17::printf("Hello, %s. Let's print a number: %d\n", "World", 10);

The call chain looks like this:

Printf(ctx, "Hello, %s. Let's print a number: %d\n", "World", 10);

[*] output: "Hello, "

↳ detail::get_flags("s. Let's print a number: %d\n", "World", 10);
   ↳ detail::get_width("s. Let's print a number: %d\n", flags, "World", 10);
      ↳ detail::get_precision("s. Let's print a number: %d\n", flags, width, "World", 10);
         ↳ detail::get_modifier("s. Let's print a number: %d\n", flags, width, precision, "World", 10);
            ↳ detail::process_format("s. Let's print a number: %d\n", flags, width, precision, modifier, "World", 10);

[*] output: "World"

               ↳ Printf(ctx, "d\n", 10);

[*] output: ". Let's print a number:  "

                  ↳ detail::get_flags("d\n", 10);
                     ↳ detail::get_width("d\n", flags, 10);
                        ↳ detail::get_precision("sd\n", flags, width, 10);
                           ↳ detail::get_modifier("d\n", flags, width, precision, 10);
                              ↳ detail::process_format("d\n", flags, width, precision, modifier, 10);

[*] output: "10"

                                 ↳ Printf(ctx, "\n");

[*] output: "\n"

And that's pretty much it. All in all, it's just a bit shy of 1,000 lines of code, but it really isn't too complicated once we break it down piece by piece. There is some complexity when it comes to number formatting, but that stuff really is standard algorithms.

Generally, it performs very well, We get exceptions if we pass the wrong types, but it still actually slightly outperforms glib's printf!

If you found this interesting, check out the code!