如何为std::optional编写自定义格式化器
我正在尝试为 std::optional 编写一个自定义的 std::fmt 格式化器。下面是我目前的做法:
#include <fmt/core.h>
#include <fmt/ranges.h>
#include <vector>
#include <optional>
#include <cstdint>
template <typename T>
class fmt::formatter<std::optional<T>> {
public:
constexpr auto parse (format_parse_context& ctx) { return ctx.begin(); }
template <typename Context>
constexpr auto format (std::optional<T> const& op, Context& ctx) const {
if(op) return format_to(ctx.out(), "{}", op.value());
else return format_to(ctx.out(), "-");
}
};
int main () {
std::vector<std::optional<uint64_t>> vec{{1}, {2}, {}};
fmt::print("{:}\n", vec);
// fmt::print("{:x}\n", vec); <----- SIGSEGV
}
The formatter works so long as I don't specify how the elements of the vector vec, i.e. the std::optional<uint64_t>s, should be formatted. What I want to do is to specify how the uint64_ts should be formatted. In this example, I want them to formatted to hex strings. However, when I try to use my formatter with {:x}, I get a segfault。
Program returned: 139
terminate called after throwing an instance of 'fmt::v6::format_error'
what(): unknown format specifier
Program terminated with signal: SIGSEGV
我不确定 std::format API的工作原理,但据我所知,我需要为自定义类型指定两个函数:parse 和 format。parse 函数负责解析格式说明符,即 x}。它应该消耗格式说明中与之相关的所有标记,然后把剩余的格式说明返回。在我的情况下,我还没有指定 parse 函数,所以它应该保持原样返回 x},这是 parse 的默认行为。
这份剩余的说明随后被 format 中的 ctx 捕获。如果 op 具有一个值,那么就返回 ctx 的值以及 op 的值。这就是 format_to(ctx.out(), "{}", op.value()); 的作用。
这套逻辑在我看来是合理的,所以我不知道为什么会出现段错误。应该如何修改我的格式化器,才能让它实现我想要的效果?
解决方案
实现格式化代码(parse 函数)的一种非常简单的方法,是从已经具备你想要使用的格式化代码的格式化器继承。
根本不用写 parse。直接使用继承自的那个。
当你想要使用这些代码时,把调用转发给基类的 format 函数。
template <typename T>
struct fmt::formatter<std::optional<T>> : fmt::formatter<T>
{
constexpr auto format (std::optional<T> const& op, auto& ctx) const
{
if(op) return fmt::formatter<T>::format(op.value(), ctx);
return format_to(ctx.out(), "-");
}
};
现在,如果你想让向量的元素以十六进制显示,你应该使用 "{::x}",因为 "{:x}" 要求向量本身也用十六进制表示。
fmt::println("{::x}", vec);
正如注释中所指出的,可能已经为每个 std::optional 存在一个格式化器,在这种情况下你会希望让自定义格式化器更加专门化,以防止ODR违规。
template <typename T>
requires std::integral<T> // <-- ADD THIS
struct fmt::formatter<std::optional<T>> : fmt::formatter<T>