What Is FMT_SAFE_DURATION_CAST? Preventing Undefined Behavior in fmtlib Chrono Conversions

FMT_SAFE_DURATION_CAST replaces the standard std::chrono::duration_cast with a bounds-checked implementation that throws fmt::format_error instead of invoking undefined behavior when a duration value overflows or underflows its target representation.

The {fmt} library provides comprehensive formatting support for std::chrono types via include/fmt/chrono.h. By default, the library defines the FMT_SAFE_DURATION_CAST macro to protect users from the undefined behavior (UB) risks inherent in narrowing duration conversions, ensuring that formatting operations fail predictably rather than silently corrupting data.

Why Standard duration_cast Risks Undefined Behavior

The C++ standard library’s std::chrono::duration_cast performs unconditional conversions between duration types. When casting a large value to a narrower representation—such as converting nanoseconds represented in 64-bit integers to a 32-bit integer type—the operation overflows without notification. This produces undefined behavior according to the C++ standard, leading to potential crashes, incorrect calculations, or platform-specific results that compromise application reliability.

How FMT_SAFE_DURATION_CAST Prevents Overflow and Underflow

When FMT_SAFE_DURATION_CAST is enabled (the default), {fmt} intercepts duration conversions through a custom safe_duration_cast namespace implemented in include/fmt/chrono.h. This infrastructure validates values before construction, transforming potential UB into well-defined exceptions.

Macro Definition and CMake Control

The safeguard activates through a preprocessor macro defined at lines 28–31 of include/fmt/chrono.h:

#ifndef FMT_SAFE_DURATION_CAST

#  define FMT_SAFE_DURATION_CAST 1

#endif

You can control this behavior at build time via the CMake option defined in CMakeLists.txt (around line 333). Setting -DFMT_SAFE_DURATION_CAST=OFF disables the checks, causing the library to fall back to standard std::chrono::duration_cast with its inherent UB risks.

Checked Integral Conversions

For integral duration representations, the implementation uses safe_duration_cast::lossless_integral_conversion defined in include/fmt/chrono.h (lines 20–50). This function validates that the conversion factor between periods does not overflow the target type before performing the multiplication. If the value lies outside the representable range, the code invokes throw_duration_error(), which throws a fmt::format_error with the message "cannot format duration" rather than proceeding with the unsafe cast.

Floating-Point Safety Checks

For floating-point representations, the code path utilizes safe_duration_cast::safe_float_conversion (lines 56–66 of include/fmt/chrono.h). This routine detects out-of-range values before constructing the target duration while preserving special values like NaN and infinity. Like its integral counterpart, it triggers throw_duration_error() when encountering values that cannot be safely represented.

Fallback to Standard Behavior

If you disable FMT_SAFE_DURATION_CAST, the library reverts to the standard behavior. The preprocessor conditional at lines 24–27 of include/fmt/chrono.h simply forwards to std::chrono::duration_cast<To>(from), inheriting all associated undefined behavior risks for overflow scenarios.

Practical Examples of Safe Duration Casting

The following examples demonstrate how the safety check operates in practice.

Detecting Overflow in Extreme Durations

This example attempts to format a duration that would overflow when cast to a smaller type:

#include <fmt/chrono.h>
#include <limits>

int main() {
  try {
    using years = std::chrono::duration<std::int64_t,
                                        std::ratio<31556952>>;
    // Maximum 64-bit value representing a huge number of years
    years huge{std::numeric_limits<std::int64_t>::max()};
    
    // This triggers safe_duration_cast and detects the overflow
    fmt::print("{:%Y-%m-%d}\n", fmt::chrono::sys_time<years>(huge));
  } catch (const fmt::format_error& e) {
    fmt::print("Caught expected error: {}\n", e.what());
    // Output: Caught expected error: cannot format duration
  }
}

Normal Safe Conversions

Standard conversions that fit within the target type work transparently:

#include <fmt/chrono.h>

int main() {
  std::chrono::seconds s{42};
  
  // Safe conversion to milliseconds; no overflow risk
  auto ms = fmt::chrono::duration_cast<std::chrono::milliseconds>(s);
  fmt::print("{} seconds = {} ms\n", s.count(), ms.count());
  // Output: 42 seconds = 42000 ms
}

Disabling Safety Checks

To use the unchecked standard conversion (not recommended for untrusted input), configure your build:

cmake -DFMT_SAFE_DURATION_CAST=OFF ..

Or add to your CMakeLists.txt:

add_definitions(-DFMT_SAFE_DURATION_CAST=0)

With the macro disabled, the previous overflow example would invoke undefined behavior instead of throwing an exception.

Summary

  • FMT_SAFE_DURATION_CAST defaults to enabled (value 1) in {fmt} installations, providing checked duration conversions.
  • The implementation in include/fmt/chrono.h intercepts duration_cast operations for both integral and floating-point representations.
  • Overflow, underflow, and sign-loss scenarios trigger throw_duration_error(), which throws fmt::format_error with the message "cannot format duration".
  • When disabled via CMake (-DFMT_SAFE_DURATION_CAST=OFF), the library falls back to std::chrono::duration_cast, exposing applications to standard undefined behavior on invalid conversions.
  • Unit tests in test/chrono-test.cc (lines 43–48) verify that the safety mechanism correctly identifies and rejects dangerous conversions.

Frequently Asked Questions

What happens when FMT_SAFE_DURATION_CAST detects an unsafe conversion?

The library invokes throw_duration_error() defined in include/fmt/chrono.h at lines 16–19, which throws a fmt::format_error exception with the message "cannot format duration". This allows applications to catch and handle the error gracefully rather than proceeding with undefined behavior.

Can I disable the safety checks if I need maximum performance?

Yes. You can disable the feature by setting the CMake option FMT_SAFE_DURATION_CAST to OFF or by defining the macro as 0 before including fmt/chrono.h. However, this reverts to std::chrono::duration_cast and its associated UB risks for narrowing conversions.

Does FMT_SAFE_DURATION_CAST affect all chrono duration types?

The protection applies to all duration conversions performed internally by {fmt}’s chrono formatting facilities. It handles both integral representations (using lossless_integral_conversion) and floating-point representations (using safe_float_conversion), ensuring comprehensive coverage across std::chrono::duration specializations.

Is there a performance penalty for using FMT_SAFE_DURATION_CAST?

The safety checks add minor overhead for the bounds validation logic. For integral types, this involves checking conversion factors and multiplication results; for floating-point types, it checks range validity. These checks are typically negligible compared to the cost of formatting operations, though users with performance-critical paths can benchmark the disabled configuration if necessary.

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 →