Replacing C++ Logging Macros with std::source_location

Introduced in C++20, std::source_location provides a standard, type-safe mechanism to capture call-site metadata—such as file names, line numbers, and function names—without relying on the C preprocessor. Historically, diagnostic logging in C++ required macros like __FILE__ and __LINE__ to track where an event occurred. This article explains how std::source_location works, demonstrates how it replaces legacy logging macros, and highlights the architectural benefits it brings to modern C++ codebases.

The Problem with Preprocessor Logging Macros

For decades, developers relied on preprocessor macros to capture call-site diagnostics. A typical C++ logging implementation often looked like this:

#define LOG_INFO(msg) log_internal(msg, __FILE__, __LINE__, __func__)

void log_internal(std::string_view message, const char* file, int line, const char* function) {
    std::cout << "[" << file << ":" << line << " in " << function << "] " << message << '\n';
}

While effective, this approach introduces several well-known issues:

  • Preprocessor Pollution: Macros do not respect C++ namespaces or scope, leading to potential name collisions across headers.
  • Obfuscated APIs: Developers must invoke the macro rather than calling a regular function directly. If a developer accidentally calls log_internal directly, the captured location is the logger itself, not the caller.
  • Tooling and Debugging Hurdles: Macros are difficult for IDEs to refactor, expand unpredictably in compiler diagnostics, and cannot be passed as first-class function arguments or templates.

The Modern Alternative: std::source_location

Defined in the <source_location> header, std::source_location encapsulates call-site metadata inside a standardized C++ class. Its most critical feature is the static member function std::source_location::current().

When std::source_location::current() is evaluated as a default function parameter, C++ specifies that it must capture the source context of the caller rather than the callee's definition.

Basic Usage

#include <iostream>
#include <string_view>
#include <source_location>

void log(std::string_view message, 
         const std::source_location location = std::source_location::current()) 
{
    std::cout << "[" << location.file_name() << ":" 
              << location.line() << " (" 
              << location.function_name() << ")] " 
              << message << '\n';
}

int main() {
    log("System initialized successfully."); // Line 14
    return 0;
}

Output:

[main.cpp:14 (int main())] System initialized successfully.

In this example, callers invoke log() as a plain, clean C++ function. Because location defaults to std::source_location::current(), the compiler injects main.cpp, line 14, and int main() automatically.

Key Methods Available

An instance of std::source_location exposes four primary inspector functions:

  • line(): Returns the line number as a std::uint_least32_t.
  • column(): Returns the column number as a std::uint_least32_t (if supported by the compiler implementation).
  • file_name(): Returns a const char* pointing to the path of the source file.
  • function_name(): Returns a const char* describing the current function signature.

Why std::source_location Improves C++ Logging

  1. True Type Safety: Unlike raw strings and integer macros, source data is encapsulated in a concrete, standardized type.
  2. First-Class Integration: Because it is an ordinary object, std::source_location can be stored in structures, forwarded through standard functions, or held within custom error classes (such as exceptions or std::expected unexpected types).
  3. constexpr Support: std::source_location::current() and all of its member functions are constexpr, enabling compile-time diagnostic checks, static assertions, and zero-overhead source tracking where applicable.
  4. Elimination of Boilerplate Wrapper Macros: APIs can expose real functions, overloaded signatures, and member functions without wrapping each variant in a corresponding #define macro.