Skip to content
C++
Language
since C++20
Basic

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++20

std::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

cpp
#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 format

bit_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:

cpp
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

cpp
#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=0x000000

Bit-field ordering is implementation-defined. This decomposition is architecture-specific — appropriate for low-level platform code and debugging tooling, not portable interchange.

constexpr evaluation

cpp
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);  // UB

Fast inverse square root (UB-free)

cpp
// 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

cpp
#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

cpp
#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

cpp
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 line

Forwarding location through call chains

cpp
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 site

Best Practices

  • Validate constraints explicitly. Add static_assert(sizeof(To) == sizeof(From)) and static_assert(std::is_trivially_copyable_v<To> && std::is_trivially_copyable_v<From>) near any bit_cast that is not obviously safe from surrounding context. This documents intent and produces better error messages.
  • Prefer bit_cast over memcpy when constexpr matters. They produce identical codegen at runtime; bit_cast additionally 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_location by 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

cpp
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

cpp
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

cpp
// 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:

cpp
// 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 construction

This "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.

cpp
// 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++20

At 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.

cpp
#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 mismatch

When a constraint is violated the error message names the exact unsatisfied requirement (sizeof mismatch or non-trivially-copyable type), making diagnosis straightforward.

cpp
// 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-endian

Endianness 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.

cpp
#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 host

if 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.

cpp
#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.

cpp
#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 full

source_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.

cpp
#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.

cpp
#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.

cpp
// 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 enqueue

See Also

  • constexpr / consteval — compile-time evaluation model that bit_cast and source_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_cast type punning violates