12
\$\begingroup\$

I had an uneventful afternoon, so I thought I might try and write a string formatter.

This is based on the documentation I found here.

#include <string>
#include <vector>
#include <map>
#include <ostream>
#include <iostream>
#include <exception>
#include <stdexcept>
#include <typeindex>
#include <cstdint>
#include <cstddef>
#include <cassert>
#include <iomanip>



template<typename... Args>
class Format
{
    class Formatter
    {
        std::size_t             used;
        // Flags
        enum class Length    {none, hh, h, l, ll, j, z, t, L};
        enum class Specifier {d, i, u, o, x, X, f, F, e, E, g, G, a, A, c, s, p, n};
        bool                    leftJustify;    // -    Left-justify within the given field width; Right justification is the default (see width sub-specifier).
        bool                    forceSign;      // +    Forces to preceed the result with a plus or minus sign (+ or -) even for positive numbers. By default, only negative numbers are preceded with a - sign.
        bool                    forceSignWidth; // (space)  If no sign is going to be written, a blank space is inserted before the value.
        bool                    prefixType;     // #    Used with o, x or X specifiers the value is preceeded with 0, 0x or 0X respectively for values different than zero.
                                                // Used with a, A, e, E, f, F, g or G it forces the written output to contain a decimal point even if no more digits follow. By default, if no digits follow, no decimal point is written.
        bool                    leftPad;        // 0    Left-pads the number with zeroes (0) instead of spaces when padding is specified (see width sub-specifier).
        int                     width;
        int                     precision;
        Length                  length;
        Specifier               specifier;
        std::type_info const*   expectedType;
        std::ios_base::fmtflags format;

        public:
            struct FormatterCheck
            {
                std::ostream&       stream;
                Formatter const&    formatter;
                FormatterCheck(std::ostream& s, Formatter const& formatter)
                    : stream(s)
                    , formatter(formatter)
                {}
                template<typename A>
                std::ostream& operator<<(A const& nextArg)
                {
                    formatter.apply(stream, nextArg);
                    return stream;
                }
            };
           Formatter(char const* formatStr)
                : used(0)
                , leftJustify(false)
                , forceSign(false)
                , forceSignWidth(false)
                , prefixType(false)
                , leftPad(false)
                , width(0)
                , precision(6)
                , length(Length::none)
                , format(0)
            {
                char const* fmt = formatStr;
                assert(*fmt == '%');

                bool flag = true;
                do {
                    ++fmt;
                    switch(*fmt) {
                        case '-':   leftJustify     = true;break;
                        case '+':   forceSign       = true;break;
                        case ' ':   forceSignWidth  = true;break;
                        case '#':   prefixType      = true;break;
                        case '0':   leftPad         = true;break;
                        default:    flag = false;
                    }
                } while (flag);
                if (std::isdigit(*fmt)) {
                    char* end;
                    width = std::strtol(fmt, &end, 10);
                    fmt = end;
                }
                if (*fmt == '.') {
                    ++fmt;
                    if (std::isdigit(*fmt)) {
                        char* end;
                        precision = std::strtol(fmt, &end, 10);
                        fmt = end;
                    }
                    else {
                        precision = 0;
                    }
                }
                char first = *fmt;
                ++fmt;
                switch(first) {
                    case 'h':   length = Length::h;
                                if (*fmt == 'h') {
                                    ++fmt;
                                    length  = Length::hh;
                                }
                                break;
                    case 'l':   length = Length::l;
                                if (*fmt == 'l') {
                                    ++fmt;
                                    length  = Length::ll;
                                }
                                break;
                    case 'j':   length = Length::j;break;
                    case 'z':   length = Length::z;break;
                    case 't':   length = Length::t;break;
                    case 'L':   length = Length::L;break;
                    default:
                        --fmt;
                }
                switch(*fmt) {
                    case 'd':   specifier = Specifier::d;break;
                    case 'i':   specifier = Specifier::i;break;
                    case 'u':   specifier = Specifier::u;break;
                    case 'o':   specifier = Specifier::o;break;
                    case 'x':   specifier = Specifier::x;break;
                    case 'X':   specifier = Specifier::X;break;
                    case 'f':   specifier = Specifier::f;break;
                    case 'F':   specifier = Specifier::F;break;
                    case 'e':   specifier = Specifier::e;break;
                    case 'E':   specifier = Specifier::E;break;
                    case 'g':   specifier = Specifier::g;break;
                    case 'G':   specifier = Specifier::G;break;
                    case 'a':   specifier = Specifier::a;break;
                    case 'A':   specifier = Specifier::A;break;
                    case 'c':   specifier = Specifier::c;break;
                    case 's':   specifier = Specifier::s;break;
                    case 'p':   specifier = Specifier::p;break;
                    case 'n':   specifier = Specifier::n;break;
                    default:
                       throw std::invalid_argument(std::string("Invalid Parameter specifier: ") + *fmt);
                }
                ++fmt;

                expectedType = getType(specifier, length);

                used  = fmt - formatStr;

                format  |= (leftJustify ? std::ios_base::left : std::ios_base::right);

                if (specifier == Specifier::d || specifier == Specifier::i) {
                    format  |= std::ios_base::dec;
                }
                else if (specifier == Specifier::o) {
                    format  |= std::ios_base::oct;
                }
                else if (specifier == Specifier::x || specifier == Specifier::X) {
                    format  |= std::ios_base::hex;
                }
                else if (specifier == Specifier::f || specifier == Specifier::F) {
                    format |= std::ios_base::fixed;
                }
                else if (specifier == Specifier::e || specifier == Specifier::E) {
                    format |= std::ios_base::scientific;
                }
                else if (specifier == Specifier::a || specifier == Specifier::A) {
                    format |= (std::ios_base::fixed | std::ios_base::scientific);
                }
                if (specifier == Specifier::X || specifier == Specifier::F || specifier == Specifier::E || specifier == Specifier::A || specifier == Specifier::G) {
                    format |= std::ios_base::uppercase;
                }
                if (prefixType && (specifier == Specifier::o || specifier == Specifier::x || specifier == Specifier::X)) {
                    format |= std::ios_base::showbase;
                }
                if (prefixType && (specifier == Specifier::a || specifier == Specifier::A || specifier == Specifier::e || specifier == Specifier::E || specifier == Specifier::f || specifier == Specifier::F || specifier == Specifier::g || specifier == Specifier::G)) {
                    format |= std::ios_base::showpoint;
                }
                if (forceSign && (specifier != Specifier::c && specifier != Specifier::s && specifier != Specifier::p)) {
                    format |= std::ios_base::showpos;
                }
            }
            std::size_t size() const {return used;}

            friend FormatterCheck operator<<(std::ostream& s, Formatter const& formatter)
            {
                return FormatterCheck(s, formatter);
            }
            private:
                template<typename A>
                void apply(std::ostream& s, A const& arg) const
                {
                    if (std::type_index(*expectedType) != std::type_index(typeid(A))) {
                        throw std::invalid_argument(std::string("Actual argument does not match supplied argument: Expected(") + expectedType->name() + ") Got(" + typeid(A).name() + ")");
                    }


                    char fill      = (!leftJustify && leftPad && (specifier != Specifier::c && specifier != Specifier::s && specifier != Specifier::p)) ? '0' : ' ';
                    int  fillWidth = width;

                    if (forceSignWidth && !forceSign && (arg >= 0) && (specifier != Specifier::c && specifier != Specifier::s && specifier != Specifier::p)) {
                        s << ' ';
                        --fillWidth;
                    }
                    std::ios_base::fmtflags oldFlags = s.flags(format);
                    char                    oldFill  = s.fill(fill);
                    int                     oldWidth = s.width(fillWidth);
                    std::streamsize         oldPrec  = s.precision(precision);

                    s << arg;

                    s.precision(oldPrec);
                    s.width(oldWidth);
                    s.fill(oldFill);
                    s.flags(oldFlags);
                }

