How to Create User-Defined Type Formatters in spdlog: A Complete Guide to fmt Integration
The best way to create user-defined type formatters in spdlog is to specialize fmt::formatter<T> for your custom type, which spdlog automatically uses because it forwards all logging arguments directly to the {fmt} library.
spdlog delegates every formatting operation to the {fmt} library rather than implementing its own engine. This design decision means that once you teach {fmt} how to format your custom C++ type, spdlog gains that capability instantly—no additional registration or adapter code required. This article walks through the implementation details, working code examples, and key source files involved in this integration.
Why fmt::Formatter Specialization Works with spdlog
In include/spdlog/details/fmt_helper.h, spdlog provides thin wrappers that forward format strings and arguments to {fmt}::vformat_to. When you call spdlog::info("Value: {}", my_obj), the library:
- Packages your arguments into a format context
- Invokes
fmt::vformat_towith the supplied pattern - Relies on {fmt}'s template machinery to locate
fmt::formatter<T>for each argument type
Because spdlog never inspects the types itself, any valid {fmt} formatter automatically becomes a valid spdlog formatter. This applies to all spdlog interfaces: console loggers, file sinks, async loggers, and custom pattern formatters.
Essential Header and Namespace Requirements
Before implementing custom formatters, include the correct headers and place your specialization in the proper namespace.
#include <spdlog/fmt.h> // Re-exports bundled fmt headers
// Or explicitly:
#include <spdlog/fmt/bundled/format.h>
Critical rule: The fmt::formatter<T> specialization must reside in namespace fmt, not spdlog. Placing it elsewhere causes compile-time errors when {fmt} attempts template resolution.
The fmt::Formatter Interface Structure
Every formatter specialization implements two member functions:
| Member | Purpose | Return Type |
|---|---|---|
parse(format_parse_context& ctx) |
Consumes format specifiers (e.g., {:x}) |
constexpr auto → iterator |
format(const T& value, FormatContext& ctx) |
Writes the formatted representation | auto → iterator to end |
Both functions are typically const and must be thread-safe, as spdlog may invoke them from multiple logging threads concurrently.
Example 1: Basic Formatter for a Custom Struct
This example demonstrates the minimal viable formatter for a Point struct with no custom format specifiers.
// point.hpp
#pragma once
#include <spdlog/fmt.h>
struct Point {
int x;
int y;
};
template <>
struct fmt::formatter<Point> {
// Accept any format spec (ignored for simplicity)
constexpr auto parse(fmt::format_parse_context& ctx) {
return ctx.begin(); // No custom flags supported
}
// Format as "{x, y}"
template <typename FormatContext>
auto format(const Point& p, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "{{{}, {}}}", p.x, p.y);
}
};
Using the formatter with spdlog:
// main.cpp
#include "point.hpp"
#include <spdlog/spdlog.h>
int main() {
Point p{3, 7};
spdlog::info("Current position: {}", p);
// Output: [2024-01-15 09:23:45.123] [info] Current position: {3, 7}
}
The double braces {{ and }} in format_to produce literal curly braces in the output, while the inner {} are format placeholders for p.x and p.y.
Example 2: Custom Format Specifiers
Real-world formatters often support flags like {:x} for hexadecimal or {:p} for precision. This Vec3 formatter demonstrates optional format specification parsing.
// vec3.hpp
#pragma once
#include <spdlog/fmt.h>
#include <stdexcept>
struct Vec3 {
float x, y, z;
};
template <>
struct fmt::formatter<Vec3> {
char presentation = 'f'; // 'f' = float, 'x' = hex
constexpr auto parse(fmt::format_parse_context& ctx) {
auto it = ctx.begin();
auto end = ctx.end();
if (it != end && (*it == 'x')) {
presentation = 'x';
++it;
}
// Validate: only empty spec or 'x' followed by '}' is legal
if (it != end && *it != '}') {
throw fmt::format_error("invalid Vec3 format");
}
return it;
}
template <typename FormatContext>
auto format(const Vec3& v, FormatContext& ctx) const {
if (presentation == 'x') {
// Reinterpret float bits as uint32 for hex display
return fmt::format_to(ctx.out(),
"({:08x}, {:08x}, {:08x})",
*reinterpret_cast<const uint32_t*>(&v.x),
*reinterpret_cast<const uint32_t*>(&v.y),
*reinterpret_cast<const uint32_t*>(&v.z));
}
return fmt::format_to(ctx.out(),
"({:.2f}, {:.2f}, {:.2f})",
v.x, v.y, v.z);
}
};
Logging demonstrations:
#include "vec3.hpp"
#include <spdlog/spdlog.h>
int main() {
Vec3 velocity{1.5f, -2.0f, 3.14159f};
spdlog::info("Velocity: {}", velocity);
// → Velocity: (1.50, -2.00, 3.14)
spdlog::info("Raw bits: {:x}", velocity);
// → Raw bits: (3fc00000, c0000000, 40490fdb)
}
The parse function advances through the format specifier string and stores state in the presentation member. This state persists for the format call that follows.
Example 3: Integrating with spdlog Pattern Formatters
spdlog's pattern formatter (configured via set_pattern) processes % flags before {fmt} sees the arguments. For user-defined types to appear in pattern output, you typically use the %v (message) flag and embed your typed values in the message string.
However, advanced use cases may require custom pattern flags. In include/spdlog/pattern_formatter.h, the library defines extension points for adding % handlers. The standard approach:
#include <spdlog/pattern_formatter.h>
#include <memory>
// Custom flag that prepends a point summary
class PointFlag : public spdlog::custom_flag_formatter {
public:
void format(const spdlog::details::log_msg& msg,
const std::tm& tm_time,
spdlog::memory_buf_t& dest) override {
// Access the formatted message, parse for Point markers, etc.
// Or use thread-local storage to pass context
fmt::format_to(std::back_inserter(dest), "[PointCtx] ");
}
std::unique_ptr<custom_flag_formatter> clone() const override {
return std::make_unique<PointFlag>();
}
};
// Register with a logger
auto formatter = std::make_unique<spdlog::pattern_formatter>();
formatter->add_flag<PointFlag>('P');
formatter->set_pattern("[%Y-%m-%d %H:%M:%S.%e] %P%v");
logger->set_formatter(std::move(formatter));
Most projects do not need custom pattern flags. The standard workflow—using fmt::formatter with {} placeholders in log messages—covers typical requirements while maintaining clean separation between formatting logic and logging configuration.
Key Source Files in spdlog's fmt Integration
Understanding these files helps diagnose integration issues or extend functionality:
| File Path | Role in fmt Integration |
|---|---|
include/spdlog/fmt.h |
Central header that selects bundled vs. external {fmt} |
include/spdlog/fmt/bundled/format.h |
The embedded {fmt} library (v9.x in spdlog v1.x) |
include/spdlog/details/fmt_helper.h |
spdlog::details::fmt_helper::to_string_view and format dispatch |
include/spdlog/formatter.h |
Abstract spdlog::formatter base class |
include/spdlog/pattern_formatter.h |
Pattern parsing and % flag dispatch |
tests/tests.cpp / tests/test_pattern_formatter.cpp |
Regression tests demonstrating formatter behavior |
In include/spdlog/details/fmt_helper.h, the key template function resembles:
template<typename... Args>
inline void format_to(fmt::memory_buffer& buf, fmt::format_string<Args...> fmt, Args&&... args) {
fmt::format_to(std::back_inserter(buf), fmt, std::forward<Args>(args)...);
}
This confirms that spdlog adds no abstraction layer—arguments pass directly to {fmt}.
Performance and Thread Safety Considerations
- Zero overhead:
fmt::formatterspecializations are resolved at compile time via templates; no runtime type dispatch occurs - No heap allocation in format path:
fmt::format_towithmemory_bufferuses stack-allocated storage that grows only when necessary - Thread safety: Formatter methods must be
constand avoid mutable static state. spdlog'sasync_loggermay invoke formatters from arbitrary threads
Common Pitfalls and Solutions
| Problem | Cause | Solution |
|---|---|---|
| "No matching formatter for type T" | Missing or misplaced fmt::formatter specialization |
Ensure specialization is in namespace fmt, not namespace spdlog |
| Compile errors with external {fmt} | Macro conflicts between bundled and external versions | Define SPDLOG_FMT_EXTERNAL before including spdlog headers |
| Custom specifiers ignored | parse() not advancing iterator correctly |
Return ctx.begin() for no-ops; validate and advance for flags |
| Linker errors for formatter | Template specialization in .cpp instead of header | Move fmt::formatter specialization to a header included where used |
Summary
- spdlog uses {fmt} exclusively for argument formatting; no spdlog-specific formatter interface exists for user types
- Specialize
fmt::formatter<T>in namespacefmtto enable logging of any custom type - Implement
parse()andformat()to handle optional format specifiers and produce output - Include
<spdlog/fmt.h>to ensure consistent {fmt} version between your code and spdlog - Pattern formatters and custom sinks automatically inherit support for your formatters without additional registration
Frequently Asked Questions
Can I use an external {fmt} installation instead of spdlog's bundled version?
Yes. Define SPDLOG_FMT_EXTERNAL before including any spdlog headers, then link against your system's {fmt} library. Your fmt::formatter specializations will work identically because spdlog references the same fmt:: namespace.
Why does my formatter work with fmt::format but fail with spdlog::info?
The specialization is likely in the wrong namespace or translation unit. Ensure fmt::formatter<YourType> is visible at the point where spdlog::info is instantiated. Template specializations must typically reside in headers, not .cpp files.
How do I format a type I cannot modify (third-party class)?
Use a wrapper type or specialize for pointers. Since C++20, you can also use fmt::format_as for primitive typedefs. For class templates, specialize fmt::formatter<std::vector<MyType>> directly if you own the element formatter.
Does spdlog cache formatter instances?
No. spdlog creates and destroys format context objects per log call. Your fmt::formatter must be cheap to construct and copy; store no persistent state in member variables beyond format specifier flags parsed in parse().
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →