# How to Use fmtlib for Logging in C++: A Complete Guide

> Learn to use fmtlib for high-performance C++ logging with fmt::print, fmt::println, and fmt::format. Get type-safe, zero-allocation formatting.

- Repository: [Hello World Foundation/fmt](https://github.com/fmtlib/fmt)
- Tags: how-to-guide
- Published: 2026-09-06

---

**`fmtlib` provides type-safe, high-performance logging through `fmt::print`, `fmt::println`, and `fmt::format` with compile-time format string checking and zero-allocation formatting.**

The [`fmtlib/fmt`](https://github.com/fmtlib/fmt) library (commonly called `fmt`) replaces C's `printf`/`fprintf` and C++ streams with a modern, thread-safe formatting engine. This guide covers the core APIs, implementation details, and practical patterns for building robust logging systems using fmtlib.

## Core Logging APIs

### fmt::print and fmt::println

The simplest way to write log messages is through **`fmt::print`** and **`fmt::println`**. These functions reside in [[`include/fmt/ostream.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/ostream.h)](https://github.com/fmtlib/fmt/blob/main/include/fmt/ostream.h) and [[`include/fmt/os.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/os.h)](https://github.com/fmtlib/fmt/blob/main/include/fmt/os.h).

```cpp
#include <fmt/core.h>
#include <fmt/ostream.h>

int main() {
    // Basic stdout logging
    fmt::print("Server started on port {}\n", 8080);
    
    // Automatic newline - cleaner syntax
    fmt::println("User {} logged in", "alice");
    
    // Logging to stderr for errors
    fmt::print(stderr, "Error: {} (code {})\n", "connection lost", -1);
}

```

**`fmt::println`** appends a newline automatically, eliminating common bugs from missing `\n` characters. The `stderr` overload ensures error messages separate from standard output.

### fmt::format for Buffered Logging

When you need to construct a message before sending it to a destination (file, network, or syslog), use **`fmt::format`** from [[`include/fmt/format.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h)](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h):

```cpp
#include <fmt/format.h>
#include <fstream>

void log_to_file(const std::string& level, const std::string& message) {
    std::string formatted = fmt::format("[{}] {}", level, message);
    
    std::ofstream logfile("app.log", std::ios::app);
    logfile << formatted << std::endl;
}

// Usage
log_to_file("INFO", "All systems nominal");
log_to_file("WARN", "High memory usage detected");

```

This pattern decouples message formatting from I/O operations, enabling async logging or multiple output destinations.

## Printf-Style Compatibility

For codebases migrating from C-style logging, **`fmt::printf`** and **`fmt::fprintf`** in [[`include/fmt/printf.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/printf.h)](https://github.com/fmtlib/fmt/blob/main/include/fmt/printf.h) provide familiar syntax with type safety:

```cpp
#include <fmt/printf.h>

int main() {
    // Familiar % syntax, but compile-time checked
    fmt::printf("Processed %d items in %.2f seconds\n", 1024, 0.37);
    
    // Direct to file descriptor
    FILE* log = fopen("legacy.log", "a");
    fmt::fprintf(log, "Event at %s: %s\n", "14:32:01", "backup complete");
    fclose(log);
}

```

Unlike C `printf`, format string mismatches trigger **compile-time errors** rather than runtime crashes.

## Thread Safety in fmtlib Logging

Per the source in [[`include/fmt/os.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/os.h)](https://github.com/fmtlib/fmt/blob/main/include/fmt/os.h), **`fmt::print` variants lock a global mutex only when writing to streams that lack native thread safety**. This means:

- Log lines are never interleaved from multiple threads
- `stdout`/`stderr` operations acquire `fmt::detail::mutex` internally
- User-provided `FILE*` handles receive the same protection

```cpp
#include <fmt/core.h>
#include <thread>
#include <vector>

void worker(int id) {
    for (int i = 0; i < 100; ++i) {
        fmt::println("Thread {}: iteration {}", id, i);
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(worker, i);
    }
    for (auto& t : threads) t.join();
    // Output lines remain intact, never mixed
}

```

## Custom Type Formatting

You can log user-defined types by specializing **`fmt::formatter<T>`**. This extends fmtlib logging to domain-specific objects:

```cpp
#include <fmt/core.h>

struct Point {
    int x, y;
};

template<>
struct fmt::formatter<Point> {
    constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
    
    auto format(const Point& p, format_context& ctx) const {
        return fmt::format_to(ctx.out(), "({}, {})", p.x, p.y);
    }
};

int main() {
    Point pos{3, 4};
    fmt::println("Position: {}", pos);  // Output: Position: (3, 4)
}

```

Place formatter specializations in headers alongside your types for reusable logging.

## Performance Characteristics

The fmtlib formatting engine in [[`include/fmt/core.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/core.h)](https://github.com/fmtlib/fmt/blob/main/include/fmt/core.h) achieves zero-allocation formatting through:

1. **Single-pass parsing** – `fmt::vformat` and `fmt::detail::vformat_impl` parse the format string once
2. **Compact argument storage** – `fmt::format_args` packs arguments without heap allocation
3. **Direct output** – No intermediate `std::string` construction unless explicitly requested

This makes fmtlib suitable for high-throughput logging where `std::ostringstream` overhead is prohibitive.

## Building a Complete Logger

Combine these APIs into a reusable logging component:

```cpp
#include <fmt/core.h>
#include <fmt/ostream.h>
#include <fstream>
#include <mutex>
#include <string>

class Logger {
    std::ofstream file_;
    std::mutex mutex_;
    std::string name_;
    
public:
    explicit Logger(const std::string& filename, const std::string& name = "App")
        : file_(filename, std::ios::app), name_(name) {}
    
    template<typename... Args>
    void info(fmt::format_string<Args...> fmt, Args&&... args) {
        log("INFO", fmt, std::forward<Args>(args)...);
    }
    
    template<typename... Args>
    void error(fmt::format_string<Args...> fmt, Args&&... args) {
        log("ERROR", fmt, std::forward<Args>(args)...);
    }
    
private:
    template<typename... Args>
    void log(const std::string& level, fmt::format_string<Args...> fmt, Args&&... args) {
        std::lock_guard<std::mutex> lock(mutex_);
        auto msg = fmt::format(fmt, std::forward<Args>(args)...);
        fmt::println(file_, "[{}] [{}] {}", 
                     fmt::format("{:%Y-%m-%d %H:%M:%S}", 
                                std::chrono::system_clock::now()),
                     level, msg);
        file_.flush();
    }
};

// Usage
int main() {
    Logger logger("app.log", "Server");
    logger.info("Listening on port {}", 8080);
    logger.error("Failed to connect to {}", "database.example.com");
}

```

## Key Source Files in fmtlib

| File | Purpose |
|------|---------|
| [[`include/fmt/core.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/core.h)](https://github.com/fmtlib/fmt/blob/main/include/fmt/core.h) | Core engine: `vformat`, `format_args`, `format_parse_context` |
| [[`include/fmt/format.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h)](https://github.com/fmtlib/fmt/blob/main/include/fmt/format.h) | `fmt::format`, `fmt::format_to`, and formatting utilities |
| [[`include/fmt/ostream.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/ostream.h)](https://github.com/fmtlib/fmt/blob/main/include/fmt/ostream.h) | `fmt::print`, `fmt::println` for C++ streams |
| [[`include/fmt/os.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/os.h)](https://github.com/fmtlib/fmt/blob/main/include/fmt/os.h) | `FILE*` output and platform-specific I/O with mutex handling |
| [[`include/fmt/printf.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/printf.h)](https://github.com/fmtlib/fmt/blob/main/include/fmt/printf.h) | `fmt::printf`, `fmt::fprintf` compatibility layer |
| [`src/format.cc`](https://github.com/fmtlib/fmt/blob/main/src/format.cc) | Implementation of the core formatting engine |

## Summary

- **`fmt::print`** and **`fmt::println`** provide immediate stdout/stderr logging with automatic thread safety
- **`fmt::format`** enables buffered, multi-destination logging architectures
- **Compile-time checking** prevents format string vulnerabilities common in C-style logging
- **Zero-allocation paths** in `fmt::detail::vformat_impl` deliver superior performance for high-frequency logs
- **Custom formatters** integrate domain types directly into log statements

## Frequently Asked Questions

### Does fmtlib support asynchronous logging?

`fmtlib` itself provides synchronous APIs. For async logging, use `fmt::format` to build messages, then pass them to a thread pool or lock-free queue for background I/O. The zero-allocation formatting keeps producer threads unblocked.

### How do I add timestamps to fmtlib log messages?

Use `std::chrono` integration with format specifiers: `fmt::format("{:%Y-%m-%d %H:%M:%S}", std::chrono::system_clock::now())`. Wrap this in a logging function for automatic timestamp injection.

### Can I use fmtlib with existing logging frameworks like spdlog?

Yes—`spdlog` itself uses `fmtlib` as its formatting backend. You can also bridge fmtlib to other frameworks by using `fmt::format` to construct messages before passing them to framework APIs.

### Is fmtlib's print output atomic on all platforms?

Yes. According to the source in [`include/fmt/os.h`](https://github.com/fmtlib/fmt/blob/main/include/fmt/os.h), all stream output acquires `fmt::detail::mutex` when the destination lacks native thread safety. This guarantees line-level atomicity across platforms.