How to Format Hexadecimal Numbers with Signs and Zero-Padding in fmtlib

Use the format string "{:+#010x}" where + forces a sign, # adds the 0x prefix, 0 enables zero-padding, and 10 sets the total field width.

Formatting integers as hexadecimal with explicit positive signs and zero-padding is essential for low-level debugging, memory dumps, and network protocol analysis. The fmtlib/fmt library provides precise control over these output characteristics through its type-safe format specification API. Understanding the internal mechanics—from the format_specs parser to the write_int implementation—ensures you can predict exactly how signs, prefixes, and padding interact.

Anatomy of the Format Specification

When you write fmt::format("{:+#010x}", 12345), the library parses the string between the braces into a format_specs object. This structure captures four critical properties:

  • Sign: Controlled by + (always show sign) or space (leading space for positive), stored in format_specs::sign as defined in include/fmt/core.h.
  • Fill/Zero-Padding: The 0 flag sets format_specs::fill to '0', triggering zero-fill logic later in the pipeline.
  • Alternate Form: The # flag indicates that hexadecimal numbers should include the 0x or 0X prefix.
  • Width and Type: The integer 10 sets the total field width, while x specifies lowercase hexadecimal presentation.

Implementation in the fmtlib Source Code

The conversion from format string to output characters traverses several internal functions in include/fmt/format.h and include/fmt/printf.h.

Detecting the Sign and Zero Flags

Legacy format parsing in include/fmt/printf.h (line 318) maps character flags to internal specifiers:

case '+': specs.set_sign(sign::plus); break;

For zero-padding, the library detects when the fill character is '0' at lines 2581–2583 of include/fmt/format.h:

const bool is_zero_fill = specs.fill_size() == 1 && specs.fill_unit<Char>() == '0';

When is_zero_fill is true, the padding logic knows to insert zeros between the sign/prefix and the digits rather than using the default space-based alignment.

Constructing the Sign and Prefix

The function make_write_int_arg (lines 42–46 in include/fmt/format.h) creates a packed representation of the prefix that includes both the sign and the hexadecimal base indicator. For positive values with the + flag, it constructs:

constexpr unsigned prefixes[4] = {0, 0, 0x1000000u | '+', 0x1000000u | ' '};
prefix = prefixes[static_cast<int>(s)];

If the alternate form (#) is requested for hexadecimal output, prefix_append (lines 3113–3115) merges the 0x or 0X characters into the existing prefix:

prefix_append(prefix, unsigned(specs.upper() ? 'X' : 'x') << 8 | '0');

This ensures the sign character appears first, followed immediately by the base prefix.

Writing the Padded Output

Finally, write_int (lines 3150–3152 in include/fmt/format.h) emits the formatted number. After writing the prefix (sign + 0x), it calculates the remaining padding required to reach the specified width and fills with zeros:

it = detail::fill_n(it, padding, static_cast<Char>('0'));

This step produces the final string where zeros appear between the prefix and the hexadecimal digits, ensuring the total output length matches the requested width.

Practical Examples

The following program demonstrates combining signs, zero-padding, and hexadecimal formatting:

#include <fmt/core.h>
#include <cstdint>
#include <iostream>

int main() {
    std::int32_t pos = 12345;   // 0x3039
    std::int32_t neg = -12345;  // -0x3039

    // Sign, zero-padding, width 8, lower-case hex, no prefix
    std::cout << fmt::format("{:+08x}\n", pos);   // +0003039
    std::cout << fmt::format("{:+08x}\n", neg);   // -0003039

    // Sign, zero-padding, width 10, alternate form (adds 0x)
    std::cout << fmt::format("{:+#010x}\n", pos); // +0x0003039
    std::cout << fmt::format("{:+#010x}\n", neg); // -0x0003039

    // Space flag instead of plus, upper-case hex, width 12
    std::cout << fmt::format("{: #012X}\n", pos); //  0X0003039
    std::cout << fmt::format("{: #012X}\n", neg); // -0X0003039
}

Key observations from the examples:

  • "{:+08x}" produces +0003039 because the width 8 includes the sign character but excludes the hexadecimal prefix.
  • "{:+#010x}" produces +0x0003039 (10 characters total) because the width includes the sign, the 0x prefix, and the digits.
  • "{: #012X}" uses a space for positive signs and upper-case letters, yielding 0X0003039 (note the leading space before 0X).

Summary

  • fmtlib stores format parameters in a format_specs object that captures sign, fill, width, and presentation type.
  • The + flag forces explicit signs for positive numbers, parsed in include/fmt/printf.h and stored via enum class sign in include/fmt/core.h.
  • Zero-padding occurs when is_zero_fill detects a '0' fill character in include/fmt/format.h, ensuring zeros appear between the prefix and digits.
  • The write_int function in include/fmt/format.h finalizes output by combining the sign, optional 0x prefix (added by prefix_append), and zero-filled digits to meet width requirements.

Frequently Asked Questions

Does the field width count the sign and the 0x prefix?

Yes. In fmtlib, the width parameter specifies the total number of characters in the final output, including the sign character and the 0x or 0X prefix added by the # flag. For example, "{:+#010x}" generates exactly 10 characters.

Can I use a space instead of a plus sign for positive numbers?

Yes. Replace the + with a space in the format string (e.g., "{: #08x}"). The library stores this as sign::space and inserts a leading space for positive values while negative values retain the minus sign.

What happens if I remove the zero flag but keep the width?

Without the 0 flag, fmtlib uses the default fill character (space) and aligns the output to the right. The padding spaces appear before the sign and prefix, resulting in output like " +0x3039" instead of "+0x0003039".

Which header should I include for hexadecimal formatting?

Include <fmt/core.h> for the main fmt::format function. The internal implementations in include/fmt/format.h and include/fmt/printf.h are pulled in automatically by the core header; you do not need to include them directly.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →