I need to generate an std::string
based on a format string and arguments for that string. I'm using C++20 and in my current implementation I have:
#include <format>
#include <iostream>
#include <string>
template<typename... Args>
std::string make_str(const std::string& msg, Args &&... args) {
return std::vformat(msg, std::make_format_args((args)...));
}
int main()
{
auto msg = make_str("This is a str {} int {}", "text", 42);
std::cout << msg << std::endl;
return 0;
}
Output:
This is a str text int 42
This works for every data type but not enums. From what I have read my options are:
make_str
.<<
operator for that enum and create mappings.Is there any other way to do this so that this works without any other code? We have thousands of enums in the code base and so writing formatters for each of them is not feasible. We can cast but it would be nice to do so without that either.
enum class wing : uint16_t {
w1 = 0,
w2 = 1,
};
auto str = make_str("This is the enum {}", wing::w1);
The simplest way to do it (and similar to the way it is being proposed) would be to add a function.
Let's say you have a bunch of enums in the same namespace (I'm guessing some of your thousands of enums share a namespace):
namespace N {
enum E1 { ... };
enum E2 { ... };
// ...
template <class E> requires std::is_enum_v<E>
auto format_as(E e) { return std::to_underlying(e); }
}
That format_as
will be found by ADL for all of the enums in your namespace. So rather than opting in every enum, you opt-in a whole group at once.
And then you add:
template <typename T>
using format_as_type = decltype(format_as(std::declval<T>()));
template <class T>
requires requires { typename format_as_type<T>; }
struct formatter<T> : formatter<format_as_type<T>> {
auto format(T e, auto& ctx) const {
return formatter<format_as_type<T>>::format(format_as(e), ctx);
}
};
That will, by default, format all your enums as the underlying integer value. But it's flexible enough to also format specific enums a different way if that's what you want instead.
Of course you could also do a manual opt-in for each one, but that's more tedious:
#define FORMAT_AS_UNDERLYING(T) \
template <> struct formatter<T> : underlying_formatter<std::underlying_type_t<T>> { }
FORMAT_AS_UNDERLYING(N::E1);
FORMAT_AS_UNDERLYING(N::E2);
FORMAT_AS_UNDERLYING(N::E3);
And then define an underlying_formatter<T>
which formats any type whose underlying type is T
as a T
. That's simpler in a way, but requires an opt-in per enum instead of the format_as
approach which takes less work.