News

Type-Safe Variadic printf in Rust and C++

Type-Safe Variadic printf in Rust and C++

February 15, 2025
Rust C++ type-safe printf compile-time variadic H-lists traits constexpr static assertions
This article explores how to implement a type-safe variadic printf function in Rust using H-lists and traits, and in C++ using constexpr and variadic templates, ensuring compile-time type checking.

Type-Safe Variadic printf Implementation

Video: Idris: Type safe printf - YouTube

Implementing a type-safe variadic printf function is a common challenge in programming, especially in languages like C++ and Rust. Below, I'll provide an overview of how this can be achieved in both languages.

Rust Implementation

In Rust, you can use heterogeneous lists (H-lists) and traits to ensure that the number of format string holes matches the number of arguments. This approach leverages Rust's strong type system and compile-time checks.

Core Mechanisms

  • Heterogeneous List (HList): A sequence of values of potentially different types.
  • Traits: Used to define and enforce the behavior of the format function.

Example Code


  pub struct FString(&'static str);
  pub struct FVar;

  let example = hlist![
    FString("Hello "),
    FVar,
    FString("! The first prime is "),
    FVar
  ];

  let args = hlist!["world", 2];

  assert_eq!(example.format(args), "Hello world! The first prime is 2");

  // Compile-time error
  // example.format(hlist!["just one arg"]);
  

Format Trait

The Format trait ensures that the format list and argument list match in length and type.


  trait Format {
    fn format(&self, args: ArgList) -> String;
  }

  impl Format for HNil {
    fn format(&self, _args: HNil) -> String {
      "".to_string()
    }
  }

  impl Format for HCons
  where
    FmtList: Format,
  {
    fn format(&self, args: ArgList) -> String {
      self.head.0.to_owned() + &self.tail.format(args)
    }
  }

  impl Format> for HCons
  where
    FmtList: Format,
    T: ToString,
  {
    fn format(&self, args: HCons) -> String {
      args.head.to_string() + &self.tail.format(args.tail)
    }
  }
  

C++ Implementation

In C++11, you can use constexpr and variadic templates to implement a type-safe printf function. This approach ensures that the format string and arguments are checked at compile time.

Example Code


  #include "safe-printf.h"

  int main() {
    int x = 42;
    safe_printf("%d -> %s", x, "hi bob");

    // Compile-time error
    // safe_printf("%s -> %d", x, "hi bob");
    return 0;
  }
  

safe-printf.h

The safe-printf.h header file contains the implementation details. It uses constexpr functions and static assertions to ensure type safety.


  template 
  void safe_printf(const char* format, Args... args) {
    static_assert(impl::checkFormat(format, args...), "Format string and arguments do not match");
    // Actual printf implementation
  }

  namespace impl {
    template 
    constexpr bool checkFormat(const char* format, Args... args) {
      // Check format string and arguments
    }
  }
  

Key Features

  • constexpr: Compile-time evaluation of format string and arguments.
  • Static Assertions: Ensure that the format string and arguments match at compile time.

Conclusion

Both Rust and C++ provide powerful type systems that can be leveraged to implement type-safe variadic printf functions. By using H-lists and traits in Rust, and constexpr and variadic templates in C++, you can ensure that format strings and arguments are checked at compile time, reducing the risk of runtime errors.

Sources

tromey/typesafe-printf: type-safe printf in C++11 - GitHub This is my attempt at writing a type-safe printf for C++. It is currently minimal. It handles formats, like %s , and type-checks these.
Implementing a Type-safe printf in Rust - Will Crichton I show how to use heterogeneous lists and traits to implement a type-safe printf in Rust. These mechanisms can ensure that two variadic argument lists share ...
Moving printf into the modern age using C++17 | Evan Teran's Blog Use variadic templates to verify the sanity of the parameters; Delegate the actual formatting to the libc printf. I think we can do better…