Text formatting using std::format for enums

Learn text formatting using std::format for enums with practical examples, diagrams, and best practices. Covers c++, c++20, fmt development techniques with visual explanations.

Text Formatting Enums in C++20 with std::format

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.

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.

A flowchart diagram illustrating the process of custom std::formatter for enums. Start with 'Define Enum'. Then, 'Specialize std::formatter'. This branches into 'Implement parse (optional, for custom specifiers)' and 'Implement format (convert enum to string, apply specifiers)'. Finally, 'Use std::format with enum type'. Blue boxes for actions, green diamond for decisions. Clean, technical style.

Flowchart of implementing a custom std::formatter for enums

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.