                static std::type_info const* getType(Specifier specifier, Length length)
                {
                    static std::map<std::pair<Specifier, Length>, std::type_info const*>    typeMap = {
                        {{Specifier::d, Length::none}, &typeid(int)},           {{Specifier::i, Length::none}, &typeid(int)},           {{Specifier::d, Length::none}, &typeid(int*)},
                        {{Specifier::d, Length::hh},   &typeid(signed char)},   {{Specifier::i, Length::none}, &typeid(signed char)},   {{Specifier::d, Length::hh},   &typeid(signed char*)},
                        {{Specifier::d, Length::h},    &typeid(short int)},     {{Specifier::i, Length::none}, &typeid(short int)},     {{Specifier::d, Length::h},    &typeid(short int*)},
                        {{Specifier::d, Length::l},    &typeid(long int)},      {{Specifier::i, Length::none}, &typeid(long int)},      {{Specifier::d, Length::l},    &typeid(long int*)},
                        {{Specifier::d, Length::ll},   &typeid(long long int)}, {{Specifier::i, Length::none}, &typeid(long long int)}, {{Specifier::d, Length::ll},   &typeid(long long int*)},
                        {{Specifier::d, Length::j},    &typeid(std::intmax_t)}, {{Specifier::i, Length::none}, &typeid(std::intmax_t)}, {{Specifier::d, Length::j},    &typeid(std::intmax_t*)},
                        {{Specifier::d, Length::z},    &typeid(std::size_t)},   {{Specifier::i, Length::none}, &typeid(std::size_t)},   {{Specifier::d, Length::z},    &typeid(std::size_t*)},
                        {{Specifier::d, Length::t},    &typeid(std::ptrdiff_t)},{{Specifier::i, Length::none}, &typeid(std::ptrdiff_t)},{{Specifier::d, Length::t},    &typeid(std::ptrdiff_t*)},


                        {{Specifier::u, Length::none}, &typeid(unsigned int)},          {{Specifier::o, Length::none}, &typeid(unsigned int)},          {{Specifier::x, Length::none}, &typeid(unsigned int)},          {{Specifier::X, Length::none}, &typeid(unsigned int)},
                        {{Specifier::u, Length::hh},   &typeid(unsigned char)},         {{Specifier::o, Length::hh},   &typeid(unsigned char)},         {{Specifier::x, Length::hh},   &typeid(unsigned char)},         {{Specifier::X, Length::hh},   &typeid(unsigned char)},
                        {{Specifier::u, Length::h},    &typeid(unsigned short int)},    {{Specifier::o, Length::h},    &typeid(unsigned short int)},    {{Specifier::x, Length::h},    &typeid(unsigned short int)},    {{Specifier::X, Length::h},    &typeid(unsigned short int)},
                        {{Specifier::u, Length::l},    &typeid(unsigned long int)},     {{Specifier::o, Length::l},    &typeid(unsigned long int)},     {{Specifier::x, Length::l},    &typeid(unsigned long int)},     {{Specifier::X, Length::l},    &typeid(unsigned long int)},
                        {{Specifier::u, Length::ll},   &typeid(unsigned long long int)},{{Specifier::o, Length::ll},   &typeid(unsigned long long int)},{{Specifier::x, Length::ll},   &typeid(unsigned long long int)},{{Specifier::X, Length::ll},   &typeid(unsigned long long int)},
                        {{Specifier::u, Length::j},    &typeid(std::uintmax_t)},        {{Specifier::o, Length::j},    &typeid(std::uintmax_t)},        {{Specifier::x, Length::j},    &typeid(std::uintmax_t)},        {{Specifier::X, Length::j},    &typeid(std::uintmax_t)},
                        {{Specifier::u, Length::z},    &typeid(std::size_t)},           {{Specifier::o, Length::z},    &typeid(std::size_t)},           {{Specifier::x, Length::z},    &typeid(std::size_t)},           {{Specifier::X, Length::z},    &typeid(std::size_t)},
                        {{Specifier::u, Length::t},    &typeid(ptrdiff_t)},             {{Specifier::o, Length::t},    &typeid(ptrdiff_t)},             {{Specifier::x, Length::t},    &typeid(ptrdiff_t)},             {{Specifier::X, Length::t},    &typeid(ptrdiff_t)},

                        {{Specifier::f, Length::none}, &typeid(double)}, {{Specifier::F, Length::none}, &typeid(double)},   {{Specifier::f, Length::L}, &typeid(long double)}, {{Specifier::F, Length::L}, &typeid(long double)},
                        {{Specifier::e, Length::none}, &typeid(double)}, {{Specifier::E, Length::none}, &typeid(double)},   {{Specifier::e, Length::L}, &typeid(long double)}, {{Specifier::E, Length::L}, &typeid(long double)},
                        {{Specifier::g, Length::none}, &typeid(double)}, {{Specifier::G, Length::none}, &typeid(double)},   {{Specifier::g, Length::L}, &typeid(long double)}, {{Specifier::G, Length::L}, &typeid(long double)},
                        {{Specifier::a, Length::none}, &typeid(double)}, {{Specifier::A, Length::none}, &typeid(double)},   {{Specifier::a, Length::L}, &typeid(long double)}, {{Specifier::A, Length::L}, &typeid(long double)},

                        {{Specifier::c, Length::none}, &typeid(int)},   {{Specifier::c, Length::l}, &typeid(std::wint_t)},
                        {{Specifier::s, Length::none}, &typeid(char*)}, {{Specifier::c, Length::l}, &typeid(wchar_t*)},

                        {{Specifier::p, Length::none}, &typeid(void*)}
                    };
                    auto find = typeMap.find({specifier, length});
                    if (find == typeMap.end()) {
                        throw std::invalid_argument("Specifier and length are not a valid combination");
                    }
                    return find->second;
                }
    };
    std::string                     format;
    std::tuple<Args const&...>      arguments;
    std::vector<std::string>        prefixString;
    std::vector<Formatter>          formater;
    public:
        Format(char const* fmt, Args const&... args)
            : format(fmt)
            , arguments(args...)
        {
            std::size_t count = sizeof...(args);
            std::size_t pos   = 0;
            for(int loop = 0; loop < count; ++loop) {
                // Not dealing with '\%' yet just trying to get it working.
                std::size_t nextFormatter = format.find('%', pos);
                if (nextFormatter == std::string::npos) {
                    throw std::invalid_argument("Invalid Format: not enough format specifiers for provided arguments");
                }
                prefixString.emplace_back(format.substr(pos, (nextFormatter - pos)));

                formater.emplace_back(format.data() + nextFormatter);
                pos = nextFormatter + formater.back().size();
            }
            std::size_t nextFormatter = format.find(pos, '%');
            if (nextFormatter != std::string::npos) {
                throw std::invalid_argument("Invalid Format: too many format specifiers for provided arguments");
            }
            prefixString.emplace_back(format.substr(pos, format.size() - pos));
        }
        void print(std::ostream& s) const
        {
            doPrint(s, std::make_index_sequence<sizeof...(Args)>());
        }
    private:
        template<typename A>
        struct Printer
        {
            Printer(std::ostream& s, std::string const& prefix, Formatter const& format, A const& value)
            {
                s << prefix << format << value;
            }
        };
        template<typename A>
        void forward(A const&...) const{}
        template<std::size_t... I>
        void doPrint(std::ostream& s, std::index_sequence<I...> const&) const
        {
            forward(1, Printer<decltype(std::get<I>(arguments))>(s, prefixString[I], formater[I], std::get<I>(arguments))...);
            s << prefixString.back();
        }

        friend std::ostream& operator<<(std::ostream& s, Format const& format)
        {
            format.print(s);
            return s;
        }
};

template<typename... Args>
Format<Args...> make_format(char const* fmt, Args const&... args)
{
    return Format<Args...>(fmt, args...);
}

Test Harness

int main()
{
    std::cout << make_format("Test One\n");
    std::cout << make_format("Test Two   %d\n", 12);
    std::cout << make_format("Test Three %d %f\n", 12, 4.56);
    std::cout << make_format("Test Four  %d %d\n", 12, 4.56); // Should throw
}

It's not going to be very efficient at the moment. In addition to the code review it would be nice to get ideas on how to make it more efficient. Maybe a way we could parse strings at compile time rather than runtime.

When you create a formatter object. It finds the '%' format specifers in the string. It then makes sure that there are the same number of format specifiers as arguments.

when you print the "Format" object it basically forces a call to Formatter::apply() for every argument. This sets the appropriate flags before printing the argument then sets them back to the original value. The Formatter objects are created during the construction of Format but not applied until the object is sent to a stream.

\$\endgroup\$
3
  • \$\begingroup\$ I haven't played much yet, but the usual way to parse strings at compile-time is with a template accepting char.... I could envisage a suitable literal operator to create a Format with compile-time checking (Format operator""_format(const char*, std::size_t)). There would be a big risk of code-bloat to deal with, though. \$\endgroup\$ Feb 12, 2018 at 10:15
  • \$\begingroup\$ With my usual warnings, GCC doesn't like initialising a std::ios_base::fmt with literal 0 - fortunately you can default-construct instead. \$\endgroup\$ Feb 12, 2018 at 10:39
  • \$\begingroup\$ @TobySpeight. I wrote this quickly and did not use my usual make file (and my default flags). So unfortunately I did not have all my normal warnings on. Which caused a few things like this. I'll fix in an upcoming review. \$\endgroup\$ Feb 12, 2018 at 17:33

