Lifetime issue when storing std::format_args

55 Views Asked by At

See following code:

#include <print>
#include <exception>
#include <string>
#include <format>

class exception_t
    : public std::exception
{
public:
    template<class... args_t>
    exception_t(std::string_view users_fmt, args_t&&... args)
        : m_users_fmt(users_fmt)
        , m_format_args(std::move(std::make_format_args(args...)))
    {}
    char const* what() const noexcept override
    {
        thread_local static std::string s = std::vformat(m_users_fmt, m_format_args);
        return s.c_str();
    }
private:
    std::string m_users_fmt;
    std::format_args m_format_args;
};

int main()
try
{
    throw exception_t("{}", 42);
}
catch (std::exception& e)
{
    std::println("{}", e.what());
}

When debugging this under MSVC 2022 v143 with SDK 10.0.19041.0, I see that during the construction of exception_t m_format_args has an appropriate value, but at the time of calling what(), m_format_args has an invalid value. Most likely this is a lifetime issue, but I cannot see what is going wrong. (Most often this program does not print 42)

What am I doing wrong here?

=================================================

Update after response of HolyBlackCat

Above is a minimal code snippet, but it is the intention to completely separate the formatting from exception/error/warning reporting. I'd like to store a standard type with the arguments e.g. on disk. And later another process reads it from disk and displays the data using a format string of the local language. That's why I don't want to call std::vformat in the constructor in this code snippet.

Does any alternative for std::format_args exist? E.g. can I store args?

1

There are 1 best solutions below

0
Jeroen Lammertink On

I found a way to store args and get the program working. I needed to add the types variant_t and variants_t and define a user defined formatter. Then the above code snippet needs only a few small changes:

#include <print>
#include <exception>
#include <string>
#include <format>

class exception_t
    : public std::exception
{
public:
    template<class... args_t>
    exception_t(std::string_view users_fmt, args_t&&... args)
        : m_users_fmt(users_fmt)
        , m_variants(args...)
    {}
    char const* what() const noexcept override
    {
        m_variants.resize(16, 0);
        thread_local static std::string s = std::vformat(m_users_fmt, std::make_format_args(m_variants[0], m_variants[1], m_variants[2], m_variants[3], m_variants[4], m_variants[5], m_variants[6], m_variants[7], m_variants[8], m_variants[9], m_variants[10], m_variants[11], m_variants[12], m_variants[13], m_variants[14], m_variants[15]));
        return s.c_str();
    }
private:
    std::string m_users_fmt;
    mutable variants_t m_variants;
};

int main()
try
{
    throw exception_t("{}", 42);
}
catch (std::exception& e)
{
    std::println("{}", e.what());
}

It is not completely ideal:

  • The amount of arguments is limited to 16
  • what() is no longer re-entrant (what you would expect from a const function)

The definitions of variant_t, variants_t and the user defined formatter is as follows:

#include <variant>
#include <vector>

using variant_t = std::variant
    < bool
    , char
    , int
    , unsigned int
    , long long
    , unsigned long long
    , float
    , double
    , long double
    , void const*
    >;

// helper type for the visitor #4
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };

template<>
struct std::formatter<variant_t, char>
{
    template<class parse_context_t>
    constexpr parse_context_t::iterator parse(parse_context_t& ctx)
    {
        for(auto it = ctx.begin(); it != ctx.end(); ++it)
        {
            m_format += *it;
            if (*it == '}')
            {
                return it;
            }
        }
        m_format += '}';
        return ctx.end();
    }
    template<class fmt_context_t>
    fmt_context_t::iterator format(variant_t const& variant, fmt_context_t& ctx) const
    {
        return std::visit(overloaded{
            [&ctx, this](bool               v) { return std::vformat_to(ctx.out(), m_format, std::make_format_args(v)); },
            [&ctx, this](char               v) { return std::vformat_to(ctx.out(), m_format, std::make_format_args(v)); },
            [&ctx, this](int                v) { return std::vformat_to(ctx.out(), m_format, std::make_format_args(v)); },
            [&ctx, this](unsigned int       v) { return std::vformat_to(ctx.out(), m_format, std::make_format_args(v)); },
            [&ctx, this](long long          v) { return std::vformat_to(ctx.out(), m_format, std::make_format_args(v)); },
            [&ctx, this](unsigned long long v) { return std::vformat_to(ctx.out(), m_format, std::make_format_args(v)); },
            [&ctx, this](float              v) { return std::vformat_to(ctx.out(), m_format, std::make_format_args(v)); },
            [&ctx, this](double             v) { return std::vformat_to(ctx.out(), m_format, std::make_format_args(v)); },
            [&ctx, this](long double        v) { return std::vformat_to(ctx.out(), m_format, std::make_format_args(v)); },
            [&ctx, this](void const*        v) { return std::vformat_to(ctx.out(), m_format, std::make_format_args(v)); },
            }, variant);
    }
    std::string m_format{"{:"};
};

class variants_t
    : public std::vector<variant_t>
{
public:
    constexpr variants_t() = default;
    template<class... args_t, class T>
    constexpr variants_t(T first_arg, args_t&&... args)
    {
        push_back(first_arg);
        variants_t v(args...);
        insert(end(), v.begin(), v.end());
    }
};