std::source_location (C++20)
Capture file, line, column, and function name at the call site without macros — the C++20 replacement for __FILE__ and __LINE__.
std::source_locationsince C++20A lightweight, copyable value type that holds a compile-time snapshot of a source location — file name, line number, column number, and enclosing function name — produced by the consteval static member current(), which captures the position where it is evaluated.
Overview
std::source_location (C++20, <source_location>) is the type-safe successor to the __FILE__, __LINE__, and __FUNCTION__ preprocessor macros. Its core mechanic turns on a single language rule: default arguments are evaluated at the call site, not at the function definition. Because current() is consteval, placing it as a default parameter causes the compiler to inject a compile-time constant representing where the calling code lives, not where the function is defined. This removes the need to wrap every logging or assertion call in a macro just to capture position information.
The type is cheap — typically four fields (a pointer, two 32-bit integers, another pointer) — and safe to store, copy, and pass by value.
Syntax
#include <source_location> // C++20
// Static factory — consteval, evaluated where it appears in source
std::source_location loc = std::source_location::current();
// Accessors
const char* loc.file_name(); // full path as seen by the compiler
uint_least32_t loc.line(); // 1-based line number
uint_least32_t loc.column(); // 1-based column (implementation-defined reliability)
const char* loc.function_name(); // enclosing function, format is implementation-definedAll four accessors are noexcept. current() is implicitly consteval — the returned object is a compile-time constant embedded in the binary, not a runtime query.
Examples
Core pattern: defaulted parameter captures the call site
#include <source_location>
#include <string_view>
#include <print> // C++23 — use std::cout or fmt::print for C++20-only targets
// The default argument is evaluated AT each call site, not here in the definition
void log(std::string_view msg,
std::source_location loc = std::source_location::current()) {
std::println("[{}:{}] {}", loc.file_name(), loc.line(), msg);
}
void process() {
log("starting"); // emits [src/process.cpp:14] starting
log("finishing"); // emits [src/process.cpp:15] finishing
// Neither line points into this log() definition
}Structured logger
#include <source_location>
#include <string_view>
#include <format> // C++20
#include <print> // C++23
enum class LogLevel { Trace, Debug, Info, Warn, Error, Fatal };
struct Logger {
static void write(
LogLevel level,
std::string_view msg,
std::source_location loc = std::source_location::current())
{
// Strip build-system prefix; file_name() is often an absolute path
std::string_view file = loc.file_name();
if (auto sep = file.rfind('/'); sep != std::string_view::npos)
file = file.substr(sep + 1);
std::println("[{}] {}:{} | {}: {}",
to_string(level), file, loc.line(),
loc.function_name(), msg);
}
static constexpr std::string_view to_string(LogLevel l) noexcept {
using enum LogLevel;
switch (l) {
case Trace: return "TRACE"; case Debug: return "DEBUG";
case Info: return "INFO "; case Warn: return "WARN ";
case Error: return "ERROR"; case Fatal: return "FATAL";
}
return "?????";
}
};
void connect(std::string_view host) {
Logger::write(LogLevel::Info, std::format("connecting to {}", host));
// [INFO ] net.cpp:40 | void connect(std::string_view): connecting to db.local
}Storing locations in exception types
source_location is trivially copyable and safe to embed in exceptions for deferred diagnostics:
#include <source_location>
#include <stdexcept>
#include <format> // C++20
class LocatedError : public std::runtime_error {
std::source_location where_;
public:
explicit LocatedError(
std::string msg,
std::source_location loc = std::source_location::current())
: std::runtime_error(
std::format("{} [{}:{}]", msg, loc.file_name(), loc.line()))
, where_(loc)
{}
const std::source_location& where() const noexcept { return where_; }
};
void validate(int x) {
if (x < 0)
throw LocatedError(std::format("negative value: {}", x));
// The exception message embeds validate()'s call site automatically
}Assertion with minimal macro surface
source_location eliminates the macro for position but cannot stringify expressions (#expr). A thin wrapper reduces macro usage to the bare minimum:
#include <source_location>
#include <string_view>
#include <cstdlib>
#include <print> // C++23
[[noreturn]] void assertion_failed(
std::string_view expr,
std::source_location loc) noexcept
{
std::println(stderr, "Assertion failed: `{}` at {}:{} in {}",
expr, loc.file_name(), loc.line(), loc.function_name());
std::abort();
}
inline void assert_impl(
bool cond, std::string_view expr,
std::source_location loc = std::source_location::current()) noexcept
{
if (!cond) [[unlikely]]
assertion_failed(expr, loc); // [[unlikely]] since C++20
}
// Only macro needed: stringification of the expression
#define ASSERT(expr) assert_impl(static_cast<bool>(expr), #expr)
ASSERT(ptr != nullptr); // location is the ASSERT() call site
ASSERT(index < size());Templates
Default argument evaluation is per-call-site, so templates work without any special handling:
#include <source_location>
#include <concepts> // C++20
#include <typeinfo>
template <std::derived_from<Base> Derived> // C++20 concept constraint
Derived* checked_downcast(
Base* ptr,
std::source_location loc = std::source_location::current())
{
auto* result = dynamic_cast<Derived*>(ptr);
if (!result) {
std::println(stderr, "checked_downcast failed at {}:{} — {} is not {}",
loc.file_name(), loc.line(),
typeid(*ptr).name(), typeid(Derived).name());
throw std::bad_cast{};
}
return result;
}Best Practices
Always forward, never re-capture in wrappers. This is the most important rule. Every intermediate layer that calls current() instead of forwarding an existing source_location captures the wrapper's location, not the original caller's:
// WRONG — captures info()'s own location
void info(std::string_view msg) {
log(LogLevel::Info, msg);
}
// RIGHT — accepts and forwards the caller's location
void info(std::string_view msg,
std::source_location loc = std::source_location::current()) {
log(LogLevel::Info, msg, loc); // pass through, do not re-capture
}Trim file_name() before logging. The path returned is the literal string the compiler received — often an absolute path like /home/ci/build/src/server.cpp. Always strip to the basename for human-readable logs.
Treat column() as advisory only. The C++20 standard permits column() to return zero or any other implementation-defined value. Clang often returns 0 in optimised builds; GCC and MSVC differ. Log it if it has value in your toolchain, but never branch on it.
function_name() format is not portable. GCC and Clang return full signatures (void Server::connect(std::string_view)); MSVC returns a different decorated form. Do not parse or pattern-match function names in portable code.
Common Pitfalls
Lambda capture semantics. current() inside a lambda body captures the lambda definition site. function_name() returns an implementation-defined label (e.g., main::<lambda(int)>). If you need the invocation site, pass the location explicitly at the call site:
// loc.line() is the lambda definition line — may not be what you want
auto fn = [](std::source_location loc = std::source_location::current()) {
Logger::write(LogLevel::Debug, "in lambda", loc);
};
fn(); // loc points to the lambda [] line, not this invocation line
// To capture the invocation site:
fn(std::source_location::current());source_location is a single frame, not a stack trace. It captures where current() was evaluated — one location. For multi-frame runtime backtraces, use std::stacktrace (C++23, <stacktrace>). The two are complementary, not interchangeable.
No pre-C++20 polyfill with identical semantics. Libraries such as {fmt} and Abseil provide macro-based shims, but they cannot replicate the defaulted-parameter trick without a macro at every call site. If your codebase supports C++17 and earlier, plan for the macro shim until you can raise the standard.
consteval is not the same as constexpr. current() is consteval, meaning it must be evaluated at compile time. You cannot call it in a context that is genuinely runtime-evaluated (e.g., through a function pointer cast to a non-consteval type). In practice this constraint is satisfied automatically by the defaulted-parameter pattern, but it can bite in unusual metaprogramming contexts.
Building a Structured Logger with source_location
The defaulted-parameter pattern turns source_location into zero-overhead call-site metadata. Every call to a Logger method automatically carries the file, line, and function name of the call site — no macros, no manual __FILE__ arguments.
// C++20
#include <source_location>
#include <string_view>
#include <format>
#include <iostream>
class Logger {
public:
void info(std::string_view msg,
std::source_location loc = std::source_location::current()) const
{
std::string_view file = loc.file_name();
if (auto pos = file.rfind('/'); pos != std::string_view::npos)
file = file.substr(pos + 1);
std::cout << std::format("[INFO] {}:{} in {}: {}\n",
file, loc.line(), loc.function_name(), msg);
}
void warn(std::string_view msg,
std::source_location loc = std::source_location::current()) const
{
std::string_view file = loc.file_name();
if (auto pos = file.rfind('/'); pos != std::string_view::npos)
file = file.substr(pos + 1);
std::cout << std::format("[WARN] {}:{} in {}: {}\n",
file, loc.line(), loc.function_name(), msg);
}
};
// ---- call site ----
Logger logger;
void handle_request() {
logger.info("Connection accepted");
// prints: [INFO] server.cpp:42 in handle_request: Connection accepted
logger.warn("Timeout threshold approaching");
// prints: [WARN] server.cpp:43 in handle_request: Timeout threshold approaching
}The macro equivalent required wrapping every call: LOG_INFO("msg") expanding to log(LogLevel::Info, "msg", __FILE__, __LINE__, __FUNCTION__). With source_location, the call site is identical to a plain function call — logger.info("Connection accepted") — while the location is still precise.
source_location in Assertion Macros
source_location eliminates the macro for position capture; only expression stringification still requires a macro. The hybrid approach below uses exactly one macro (ASSERT) and shrinks its body to a single #expr token paste.
// C++20
#include <source_location>
#include <string_view>
#include <cstdlib>
#include <format>
#include <iostream>
[[noreturn]] inline void assertion_failure(
std::string_view expr,
std::source_location loc) noexcept
{
std::cerr << std::format(
"Assertion failed: `{}` at {}:{} in {}\n",
expr, loc.file_name(), loc.line(), loc.function_name());
std::abort();
}
// Takes the pre-stringified expression plus a defaulted location.
// The default is evaluated at the call site of assert_impl(), which is
// the ASSERT() macro expansion — exactly the right location.
inline void assert_impl(
bool cond,
std::string_view expr,
std::source_location loc = std::source_location::current()) noexcept
{
if (!cond) [[unlikely]]
assertion_failure(expr, loc);
}
// The only macro — needed solely for `#expr` stringification
#define ASSERT(expr) assert_impl(static_cast<bool>(expr), #expr)
// ---- usage ----
void process(int* ptr, std::size_t index, std::size_t size) {
ASSERT(ptr != nullptr); // location: this exact line
ASSERT(index < size); // location: this exact line
}expr still needs a macro for stringification because the language provides no runtime mechanism to turn ptr != nullptr into the string "ptr != nullptr". Everything else — file, line, column, function — is handled by source_location without any macro involvement.
Combining source_location with std::stacktrace (C++23)
source_location gives you one frame; std::stacktrace gives you the full call chain. They are complementary: embed both in an exception base class to produce rich diagnostics that show not just where the exception was thrown but how execution arrived there.
// C++23
#include <source_location>
#include <stacktrace>
#include <stdexcept>
#include <format>
#include <string>
class LoggedException : public std::runtime_error {
std::source_location throw_site_;
std::stacktrace backtrace_;
public:
explicit LoggedException(
std::string msg,
std::source_location loc = std::source_location::current(),
std::stacktrace trace = std::stacktrace::current())
: std::runtime_error(std::move(msg))
, throw_site_(loc)
, trace_(std::move(trace))
{}
const std::source_location& throw_site() const noexcept { return throw_site_; }
const std::stacktrace& backtrace() const noexcept { return backtrace_; }
std::string full_report() const {
return std::format(
"Exception: {}\nThrown at {}:{} in {}\nBacktrace:\n{}",
what(),
throw_site_.file_name(), throw_site_.line(),
throw_site_.function_name(),
std::to_string(backtrace_)); // C++23
}
private:
std::stacktrace trace_; // store under the right name
};
void deep_call() {
throw LoggedException("unexpected state");
// Captures both:
// throw_site_ → deep_call(), this line
// backtrace_ → full chain: main → process → deep_call
}std::stacktrace::current() as a default argument is evaluated at the call site, mirroring the source_location pattern. Both parameters may appear in the same signature without conflict. Note that stacktrace collection has non-trivial runtime overhead; prefer source_location-only exceptions on hot paths.
Forwarding source_location Through Call Chains
The most common mistake when layering helpers is letting an intermediate function call current() instead of forwarding the loc it already received. Each re-capture overwrites the location with the wrapper's own position.
// C++20
#include <source_location>
#include <string_view>
#include <iostream>
#include <format>
void raw_log(std::string_view level, std::string_view msg,
std::source_location loc)
{
std::cout << std::format("[{}] {}:{} {}\n",
level, loc.file_name(), loc.line(), msg);
}
// WRONG — captures info()'s own definition site, not the caller's site
void info_wrong(std::string_view msg) {
raw_log("INFO", msg, std::source_location::current()); // always points here
}
// RIGHT — accepts a defaulted loc (captured at the call site) and forwards it
void info(std::string_view msg,
std::source_location loc = std::source_location::current()) {
raw_log("INFO", msg, loc); // forward the already-captured location
}
// Generic trace wrapper — same pattern for arbitrary callables
template <typename F>
void trace_call(F&& f,
std::source_location loc = std::source_location::current())
{
std::cout << std::format("entering {}:{}\n", loc.file_name(), loc.line());
std::forward<F>(f)();
std::cout << std::format("leaving {}:{}\n", loc.file_name(), loc.line());
}
// ---- call site ----
void request_handler() {
info("processing request"); // location: this line ✓
info_wrong("processing request"); // location: inside info_wrong() ✗
trace_call([] { /* work */ }); // loc is the trace_call() invocation line
}The rule generalises: every function that accepts a source_location parameter must take it as the last defaulted argument and pass it explicitly to any downstream call. Never call current() inside a function that received a location from its caller.
source_location in Concepts and Constraints
source_location::current() is consteval, so it is usable in constant-evaluated contexts, including consteval functions and static_assert messages. This enables compile-time diagnostics that embed precise source position without macros.
// C++20
#include <source_location>
#include <concepts>
#include <type_traits>
// A consteval helper that builds a descriptive compile-time message.
// consteval guarantees the entire call is evaluated at compile time.
consteval auto make_type_error_msg(
std::string_view requirement,
std::source_location loc = std::source_location::current())
{
// In a real codebase, return a constexpr-friendly string type
// (e.g., a fixed_string or simply use static_assert with a literal).
// Shown here for illustration — loc is a compile-time constant.
return loc; // carry location info forward
}
// Constrained function with a targeted static diagnostic
template <typename T>
requires std::integral<T>
constexpr T safe_shift(T value, int amount) {
static_assert(sizeof(T) >= 2,
"safe_shift: T must be at least 16 bits to avoid undefined behaviour");
return value << amount;
}
// Using source_location in a consteval diagnostic builder
template <typename T>
consteval void require_trivial(
std::source_location loc = std::source_location::current())
{
// static_assert inside consteval fires at the call site
static_assert(std::is_trivially_copyable_v<T>,
"Type passed to require_trivial must be trivially copyable; "
"check the call site reported by the compiler");
// loc is available here if you wish to embed it in a constexpr string
}
struct NonTrivial { NonTrivial(const NonTrivial&) {} };
// require_trivial<NonTrivial>();
// ^ compile error with message referencing the call site via locsource_location::current() in a consteval context is fully supported from C++20. The captured object is a compile-time constant — all four accessors (file_name(), line(), column(), function_name()) return values that the compiler can fold into diagnostic messages or embed in binary string tables at zero runtime cost.
Comparison: source_location vs FILE/LINE vs std::stacktrace
Choose the tool that matches your diagnostic depth requirement. The three mechanisms are complementary rather than competing, and can be combined in a single interface.
| Feature | std::source_location | __FILE__ / __LINE__ | std::stacktrace |
|---|---|---|---|
| Call-site capture | Defaulted parameter (no macro) | Macro required at every call site | Defaulted parameter or explicit |
| Multi-level forwarding | Pass loc explicitly through chain | Re-expand macro at each layer | Pass stacktrace object through chain |
| Runtime overhead | Zero (compile-time constant) | Zero (string/integer literal) | Non-trivial (walks call stack) |
| Full backtrace | No — single frame only | No — single frame only | Yes — all frames |
consteval usable | Yes (C++20) | Yes (preprocessor) | No — runtime only |
| Column number | Yes (advisory) | No | No |
| Expression stringification | No | Yes (#expr) | No |
| Standard since | C++20 | C (pre-standard) | C++23 |
For new C++20 code, prefer source_location for all logging and diagnostic APIs. The call site is cleaner, forwarding is explicit and compiler-checked, and the interface is type-safe. Keep __FILE__/__LINE__ macros only where expression stringification is genuinely needed — typically the one-line wrapper macro for ASSERT-style checks. Reach for std::stacktrace when a single frame is insufficient — exceptions that need full backtraces, post-mortem crash reporters, or performance profilers. The combination of source_location (immediate throw site) and std::stacktrace (full chain) in an exception base class gives the best diagnostic signal at a bounded cost.
See Also
std::stacktrace— C++23 runtime multi-frame stack tracestd::format— C++20 type-safe text formattingstd::println— C++23 formatted output with automatic newline- Predefined macros (
__FILE__,__LINE__,__func__) — the C++98 predecessors [[nodiscard]]— C++17 attribute; combine with diagnostic functions to prevent accidental discard