4 Answers 4

6
\$\begingroup\$

Type / portability issues:

This might be a feature I'm not aware of, but it doesn't compile with Visual Studio 2015, since A isn't a parameter pack:

template<typename A>
void forward(A const&...) const {}

It should probably be:

template<typename... A>
void forward(A const&...) const {}

(BUG:) These two arguments to find are the wrong way around!!!

std::size_t nextFormatter = format.find(pos, '%');

MSVC catches this with a conversion from 'size_t' to 'char', possible loss of data warning.


Other small issues that produce warnings / errors on MSVC:

for (int loop = 0; loop < count; ++loop) { // signed / unsigned mismatch
...
int oldWidth = s.width(fillWidth); // conversion from 'std::streamsize' to 'int', possible loss of data

auto is only used in one place to save typing, but it can also be a huge help to prevent accidental conversions such as these because it forces you to think about the type only where necessary. (i.e. the for loop above requires you to think about the type, because you're not taking the type from an existing variable. The width declaration doesn't require any thought: you want the type of the function return value).


std::isdigit needs a #include <cctype>.


Naming:

Consider using enums and more descriptive names, instead of booleans and comments. e.g.

enum JustificationType { Left, Right };
JustificationType justification;

enum SignDisplayType { Always, OnlyPositive };
SignDisplayType signDisplay;

enum SignPaddingType { PadWhenNoSign, DontPad };
SignPaddingType signPadding;

This puts documentation of what the options do directly in the code.

\$\endgroup\$
5
\$\begingroup\$

Interesting idea! Here's what I noticed:

Handle All Formatters

You've left out one very important formatter: "%%". In fact, I see different behavior for just a bare "%" in your version than I do with printf(). For this code:

std::cout << make_format("This is a percent sign: %\n");
printf ("This is a percent sign: %\n");

I get this result:

This is a percent sign: %

This is a percent sign:

I also get a nice compiler warning from printf() telling me:

CPlusPlusTester/main.cpp:328:39: Invalid conversion specifier '\x0a'

Naming

I know, I know, I'm always harping on people about naming. It really does help readability. I can never remember what the various format and length specifiers are. This doesn't help:

enum class Length    {none, hh, h, l, ll, j, z, t, L};
enum class Specifier {d, i, u, o, x, X, f, F, e, E, g, G, a, A, c, s, p, n};

Great, the Length is hh. Which one is that again? I'd give them meaningful names like Length::Char, maybe, or something along those lines. Or at least something that makes them comprehensible at a glance. Likewise with the specifiers.

The rest of your naming is quite good!

Simplify

If you defined your Specifier class like this:

enum class Specifier {
    d = 'd',
    i = 'i',
    u = 'o',
    // ... etc.
};

then you could remove this entire case statement:

            switch(*fmt) {
                case 'd':   specifier = Specifier::d;break;
                case 'i':   specifier = Specifier::i;break;
                case 'u':   specifier = Specifier::u;break;
                case 'o':   specifier = Specifier::o;break;
                case 'x':   specifier = Specifier::x;break;
                case 'X':   specifier = Specifier::X;break;
                case 'f':   specifier = Specifier::f;break;
                case 'F':   specifier = Specifier::F;break;
                case 'e':   specifier = Specifier::e;break;
                case 'E':   specifier = Specifier::E;break;
                case 'g':   specifier = Specifier::g;break;
                case 'G':   specifier = Specifier::G;break;
                case 'a':   specifier = Specifier::a;break;
                case 'A':   specifier = Specifier::A;break;
                case 'c':   specifier = Specifier::c;break;
                case 's':   specifier = Specifier::s;break;
                case 'p':   specifier = Specifier::p;break;
                case 'n':   specifier = Specifier::n;break;
                default:
                   throw std::invalid_argument(std::string("Invalid Parameter specifier: ") + *fmt);
            }

and replace it with:

specifier = static_cast<Specifier>(*fmt);

The call to getType() will throw an exception if it's an invalid specifier, though the message it throws is different from the one in the default case above. If the cast bothers you, you could do something similar to the static map you have in getType().

\$\endgroup\$
4
\$\begingroup\$

We can simplify the parameter pack unpacking a bit:

private:
    // The template parameter is the index of the argument we are printing.
    // Generated from the doPrint() function.
    template<std::size_t I>
    std::ostream& printValue(std::ostream& s) const
    {   
        return s << prefixString[I] << formater[I] << std::get<I>(arguments);
    }   

    // Called from print()
    //    The std::index_sequence<> is constructed based on the number of
    //    arguments passed to the constructor. This mean I is a sequence
    //    of numbers from 0 ... A   (A is one less the number of param)
    //    We can use this to call printValue() A times using parameter
    //    pack expansion.
    template<std::size_t... I>
    void doPrint(std::ostream& s, std::index_sequence<I...> const&) const
    {   
        std::ostream* ignore[] = {&printValue<I>(s)...};
        s << prefixString.back();
    }   

This gets rid of the us-less function:

    template<typename A>
    void forward(A const&...) const{}

We converted struct Printer into a function call printValue().

    template<typename A>
    std::ostream& printValue(std::ostream&      s,
                             std::string const& prefix,
                             Formatter const&   format,
                             A const&           value) const

This means the compiler could do argument type deduction. So the call could be simplified:

 // from 
 Printer<decltype(std::get<I>(arguments))>(s, prefixString[I], formater[I], std::get<I>(arguments))...

 // too
 printValue(s, prefixString[I], formater[I], std::get<I>(arguments))...

But now that it is a function (rather than a class) it has accesses to the members. So we don't actually need to pass all those parameters. So we can simplify even more:

 // too
 template<std::size_t I>
 std::ostream& printValue(std::ostream& s) const

 // Now the call is simply
 printValue<I>(s)...
\$\endgroup\$
4
\$\begingroup\$

Code Review

Code seems big, scary, and unapproachable. I didn't look deep into it, but here is what I caught on a glance:

  • Templates

    They seem to be underutilized. The main mechanism to relate arguments to actions seem to be runtime type_info. It would be great to split apply and use std::is_same_v<T, U> on different specifiers.

  • Class when function is needed

    This one is easy to fix by changing some names.

  • Never store life-prolonging references without banning constructors with the arguments

    Example:

    Format<std::string, std::string> format("something", "a", "b");
    std::cout << format; //the strings inside are dangling now
    

    Hide intermediate steps, e.g. make it a function and hide the class in some details namespace or something.

Alternative approach

Here is my preference for the format:

{x} -> here stands xth argument
other specifiers are inside {}, e.g. {x:4} -> fit in 4 chars

It probably will be slow (not unusably though), but the convenience will easily balance that. Here is why the above is better in my opinion:

  • No need to duplicate type

  • No need to pass one parameter multiple times to replicate it in the output, just put {x} in the places.

  • Looks less 90's (or may be even 80's)

The main problem is getting the n-th argument during runtime. Long time ago, Yakk provided me a sketch. My template metaprogramming skills got better since then, so I decided to write my own.

template <std::size_t Index, typename Tuple>
void fallthrough(const Tuple& tup, std::ostream& os, std::size_t runtime_index)
{
    if (runtime_index == Index)
    {
        os << std::get<Index>(tup);
        return;
    }

    if constexpr (Index != 0)
    {
        return fallthrough<Index - 1>(tup, os, runtime_index);
    }

    throw "unreachable";
}

The main idea is to keep falling through until the runtime index is reached. Fallthrough demo. What is interesting about it though, is that if the index is known at compile time, the whole thing will just disappear. Also if types are the same, it will decay into jump table. I'm sure compiler could generate jump table even in the case where types are not the same, but I guess it knows something that I don't, or vice versa.

Now, with that in our hands, one can write the formatter:

#include <regex>
#include <cstddef>
#include <iosfwd>
#include <utility>
#include <string_view>
#include <string>

namespace details
{
    template <typename InputIterator>
    std::size_t to_size(InputIterator first, InputIterator last)
    {
        std::size_t size{0};

        for (; first != last; ++first)
        {
            size += size * 10 + *first - '0';
        }

        return size;
    }

    template <std::size_t Index, typename Tuple>
    void fallthrough(const Tuple& tup, std::ostream& os, std::size_t runtime_index)
    {
        if (runtime_index == Index)
        {
            os << std::get<Index>(tup);
            return;
        }

        if constexpr (Index != 0)
        {
            return fallthrough<Index - 1>(tup, os, runtime_index);
        }

        throw "unreachable";
    }
}

