{fmt} (Validation)
Library
{fmt} (Validation)
Overview
{fmt} is a modern C++ formatting library that became the foundation for C++20's std::format. While primarily specialized in string formatting, it provides powerful validation capabilities through type safety and compile-time checking features. It validates the correctness of format strings at compile time, significantly reducing runtime errors. With Python-like f-string syntax and high performance surpassing printf/iostream, it is positioned as the standard for string processing in modern C++ applications.
Details
The {fmt} library is the original implementation that became the prototype for std::format standardized in C++20, providing type-safe string formatting and validation capabilities. It checks the consistency between format strings and argument types at compile time, preventing type mismatches and buffer overflows. By defining custom formatters for user-defined types, you can incorporate custom validation logic, enabling safe stringification of complex data structures. It also delivers excellent runtime performance, achieving faster processing compared to traditional printf-family functions and iostreams.
Key Features
- Type Safety: Safe formatting with compile-time type checking
- High Performance: Execution speed surpassing printf/iostream
- Buffer Safety: Overflow prevention through automatic memory management
- Customizability: Support for user-defined type formatters
- Standards Compliance: Foundation implementation of C++20 std::format
- Wide Support: Broad compiler support from C++11 onwards
Pros and Cons
Pros
- High safety through compile-time type checking
- Prevention of security issues like buffer overflows
- Faster execution performance than printf/iostream
- Readable and intuitive Python-like syntax
- Rich formatting options
- Proven track record and reliability as foundation of C++20 standard
Cons
- Primarily specialized in formatting (not a general-purpose validation library)
- Does not provide complex schema validation features
- Learning curve exists (especially for creating custom formatters)
- Large library (feature-rich for simple use cases)
- Migration work required from traditional printf-style code
References
Code Examples
Installation and Basic Setup
# Install using vcpkg
vcpkg install fmt
# Install using Conan
conan install fmt/8.1.1@
# CMake find_package usage example
find_package(fmt REQUIRED)
target_link_libraries(your_target fmt::fmt)
# Without package manager
git clone https://github.com/fmtlib/fmt.git
cd fmt
mkdir build && cd build
cmake ..
make -j4
Basic Type-safe Formatting and Validation
#include <fmt/format.h>
#include <fmt/printf.h>
#include <fmt/chrono.h>
#include <iostream>
#include <string>
#include <vector>
#include <chrono>
int main() {
// Basic type-safe formatting
{
int number = 42;
std::string name = "Alice";
// Type-safe formatting (compile-time check)
std::string message = fmt::format("User {} has number {}", name, number);
fmt::print("{}\n", message);
// Positional formatting
std::string formatted = fmt::format("{1} years ago, {0} was born", name, 25);
fmt::print("{}\n", formatted);
}
// Numeric formatting and validation
{
double pi = 3.14159265359;
int value = 1234567;
// Precision control
fmt::print("Pi with 3 decimals: {:.3f}\n", pi);
fmt::print("Value with thousands separator: {:,}\n", value);
// Hexadecimal and binary formatting
int num = 255;
fmt::print("Decimal: {}, Hex: {:x}, Binary: {:b}\n", num, num, num);
}
// Date/time formatting validation
{
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
// Safe date/time formatting
fmt::print("Current time: {:%Y-%m-%d %H:%M:%S}\n", fmt::localtime(time_t));
fmt::print("ISO format: {:%FT%T}\n", fmt::localtime(time_t));
}
// Container formatting
{
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
// Direct container formatting
fmt::print("Numbers: {}\n", fmt::join(numbers, ", "));
fmt::print("Names: [{}]\n", fmt::join(names, ", "));
}
return 0;
}
Custom Type Validation and Formatting
#include <fmt/format.h>
#include <fmt/ostream.h>
#include <string>
#include <stdexcept>
#include <regex>
// Custom data structure
struct EmailAddress {
std::string address;
EmailAddress(const std::string& addr) : address(addr) {
validateEmail(addr);
}
private:
void validateEmail(const std::string& email) {
static const std::regex email_pattern(
R"(^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)"
);
if (!std::regex_match(email, email_pattern)) {
throw std::invalid_argument("Invalid email address format: " + email);
}
}
};
struct PhoneNumber {
std::string country_code;
std::string number;
PhoneNumber(const std::string& cc, const std::string& num)
: country_code(cc), number(num) {
validatePhone(cc, num);
}
private:
void validatePhone(const std::string& cc, const std::string& num) {
if (cc.empty() || cc[0] != '+') {
throw std::invalid_argument("Country code must start with '+'");
}
if (num.length() < 7 || num.length() > 15) {
throw std::invalid_argument("Phone number must be 7-15 digits");
}
for (char c : num) {
if (!std::isdigit(c)) {
throw std::invalid_argument("Phone number must contain only digits");
}
}
}
};
// Custom formatter for EmailAddress
template <>
struct fmt::formatter<EmailAddress> {
// Parse format specification
constexpr auto parse(format_parse_context& ctx) {
return ctx.begin();
}
// Format the EmailAddress
template <typename FormatContext>
auto format(const EmailAddress& email, FormatContext& ctx) {
return format_to(ctx.out(), "<{}>", email.address);
}
};
// Custom formatter for PhoneNumber with validation
template <>
struct fmt::formatter<PhoneNumber> {
char presentation = 'i'; // 'i' for international, 'n' for national
constexpr auto parse(format_parse_context& ctx) {
auto it = ctx.begin(), end = ctx.end();
if (it != end && (*it == 'i' || *it == 'n')) {
presentation = *it++;
}
if (it != end && *it != '}') {
throw format_error("Invalid format specification for PhoneNumber");
}
return it;
}
template <typename FormatContext>
auto format(const PhoneNumber& phone, FormatContext& ctx) {
if (presentation == 'i') {
// International format: +1 234-567-8900
return format_to(ctx.out(), "{} {}-{}-{}",
phone.country_code,
phone.number.substr(0, 3),
phone.number.substr(3, 3),
phone.number.substr(6));
} else {
// National format: (234) 567-8900
return format_to(ctx.out(), "({}) {}-{}",
phone.number.substr(0, 3),
phone.number.substr(3, 3),
phone.number.substr(6));
}
}
};
// User information with validation
struct User {
std::string name;
EmailAddress email;
PhoneNumber phone;
int age;
User(const std::string& n, const std::string& e,
const std::string& cc, const std::string& p, int a)
: name(n), email(e), phone(cc, p), age(a) {
if (name.empty()) {
throw std::invalid_argument("Name cannot be empty");
}
if (age < 0 || age > 150) {
throw std::invalid_argument("Age must be between 0 and 150");
}
}
};
// Custom formatter for User
template <>
struct fmt::formatter<User> {
constexpr auto parse(format_parse_context& ctx) {
return ctx.begin();
}
template <typename FormatContext>
auto format(const User& user, FormatContext& ctx) {
return format_to(ctx.out(),
"User: {} (age: {})\n Email: {}\n Phone: {:i}",
user.name, user.age, user.email, user.phone);
}
};
int main() {
try {
// Valid user creation and formatting
User user("John Doe", "[email protected]", "+1", "2345678900", 30);
fmt::print("{}\n\n", user);
// Demonstrate different phone formats
PhoneNumber phone("+1", "2345678900");
fmt::print("International format: {:i}\n", phone);
fmt::print("National format: {:n}\n", phone);
// Email formatting
EmailAddress email("[email protected]");
fmt::print("Email: {}\n", email);
} catch (const std::exception& e) {
fmt::print("Validation error: {}\n", e.what());
}
// Demonstrate validation failures
try {
User invalid_user("", "invalid-email", "+1", "123", -5);
} catch (const std::exception& e) {
fmt::print("Invalid user creation failed: {}\n", e.what());
}
return 0;
}
Compile-time Format String Validation
#include <fmt/format.h>
#include <fmt/compile.h>
#include <string>
#include <vector>
// Compile-time format string validation
int main() {
// Compile-time validated format strings
{
constexpr auto format_str = fmt::compile<int, std::string>("ID: {}, Name: {}");
std::string result = fmt::format(format_str, 123, "Alice");
fmt::print("{}\n", result);
}
// Performance-optimized compiled formats
{
// Pre-compiled format for repeated use
static constexpr auto log_format = fmt::compile<std::string, int, double>(
"[{}] Value: {}, Score: {:.2f}"
);
// High-performance formatting
for (int i = 0; i < 5; ++i) {
std::string log_entry = fmt::format(log_format, "INFO", i * 10, i * 1.5);
fmt::print("{}\n", log_entry);
}
}
// Format string validation with type checking
{
struct LogEntry {
std::string level;
std::string message;
int line_number;
};
auto entries = std::vector<LogEntry>{
{"INFO", "Application started", 100},
{"WARN", "Low memory warning", 250},
{"ERROR", "Connection failed", 300}
};
// Safe formatting with compile-time validation
constexpr auto entry_format = fmt::compile<std::string, int, std::string>(
"[{}:{:04d}] {}"
);
for (const auto& entry : entries) {
std::string formatted = fmt::format(entry_format,
entry.level,
entry.line_number,
entry.message);
fmt::print("{}\n", formatted);
}
}
// Conditional formatting with validation
{
auto format_percentage = [](double value, bool show_sign = false) {
if (value < 0.0 || value > 1.0) {
throw std::out_of_range("Percentage value must be between 0.0 and 1.0");
}
if (show_sign) {
return fmt::format("{:+.1%}", value);
} else {
return fmt::format("{:.1%}", value);
}
};
try {
fmt::print("Success rate: {}\n", format_percentage(0.95));
fmt::print("Change: {}\n", format_percentage(-0.05, true));
// This will throw an exception
format_percentage(1.5);
} catch (const std::exception& e) {
fmt::print("Formatting error: {}\n", e.what());
}
}
return 0;
}
Advanced Validation Patterns
#include <fmt/format.h>
#include <fmt/ranges.h>
#include <string>
#include <vector>
#include <map>
#include <optional>
#include <variant>
// Validation helper functions
namespace validation {
template<typename T>
std::string safe_format(const std::string& format_str, T&& value) {
try {
return fmt::format(format_str, std::forward<T>(value));
} catch (const std::exception& e) {
return fmt::format("FORMAT_ERROR: {}", e.what());
}
}
template<typename Container>
std::string format_container_safe(const Container& container,
const std::string& separator = ", ",
size_t max_elements = 10) {
if (container.empty()) {
return "[]";
}
if (container.size() <= max_elements) {
return fmt::format("[{}]", fmt::join(container, separator));
} else {
auto begin = container.begin();
auto end = begin;
std::advance(end, max_elements);
std::vector<typename Container::value_type> preview(begin, end);
return fmt::format("[{}... ({} more)]",
fmt::join(preview, separator),
container.size() - max_elements);
}
}
// Safe formatting for optional values
template<typename T>
std::string format_optional(const std::optional<T>& opt,
const std::string& format_str = "{}",
const std::string& null_value = "null") {
if (opt.has_value()) {
return safe_format(format_str, opt.value());
} else {
return null_value;
}
}
}
// Configuration validation and formatting
struct DatabaseConfig {
std::string host;
int port;
std::string database;
std::optional<std::string> username;
std::optional<std::string> password;
std::vector<std::string> options;
std::string validate_and_format() const {
std::vector<std::string> errors;
if (host.empty()) {
errors.push_back("Host cannot be empty");
}
if (port <= 0 || port > 65535) {
errors.push_back(fmt::format("Invalid port: {} (must be 1-65535)", port));
}
if (database.empty()) {
errors.push_back("Database name cannot be empty");
}
if (!errors.empty()) {
return fmt::format("INVALID CONFIG:\n - {}", fmt::join(errors, "\n - "));
}
return fmt::format(
"Database Config:\n"
" Host: {}\n"
" Port: {}\n"
" Database: {}\n"
" Username: {}\n"
" Options: {}",
host, port, database,
validation::format_optional(username, "{}", "<not set>"),
validation::format_container_safe(options)
);
}
};
// Variant formatting with type validation
using ConfigValue = std::variant<std::string, int, double, bool>;
struct ConfigEntry {
std::string key;
ConfigValue value;
std::string format_entry() const {
return std::visit([&](const auto& v) -> std::string {
using T = std::decay_t<decltype(v)>;
if constexpr (std::is_same_v<T, std::string>) {
return fmt::format("{}: \"{}\"", key, v);
} else if constexpr (std::is_same_v<T, bool>) {
return fmt::format("{}: {}", key, v ? "true" : "false");
} else {
return fmt::format("{}: {}", key, v);
}
}, value);
}
};
int main() {
// Database configuration validation
{
DatabaseConfig valid_config{
"localhost",
5432,
"myapp",
"admin",
std::nullopt,
{"ssl=true", "connect_timeout=30"}
};
fmt::print("{}\n\n", valid_config.validate_and_format());
DatabaseConfig invalid_config{
"", // Invalid: empty host
-1, // Invalid: negative port
"", // Invalid: empty database
std::nullopt,
std::nullopt,
{}
};
fmt::print("{}\n\n", invalid_config.validate_and_format());
}
// Configuration entries with variant types
{
std::vector<ConfigEntry> config_entries = {
{"server_name", std::string("web-server-01")},
{"port", 8080},
{"timeout", 30.5},
{"debug_mode", true},
{"max_connections", 1000}
};
fmt::print("Configuration entries:\n");
for (const auto& entry : config_entries) {
fmt::print(" {}\n", entry.format_entry());
}
}
// Safe container formatting
{
std::vector<int> small_list = {1, 2, 3};
std::vector<int> large_list(50, 42); // 50 elements of value 42
fmt::print("Small list: {}\n", validation::format_container_safe(small_list));
fmt::print("Large list: {}\n", validation::format_container_safe(large_list));
std::map<std::string, int> scores = {
{"Alice", 95}, {"Bob", 87}, {"Charlie", 92}
};
std::vector<std::string> score_entries;
for (const auto& [name, score] : scores) {
score_entries.push_back(fmt::format("{}={}", name, score));
}
fmt::print("Scores: {}\n", validation::format_container_safe(score_entries));
}
return 0;
}