Text formatting using std::format for enums
Categories:
Text Formatting Enums in C++20 with std::format
Explore how std::format
in C++20 simplifies text formatting for enums, making your output cleaner, more readable, and type-safe. Learn to integrate custom formatters for enhanced control.
The introduction of std::format
in C++20 revolutionized string formatting, offering a type-safe, extensible, and efficient alternative to older methods like printf
and stringstream
. One particularly useful application is formatting enumerations. Directly printing an enum often results in its underlying integer value, which isn't always human-readable. This article delves into how std::format
can be leveraged to elegantly display enum values as their string representations, significantly improving diagnostic messages and user-facing output.
The Challenge of Enum Output
By default, when you attempt to print an enum using standard output streams or older formatting functions, you typically get its integral value. While technically correct, this output lacks context and readability, especially when dealing with complex systems or debugging. Consider an enum representing different log levels:
enum class LogLevel {
Debug,
Info,
Warning,
Error
};
int main() {
LogLevel level = LogLevel::Warning;
// std::cout << level; // This would print '2' (the underlying integer value)
return 0;
}
Default enum output shows integer value
To get a meaningful string, developers traditionally resorted to switch
statements or lookup tables, which can be verbose and error-prone. std::format
provides a more elegant solution by allowing custom formatters to be defined, integrating seamlessly into its powerful formatting capabilities.
Implementing a Custom Formatter for Enums
The std::format
library allows you to define custom formatters for your types by specializing std::formatter
. This specialization requires implementing two member functions: parse
and format
. The parse
function handles format specifiers (e.g., {}
or {:x}
), while the format
function converts the type into its string representation.
#include <format>
#include <string>
enum class LogLevel {
Debug,
Info,
Warning,
Error
};
template <>
struct std::formatter<LogLevel> : std::formatter<std::string> {
auto format(LogLevel level, std::format_context& ctx) const {
std::string s;
switch (level) {
case LogLevel::Debug: s = "DEBUG"; break;
case LogLevel::Info: s = "INFO"; break;
case LogLevel::Warning: s = "WARNING"; break;
case LogLevel::Error: s = "ERROR"; break;
}
return std::formatter<std::string>::format(s, ctx);
}
};
int main() {
LogLevel level = LogLevel::Warning;
std::string formatted_message = std::format("Current log level: {}", level);
// formatted_message will be "Current log level: WARNING"
// std::cout << formatted_message << std::endl;
return 0;
}
Custom std::formatter
for LogLevel
enum
In this example, we specialize std::formatter<LogLevel>
and inherit from std::formatter<std::string>
. This allows us to leverage std::formatter<std::string>
's existing parse
function and just focus on converting our LogLevel
enum to a std::string
within the format
method. The format
function then uses the base class's format
method to actually write the string to the context.
std::map<LogLevel, std::string>
or a magic_enum
library for more maintainable string conversions, especially if your enums might change frequently. This avoids large switch
statements.Handling Format Specifiers (Advanced)
The parse
function of std::formatter
allows you to inspect and process format specifiers provided by the user (e.g., {:.3f}
for floats). While our basic enum formatter inherits parse
from std::formatter<std::string>
, you might want to add custom specifiers for enums, such as displaying them in lowercase or uppercase, or with a specific prefix/suffix. This involves parsing the format string yourself.
#include <format>
#include <string>
enum class Status {
Pending,
Complete,
Failed
};
template <>
struct std::formatter<Status> {
bool uppercase = false;
constexpr auto parse(std::format_parse_context& ctx) {
auto it = ctx.begin();
auto end = ctx.end();
if (it != end && *it == 'U') {
uppercase = true;
++it;
}
return it; // Return iterator to the end of the parsed specifiers
}
auto format(Status status, std::format_context& ctx) const {
std::string s;
switch (status) {
case Status::Pending: s = "pending"; break;
case Status::Complete: s = "complete"; break;
case Status::Failed: s = "failed"; break;
}
if (uppercase) {
for (char &c : s) {
c = static_cast<char>(std::toupper(c));
}
}
return std::format_to(ctx.out(), "{}", s);
}
};
int main() {
Status current_status = Status::Complete;
std::string msg1 = std::format("Status: {}", current_status); // Status: pending
std::string msg2 = std::format("Status: {:U}", current_status); // Status: PENDING
// std::cout << msg1 << std::endl;
// std::cout << msg2 << std::endl;
return 0;
}
Custom formatter with 'U' specifier for uppercase
In this advanced example, the parse
function looks for a 'U' specifier. If found, it sets an internal uppercase
flag. The format
function then uses this flag to conditionally convert the string representation of the enum to uppercase before writing it to the output context. This demonstrates the powerful extensibility of std::format
.
Flowchart of implementing a custom std::formatter
for enums
parse
, ensure you return the iterator pointing to the first character not consumed by your specifier. Failing to do so can lead to errors or incorrect parsing of subsequent format arguments.Using std::format
for enums dramatically improves code clarity and maintainability. It centralizes the logic for converting enums to strings, preventing repetition and ensuring consistency across your application. This approach aligns with modern C++ practices, promoting type safety and extensibility.
1. Step 1
Define your enum class
with clear, descriptive names for its members.
2. Step 2
Specialize std::formatter<YourEnum>
in the std
namespace (or a custom namespace if preferred).
3. Step 3
If you don't need custom format specifiers, inherit from std::formatter<std::string>
to simplify parse
.
4. Step 4
Implement the format
method to convert your enum value to a std::string
using a switch
statement or a lookup table.
5. Step 5
If you need custom specifiers (e.g., for uppercase/lowercase), implement the parse
method to process them and store any relevant flags.
6. Step 6
Use std::format
or std::print
(C++23) with your enum type, and observe the beautifully formatted output.
By following these guidelines, you can seamlessly integrate your enums into the std::format
ecosystem, enhancing the readability and robustness of your C++ applications.