namespace nice_formatter
{
    template <typename ... Args>
    void format_nicely(std::ostream& os, const std::string& format, const Args& ... args)
    {
        auto tied = std::tie(args...);

        std::regex arg_regex{"\\{\\d+\\}"};
        std::sregex_iterator first_match{format.begin(), format.end(), arg_regex};
        std::sregex_iterator last_match{};
        auto last_pos = format.c_str();

        while (first_match != last_match)
        {
            //output the stuff preceeding it
            size_t match_index = first_match->position();
            os << std::string_view{last_pos, static_cast<std::size_t>(&format[match_index] - last_pos)};
            last_pos = &format[match_index] + first_match->length();


            const char* argindex_str = &format[match_index];
            //discard the { and } when passing to to_size
            auto arg_index{details::to_size(argindex_str + 1, argindex_str + first_match->length() - 1)};
            if (arg_index > sizeof...(Args))
            {
                throw std::invalid_argument{"Not enough arguments provided"};
            }
            details::fallthrough<sizeof...(Args) - 1>(tied,
                                                      os,
                                                      arg_index);

            ++first_match;
        }
    }
}

(It probably has a lot of bugs inside, I haven't tested it much)

Specifiers can be added by changing the regex pattern and adding more logic inside of the loop.

Small examples:

#include <iostream>

int main()
{
    std::size_t x = 0;
    std::string some_text = "some text";
    double y = 1.2;

    nice_formatter::format_nicely(std::cout, "{0} {1} {2}\n", x, some_text, y);
    nice_formatter::format_nicely(std::cout, "Duplicate of \"{0}\" is \"{0}\"", some_text);
}

More thoughts

Format strings are usually raw string literals, so it is a constant expression. Maybe it is possible to write some class that would be able to tokenize the format string at compile time.


Note: please run the code above in your local machines. It is very complex for compiler, as it took around a second even for examples above. Trying to run it on online compilers might take quite a while.

\$\endgroup\$
2
  • \$\begingroup\$ Let me know if you didn’t like my post. I haven’t written one in a long while. \$\endgroup\$ Feb 10, 2018 at 20:24
  • \$\begingroup\$ All input is good input. \$\endgroup\$ Feb 12, 2018 at 17:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.