std::bit_cast and std::source_location (C++20)
Type-safe bit reinterpretation with std::bit_cast and compile-time call-site capture with std::source_location — replacing reinterpret_cast punning and preprocessor logging macros.
std::bit_cast / std::source_locationsince C++20std::bit_cast<To>(from) reinterprets the object representation of from as type To in a well-defined, constexpr-capable way; std::source_location::current() captures the calling file, line, column, and function name at compile time — together eliminating two of C++'s most persistent unsafe patterns.
Overview
Before C++20, type punning between unrelated types required either reinterpret_cast (strict aliasing UB) or memcpy (correct at runtime but not constexpr). std::bit_cast closes this gap: it produces the same result as memcpy into a zero-initialized To object, is guaranteed defined behavior, and participates fully in constant evaluation.
Similarly, diagnostic logging has long relied on __FILE__, __LINE__, and __func__ macros. These leak into call-site syntax, resist composition, and cannot be passed as typed values. std::source_location captures the same information as a first-class C++ object usable as a default function argument that resolves at the call site rather than the definition site.
Syntax
#include <bit> // std::bit_cast (C++20)
#include <source_location> // std::source_location (C++20)
// bit_cast: From is deduced; To must be specified explicitly
template<class To, class From>
constexpr To bit_cast(const From& from) noexcept;
// source_location::current() — consteval (must evaluate at compile time)
static consteval std::source_location current(/* impl-defined */) noexcept;
// Members
const char* file_name() const noexcept; // absolute or relative path
uint_least32_t line() const noexcept; // 1-based
uint_least32_t column() const noexcept; // implementation-defined
const char* function_name() const noexcept; // impl-defined formatbit_cast is noexcept unconditionally. source_location::current() is consteval (not merely constexpr), meaning the compiler must evaluate it at translation time; source coordinates are supplied via hidden compiler-intrinsic arguments.
Constraints on bit_cast are verified statically:
static_assert(sizeof(To) == sizeof(From));
static_assert(std::is_trivially_copyable_v<To>);
static_assert(std::is_trivially_copyable_v<From>);Violation produces a hard compile error, not UB — a significant advantage over reinterpret_cast.
Examples
IEEE 754 layout inspection
#include <bit>
#include <cstdint>
#include <print>
struct Float32Bits {
uint32_t mantissa : 23;
uint32_t exponent : 8;
uint32_t sign : 1;
};
static_assert(sizeof(Float32Bits) == sizeof(float));
static_assert(std::is_trivially_copyable_v<Float32Bits>);
void inspect(float f) {
auto [m, e, s] = std::bit_cast<Float32Bits>(f); // C++20
std::println("sign={} exp={} mantissa=0x{:06X}", s, e, m);
}
// inspect(1.0f) → sign=0 exp=127 mantissa=0x000000
// inspect(2.0f) → sign=0 exp=128 mantissa=0x000000
// inspect(-0.0f) → sign=1 exp=0 mantissa=0x000000Bit-field ordering is implementation-defined. This decomposition is architecture-specific — appropriate for low-level platform code and debugging tooling, not portable interchange.
constexpr evaluation
constexpr uint32_t one_bits = std::bit_cast<uint32_t>(1.0f); // C++20: compile-time
static_assert(one_bits == 0x3F800000u);
static_assert(std::bit_cast<float>(0x3F800000u) == 1.0f);
// Pre-C++20: memcpy — correct but not constexpr
uint32_t u;
std::memcpy(&u, &one_bits, 4); // runtime only
// reinterpret_cast — strict aliasing UB, never for type punning
// uint32_t bad = *reinterpret_cast<const uint32_t*>(&one_bits); // UBFast inverse square root (UB-free)
// Classic 0x5F3759DF trick — now well-defined
float q_rsqrt(float x) noexcept {
uint32_t i = std::bit_cast<uint32_t>(x); // C++20
i = 0x5F3759DFu - (i >> 1);
float y = std::bit_cast<float>(i); // C++20
return y * (1.5f - x * 0.5f * y * y);
}The original used *reinterpret_cast<long*>(&x) — undefined behavior under strict aliasing. bit_cast gives identical codegen with defined semantics.
Network packet deserialization
#include <array>
#include <cstddef>
#include <cstdint>
#include <bit>
struct PacketHeader {
uint16_t magic;
uint16_t version;
uint32_t payload_length;
};
static_assert(sizeof(PacketHeader) == 8);
static_assert(std::is_trivially_copyable_v<PacketHeader>);
PacketHeader parse(const std::array<std::byte, 8>& wire) {
auto h = std::bit_cast<PacketHeader>(wire); // C++20
// Correct for host-endian protocols; use std::byteswap (C++23) for network byte order
return h;
}Logger without macros
#include <source_location>
#include <string_view>
#include <print>
class Logger {
public:
void info(std::string_view msg,
std::source_location loc = std::source_location::current()) const {
emit("INFO", msg, loc);
}
void warn(std::string_view msg,
std::source_location loc = std::source_location::current()) const {
emit("WARN", msg, loc);
}
void error(std::string_view msg,
std::source_location loc = std::source_location::current()) const {
emit("ERROR", msg, loc);
}
private:
void emit(std::string_view level, std::string_view msg,
const std::source_location& loc) const {
std::println("[{}] {}:{} ({}): {}",
level, loc.file_name(), loc.line(),
loc.function_name(), msg);
}
};
Logger log;
log.info("server started"); // [INFO] main.cpp:42 (main): server started
log.error("connection refused");Typed assertion helper
void check(bool condition, std::string_view msg,
std::source_location loc = std::source_location::current()) {
if (!condition)
throw std::logic_error(
std::format("{}:{} [{}] assertion failed: {}",
loc.file_name(), loc.line(),
loc.function_name(), msg));
}
check(x > 0, "x must be positive"); // exception names this exact lineForwarding location through call chains
void inner_log(std::string_view msg, std::source_location loc) {
std::println("[{}:{}] {}", loc.file_name(), loc.line(), msg);
}
void log_with_prefix(std::string_view prefix, std::string_view msg,
std::source_location loc = std::source_location::current()) {
// Forward the original location — do NOT call ::current() here
inner_log(std::format("[{}] {}", prefix, msg), loc);
}
log_with_prefix("NET", "connected"); // prints log_with_prefix's call siteBest Practices
- Validate constraints explicitly. Add
static_assert(sizeof(To) == sizeof(From))andstatic_assert(std::is_trivially_copyable_v<To> && std::is_trivially_copyable_v<From>)near anybit_castthat is not obviously safe from surrounding context. This documents intent and produces better error messages. - Prefer
bit_castovermemcpywhen constexpr matters. They produce identical codegen at runtime;bit_castadditionally works in constant expressions and expresses intent more clearly. - Place
source_location::current()only as a default parameter. A call inside the function body always captures that function's location, never the caller's. The default-parameter position is the only place that reliably captures the call site. - Store
source_locationby value. It is small (~16 bytes: two 32-bit integers and two pointers) and trivially copyable. Pass and copy it freely. - Treat
column()as advisory. GCC provides reliable column numbers; MSVC support has been inconsistent across versions. Code must not branch on column values for correctness.
Common Pitfalls
Trivially copyable constraint fails on library types
struct Config {
std::string name; // non-trivial copy constructor
int value;
};
// std::bit_cast<OtherType>(Config{}) — compile error, constraint violation
// The compiler catches this; reinterpret_cast would silently produce UB.Any type with a user-defined copy/move constructor, destructor, or a non-trivially-copyable member fails the check. The constraint fires at compile time.
Padding bytes land with unspecified values
struct Padded {
uint8_t a; // 1 byte
// 3 bytes implicit padding
uint32_t b; // 4 bytes
};
static_assert(sizeof(Padded) == 8);
Padded p{1, 42};
uint64_t bits = std::bit_cast<uint64_t>(p); // C++20: defined, but...
// The 3 padding bytes in bits have unspecified values.
// Do not memcmp, hash, or serialize this result.Behavior is fully defined (no UB), but the padding bits in To are unspecified. Equality comparisons and hashing of such results are unreliable.
current() called in the wrong place
// WRONG: always captures a line inside log(), not the caller
void log(std::string_view msg) {
auto loc = std::source_location::current(); // points here, not the call site
std::println("{}:{} {}", loc.file_name(), loc.line(), msg);
}
// CORRECT
void log(std::string_view msg,
std::source_location loc = std::source_location::current()) {
std::println("{}:{} {}", loc.file_name(), loc.line(), msg);
}Variadic templates cannot trail source_location
Variadic packs must be the last function parameter, which conflicts with a trailing source_location default:
// DOES NOT COMPILE — non-trailing default after variadic pack
template<typename... Args>
void log(Args&&... args,
std::source_location loc = std::source_location::current());
// Workaround: capture location at construction of a wrapper type
template<typename... Args>
struct LogArgs {
std::source_location loc;
std::tuple<Args...> args;
explicit LogArgs(Args&&... a,
std::source_location l = std::source_location::current())
: loc(l), args(std::forward<Args>(a)...) {}
};
template<typename... Args>
void log(LogArgs<Args...> call) {
// use call.loc and call.args
}
log(LogArgs("user", 42, "connected")); // location captured at LogArgs constructionThis "deducing constructor" pattern is the standard idiom for combining variadic arguments with source_location.
std::bit_cast for type punning
std::bit_cast is the sanctioned replacement for all type-punning patterns in C++20 and later. It produces exactly the same bit pattern that memcpy would copy into a zero-initialized destination object, but participates in constant evaluation and communicates intent clearly at the call site.
// C++20 — all three express the same intent; only bit_cast is constexpr-safe
#include <bit>
#include <cstdint>
#include <cstring>
float val = 3.14f;
// memcpy — correct at runtime, not constexpr
uint32_t a;
std::memcpy(&a, &val, sizeof(val));
// reinterpret_cast — strict aliasing UB, never correct for punning
// uint32_t b = *reinterpret_cast<const uint32_t*>(&val); // UB
// bit_cast — defined, constexpr, expressive
constexpr uint32_t c = std::bit_cast<uint32_t>(3.14f); // C++20At runtime all three emit the same load instruction on x86-64. Only bit_cast works inside constexpr functions and static_assert expressions.
std::bit_cast constraints and triviality
Both the source and destination types must be trivially copyable and of equal size — the compiler enforces this at substitution time, not at runtime. This turns what would be silent UB with reinterpret_cast into a hard diagnostic.
#include <bit>
#include <cstdint>
#include <string>
#include <type_traits>
// Passes: both types are trivially copyable, same size
static_assert(std::is_trivially_copyable_v<float>);
static_assert(std::is_trivially_copyable_v<uint32_t>);
constexpr float from_bits(uint32_t u) noexcept {
return std::bit_cast<float>(u); // C++20
}
// Fails at compile time — std::string is not trivially copyable
// auto bad = std::bit_cast<std::string>(some_struct);
// Fails at compile time — sizes differ
struct ThreeBytes { uint8_t x, y, z; };
// auto bad2 = std::bit_cast<uint32_t>(ThreeBytes{}); // sizeof mismatchWhen a constraint is violated the error message names the exact unsatisfied requirement (sizeof mismatch or non-trivially-copyable type), making diagnosis straightforward.
// Struct with padding — legal but caveat on padding bytes
struct Pair {
uint16_t lo;
uint16_t hi;
};
static_assert(sizeof(Pair) == sizeof(uint32_t));
constexpr uint32_t pack(uint16_t lo, uint16_t hi) noexcept {
return std::bit_cast<uint32_t>(Pair{lo, hi}); // C++20
}
static_assert(pack(0x00FF, 0xFF00) == 0xFF0000FFu // little-endian
|| pack(0x00FF, 0xFF00) == 0x00FFFF00u); // big-endianEndianness detection with std::endian
std::endian (C++20, <bit>) exposes the host byte order as a compile-time constant, eliminating hand-rolled endianness probes. It provides three enumerators: little, big, and native; native equals one of the first two on all conforming implementations and may equal neither only on exotic mixed-endian hardware.
#include <bit> // C++20
#include <cstdint>
constexpr bool is_little_endian = (std::endian::native == std::endian::little);
constexpr bool is_big_endian = (std::endian::native == std::endian::big);
// Portable ntohl replacement using bit_cast + conditional byteswap
constexpr uint32_t from_network(uint32_t net) noexcept {
if constexpr (std::endian::native == std::endian::little) {
// swap bytes: network order is big-endian
return ((net & 0xFF000000u) >> 24)
| ((net & 0x00FF0000u) >> 8)
| ((net & 0x0000FF00u) << 8)
| ((net & 0x000000FFu) << 24);
}
return net; // already big-endian
}
static_assert(from_network(0x01020304u) == 0x04030201u // little-endian host
|| from_network(0x01020304u) == 0x01020304u); // big-endian hostif constexpr on std::endian::native is dead-code-eliminated by the compiler; the branch that does not apply generates no instructions.
std::byteswap (C++23)
std::byteswap<T>(value) (C++23, <bit>) reverses the bytes of any unsigned integer type in a single portable expression. It supersedes hand-written swap idioms and maps to a single bswap instruction on most targets.
#include <bit> // C++23
#include <cstdint>
#include <print>
constexpr uint32_t swap32(uint32_t v) noexcept {
return std::byteswap(v); // C++23
}
static_assert(swap32(0x0A0B0C0Du) == 0x0D0C0B0Au);
// Combined with std::endian for portable network deserialization
#include <array>
struct Header {
uint32_t magic;
uint32_t length;
};
static_assert(std::is_trivially_copyable_v<Header>);
Header parse_network(const std::array<std::byte, 8>& wire) {
auto h = std::bit_cast<Header>(wire); // C++20
if constexpr (std::endian::native == std::endian::little) {
h.magic = std::byteswap(h.magic); // C++23
h.length = std::byteswap(h.length); // C++23
}
return h;
}std::byteswap is constrained to unsigned integer types; passing a signed type or a struct produces a compile error. For C++20-only code, the manual swap shown in the std::endian section above is the portable alternative.
std::source_location for logging frameworks
std::source_location enables zero-overhead, type-safe logging APIs that capture call-site metadata without macros. The default-argument placement means users call log.info("msg") with no extra syntax while the framework receives the exact file, line, and function automatically.
#include <source_location>
#include <string_view>
#include <chrono>
#include <print>
// C++20
class StructuredLogger {
public:
enum class Level { Debug, Info, Warn, Error };
void debug(std::string_view msg,
std::source_location loc = std::source_location::current()) {
record(Level::Debug, msg, loc);
}
void info(std::string_view msg,
std::source_location loc = std::source_location::current()) {
record(Level::Info, msg, loc);
}
void warn(std::string_view msg,
std::source_location loc = std::source_location::current()) {
record(Level::Warn, msg, loc);
}
void error(std::string_view msg,
std::source_location loc = std::source_location::current()) {
record(Level::Error, msg, loc);
}
private:
void record(Level lvl, std::string_view msg,
const std::source_location& loc) {
const char* label[] = {"DBG","INF","WRN","ERR"};
std::println("[{}] {}:{}: {}", label[int(lvl)],
loc.file_name(), loc.line(), msg);
}
};
StructuredLogger logger;
logger.info("application started"); // [INF] main.cpp:47: application started
logger.error("disk full"); // [ERR] main.cpp:48: disk fullsource_location is trivially copyable and typically 16 bytes; store or pass by value. Forwarding through helper functions requires explicitly threading the loc parameter — do not call ::current() again inside an intermediary.
Combining source_location with std::format
std::format (C++20) and std::source_location compose naturally to build structured, human-readable log lines with compile-time location strings. Formatting directly from loc.file_name() and loc.line() avoids any heap allocation for the location portion of the message.
#include <source_location>
#include <format>
#include <string>
#include <print>
// C++20
std::string make_prefix(const std::source_location& loc) {
return std::format("[{}:{}@{}]",
loc.file_name(),
loc.line(),
loc.function_name());
}
void log(std::string_view msg,
std::source_location loc = std::source_location::current()) {
std::println("{} {}", make_prefix(loc), msg);
}
// C++23: std::print / std::println as a direct shorthand
void log23(std::string_view msg,
std::source_location loc = std::source_location::current()) {
std::println("[{}:{}] {}", loc.file_name(), loc.line(), msg);
}
void connect(std::string_view host) {
log(std::format("connecting to {}", host));
// prints: [net.cpp:31@connect] connecting to example.com
}When using std::format to build the message string before passing it to log(), the source location still refers to the log() call site, not the format() call — the location is always the outermost call with ::current() as a default argument.
source_location in macros vs function default arguments
The default-argument idiom entirely eliminates the need for __FILE__/__LINE__ macros in new code — but legacy macro-based APIs require an adapter if you need interoperability. Understanding the difference prevents subtle bugs where location resolves to the wrong site.
#include <source_location>
#include <string_view>
#include <print>
// Macro approach (pre-C++20): location baked in at expansion site
#define LOG_MACRO(msg) \
::log_impl(__FILE__, __LINE__, __func__, (msg))
void log_impl(const char* file, int line, const char* func, std::string_view msg) {
std::println("[{}:{}@{}] {}", file, line, func, msg);
}
// Modern approach: location resolved at call site via default argument
void log_modern(std::string_view msg, // C++20
std::source_location loc = std::source_location::current()) {
std::println("[{}:{}@{}] {}", loc.file_name(), loc.line(),
loc.function_name(), msg);
}
// Adapter: wrap legacy macro output in source_location value
// (only needed when passing location to template/generic code)
auto make_loc(const char* file, int line) {
// source_location has no public constructor — cannot fabricate one portably.
// Prefer the default-argument pattern for all new APIs.
return std::source_location::current(); // captures the adapter site, not caller
}The key rule: std::source_location::current() as a default function argument resolves at the call site; called anywhere else (including inside a macro body or a helper function body) it resolves at the point of the call inside that body. There is no portable way to construct a source_location with arbitrary coordinates — if you need to propagate location across an API boundary, pass the source_location object itself.
// Pattern: store at construction time for deferred logging
struct LogEntry {
std::string_view msg;
std::source_location loc;
explicit LogEntry(std::string_view m,
std::source_location l = std::source_location::current())
: msg(m), loc(l) {}
};
void enqueue(LogEntry entry); // can forward entry.loc without losing origin
enqueue(LogEntry{"disk flushed"}); // location = this line, preserved through enqueueSee Also
constexpr/consteval— compile-time evaluation model thatbit_castandsource_location::current()participate in- Casts —
reinterpret_cast,static_cast, and when each applies - Bit manipulation —
std::byteswap,std::popcount,std::countl_zero(C++20,<bit>) - Undefined behavior — strict aliasing rule that
reinterpret_casttype punning violates