Search code examples
c++nesteduser-defined-typesspecializationfmt

C++ {fmt} library, user-defined types with nested replacement fields?


I am trying to add {fmt} into my project, and all is going well, except I hit a little snag when trying to add a user-defined type for my simple Vec2 class.

struct Vec2 { float x; float y; };

What I would like is to be able to use the same format flags/arguments as the basic built-in float type, but have it duplicated to both the x and y members of the vec2, with parentheses around it.

For example, with just a float:

fmt::format("Hello '{0:<8.4f}' World!", 1.234567);
// results in "Hello '1.2346  ' World!"

With my vec2 class:

Vec2 v {1.2345678, 2.3456789};
fmt::format("Hello '{0:<8}' World!", v);
// results in "Hello (1.2346  , 2.3457  )' World!"

But my naïve method of copying over the replacement field contents doesn't work when we try to use nested replacement fields. Eg with a float:

fmt::format("Hello '{0:<{1}.4f}' World!", 1.234567, 8);
// results in "Hello '1.2346  ' World!"

But trying with my Vec2 type...

Vec2 v {1.2345678, 2.3456789};
fmt::format("Hello '{0:<{1}.4f}' World!", v, 8);
// throws format_error, what(): "argument not found"

Of course, this is happening because all I'm doing is copying the replacement field after the ':' and before the '}', and respecting the {} balancing, so if I've used nested replacement fields, there will be a {} inside that refers to some argument from the original list, which is no-good for this.

My specialization for the user-defined type:

struct Vec2
{
    float x;
    float y;
};

template<>
struct fmt::formatter<Vec2>
{
    auto parse(fmt::format_parse_context& ctx) -> decltype(ctx.begin())
    {
        int curlyBalance = 1;
        auto it = ctx.begin(), end = ctx.end();
        while (it != end)
        {
            if (*it == '}')
            {
                --curlyBalance;
            }
            else if (*it == '{')
            {
                ++curlyBalance;
            }
            
            if (curlyBalance <= 0)
                break;
            else
                ++it;
        }

        const char* beginPtr = &(*ctx.begin());
        const char* endPtr = &(*it);
        size_t len = endPtr - beginPtr;

        if (len == 0)
        {
            formatStr = "{}";
        }
        else
        {
            formatStr = "{0:";
            formatStr += std::string(beginPtr, len + 1);
        }

        return it;
    }

    template <typename FormatContext>
    auto format(const Vec2& vec, FormatContext& context)
    {
        fmt::format_to(context.out(), "(");
        fmt::format_to(context.out(), formatStr, vec.x);
        fmt::format_to(context.out(), ", ");
        fmt::format_to(context.out(), formatStr, vec.y);
        return fmt::format_to(context.out(), ")");
    }

    std::string formatStr;
};


int main()
{
    std::cout << "Hello world!" << std::endl;
    Vec2 v {1.234567, 2.345678};
    
    // Simple, static width.
    //std::string fmtResult = fmt::format("Hello '{0:<8.4f}' World!\n", v, 5);

    // Dynamic width, oh god, oh dear god no!
    std::string fmtResult = fmt::format("Hello '{0:<{1}}' World!\n", v, 5);

    std::cout << fmtResult;
}

It seems that what needs to happen is my parse function needs to have access to the other args so it can fill in the nested replacement field with the correct value... but I'm still new to this library, and would greatly appreciate some help with this!

Godbolt link: https://godbolt.org/z/6fxWszTT8


Solution

  • You can reuse formatter<float> for this (https://godbolt.org/z/vb9c5ffd5):

    template<> struct fmt::formatter<Vec2> : formatter<float> {
      template <typename FormatContext>
      auto format(const Vec2& vec, FormatContext& ctx) {
        auto out = ctx.out();
        *out = '(';
        ctx.advance_to(out);
        out = formatter<float>::format(vec.x, ctx);
        out = fmt::format_to(out, ", ");
        ctx.advance_to(out);
        out = formatter<float>::format(vec.y, ctx);
        *out = ')';
        return out;
      }
    };