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_internaldirectly, 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 astd::uint_least32_t.column(): Returns the column number as astd::uint_least32_t(if supported by the compiler implementation).file_name(): Returns aconst char*pointing to the path of the source file.function_name(): Returns aconst char*describing the current function signature.
Why
std::source_location Improves C++ Logging
- True Type Safety: Unlike raw strings and integer macros, source data is encapsulated in a concrete, standardized type.
- First-Class Integration: Because it is an ordinary
object,
std::source_locationcan be stored in structures, forwarded through standard functions, or held within custom error classes (such as exceptions orstd::expectedunexpected types). constexprSupport:std::source_location::current()and all of its member functions areconstexpr, enabling compile-time diagnostic checks, static assertions, and zero-overhead source tracking where applicable.- Elimination of Boilerplate Wrapper Macros: APIs can
expose real functions, overloaded signatures, and member functions
without wrapping each variant in a corresponding
#definemacro.