#include <iostream>
// 1. Define the master list of colors (X Macro)
// This is the single source of truth. Adding a color here
// automatically updates both the enum and the string converter below.
#define COLORS \
FUNC(Red) \
FUNC(Blue) \
FUNC(Green)
// 2. Generate the Enum Class
enum class Color {
#define FUNC(name) name,
COLORS
#undef FUNC
};
// 3. Generate a Helper Function to convert Enum to String using '#' (stringification)
const char* ColorToString(Color color) {
switch (color) {
#define FUNC(name) case Color::name: return #name;
COLORS
#undef FUNC
default: return "Unknown";
}
}
int main() {
// Instantiate color variables
Color r = Color::Red;
Color b = Color::Blue;
Color g = Color::Green;
// Output and verify the mapping works correctly
std::cout << "Color r is: " << ColorToString(r) << " (Enum value: " << static_cast<int>(r) << ")\n";
std::cout << "Color b is: " << ColorToString(b) << " (Enum value: " << static_cast<int>(b) << ")\n";
std::cout << "Color g is: " << ColorToString(g) << " (Enum value: " << static_cast<int>(g) << ")\n";
return 0;
}
Aug 3, 2026
[C++26] Reflection minute
Jun 16, 2026
[low latency] What is low latency (definition)
Reference:
https://github.com/crill-dev/crill
Low latency <-> High throughput
Server side is in the middle.
2 Categories
- Efficient programming
- Programming for deterministic execution time
Efficient programming
Microbenchmarks are tricky
- Warm the cache
- Randomise the heap
- Measure release build with same compiler flags
- But optimisations change what code you are measuring!
- A lot of stuff can "constexpr away" in microbenchmark but not in production code
Writing efficient code requires...
- Knowledge of the programming language
- Knowledge of the libraries used
- Knowledge of the compiler
- Optimiser
- ABI (Itanium, Microsoft, ...)
- Knowledge of the hardware architecture
- CPU architecture (Instruction set, pipeline, SIMD...)
- Cache hierarchy (Registers, L1/2/3 cache)
- Prefetcher, translation look-aside buffer
- Branch predictor, branch target buffer
Avoid unnecessary work
- Avoid unnecessary copies
- Avoid unnecessary function calls / indirections
- inline functions
- prefer std::variant / CRTP / "deducing this" (since C++23)
over virtual functions - Make as many decisions as possible at compile time
- constexpr all the things
- Template meta-programming
- Generate lookup tables at compile time
- Use efficient mathematical operations
- Fast approximations
- Use powers of two for sizes (compiler can replace division/mod with bit shifts)
- Lookup tables
- Many other techniques
Low-level bit manipulation in C++
- Object lifetime rules
- Aliasing rules
- Alignment rules
- Object representations
- Value representations
- std::bit_cast (since C++20)
- Implicit-lifetime types (since C++20)
- std::start_lifetime_as (since C++23)
The optimiser & undefined behaviour
- memory-related UB
- type system violation
- out-of-bounds
- lifetime violation (dangling pointers, use-after-free)
- uninitialised variables
- data races
- signed integer overflow
- infinite loops with no side effects
The optimiser & undefined behaviour
- std::assume_aligned (C++20)
- [[assume]] (C++23)
- [[noalias]] (C++26 ??)
- [unsequenced]] == [[gnu::const]]
[reproducible]] == [[gnu::pure]] (C23, C++26 ??)
Sub-properties of reproducible & unsequenced
- stateless: function that does not define mutable static or thread-local objects (nor do functions that are called by it)
- effect-less: function that does not have observable side effects
- idempotent: repeated evaluation gives the same result (hence may read global state)
- independent: does not depend on other state than the arguments or constants (hence may write to globals)
- reproducible: effect-less and idempotent
- un-sequenced: stateless, effect-less, idempotent, and independent
CPU pipeline hazards
- Branch hazard
- Data hazard
- Hardware hazard
- Limited amount of adders/shifters
- Limited amount of load/store units
- (latest Intel CPUs: 3 loads + 2 stores per cycle)
- → sometimes you can replace loads by shifts → increase bandwidth of every load using SIMD
[likely]] and [unlikely]]
https://vsdmars.blogspot.com/2016/01/likely-or-unlikely-easy-misleading.html
- Does not affect branch predictor!
- Can affect code layout
- Have various of pitfalls
- Aaron Ballman: "Don't use the [likely]] or [unlikely]] attributes"
- Amir Kirsh & Tomer Vromen:"C++20 Likely and Unlikely:
- A Journey Through Branch Prediction and Compiler Optimizations" (2022)
The 3 Types of Data Hazards
- RAW (Read After Write): The most common type. Instruction B tries to read data before Instruction A finishes writing it.
- WAR (Write After Read): Instruction B tries to write to a location before Instruction A has a chance to read it.
- WAW (Write After Write): Instruction B tries to write to a destination before Instruction A writes to it, potentially leaving the wrong final value.
Rarely have to worry about this breaking your program because the system handles it automatically:
- The Hardware (CPU): Modern CPUs use a trick called Data Forwarding (passing the result directly from the math unit to the next instruction before it's even written to memory) or they just stall the processor for a cycle to let the data catch up.
- The Compiler: Optimization flags (like -O2 or -O3 in GCC/Clang) allow the C++ compiler to smartly rearrange your instructions so that independent calculations are placed in between dependent ones, keeping the pipeline moving smoothly without stalls.
SIMD
- "Single instruction, multiple data"
- CPU-specific: MMX, SSE 1/2/3/4, AVX, AVX2, AVX-512, AMX, NEON, SVE...
- How to use?
- Auto-vectorisation
- Explicit vectorisation using SIMD libraries
- Google Highway, xsimd, vectorclass, eve, std::simd proposal for C++26 (P1928)
- Jeff Garland: "SIMD Libraries in C++" (CppNow 2023)
- Writing intrinsics
- Writing assembly
- SWAR ("SIMD Within A Register")
Other SIMD considerations
- If you know the exact target CPU:
- use arch compiler flags
- If you don't:
- dynamic dispatch
- function multiversioning (GCC/Clang only)
function multiversioning:
#include <iostream>
// 1. Version optimized for modern CPUs with AVX2
__attribute__((target("avx2")))
void process_data() {
std::cout << "Running high-performance AVX2 vectorized version!\n";
// Fast vector math goes here
}
// 2. Version optimized for older SSE4.2 capable CPUs
__attribute__((target("sse4.2")))
void process_data() {
std::cout << "Running mid-tier SSE4.2 version!\n";
}
// 3. The mandatory default fallback version
__attribute__((target("default")))
void process_data() {
std::cout << "Running generic baseline version.\n";
}
int main() {
// You call it like a regular function.
// The resolution happens completely behind the scenes.
process_data();
return 0;
}
Autovectorisation
- Highly dependent on compiler
- Only works with vectors/arrays of int, char, double etc. - no structs
- Workaround: struct of arrays instead of array of structs
- Traverse data linearly
- for loop, not while loop
- Number of iterations predetermined (ideally, known at compile time)
- No data-dependent break, goto, etc.
- No conditionals
- No data dependencies between array elements
- No aliasing
Cache miss
- Data locality
- Align data on cachelines
- Concurrency aspect (true/false sharing)
- Contiguous data traversal (also good for prefetcher)
- "Almost always vector"
- Cache-friendly associative containers
- std::flat_set/std::flat_map (C++23), Abseil containers
- Cache-friendly algorithms
- Cache-friendly binary search
- Consider generated code layout & alignment
- Avoid branches
- Avoid virtual functions
- std::variant
- Compile-time polymorphism
- CRTP, mixins, "deducing this" (since C++23)
- Data cache
- Periodically poke data on a timer
- Instruction cache
- Periodically run the hot path with dummy input/output
- CPU throttling
- Due to overheating
- spread thermal dissipation
- Due to low activity
- keep CPU busy with pause instructions
Programming for deterministic execution time
What not to do in the hot path:
- Dynamic memory allocations/deallocations
- blocking the thread
- I/O
- exceptions
- context switches / mode switches (user/kernel space)
- syscalls
- calling into unknown code
- loops without definite bounds
- algorithms > O(1), or with no statically known upper bound on N
Dynamic memory allocations/deallocations
- Do not use data types that allocate
- Do not use algorithms that allocate
- Do not use data structures that allocate
- std::stable_sort
- std::stable_partition
- std::inplace_merge
- minimising average cost, not worst case
- not constant time
- multithreaded (locks)
- eventually go to OS to request dynamic memory
- Monotonic allocators
- std::pmr::monotonic_buffer_resource
- Pool allocators
- std::pmr::unsynchronised_pool_resource
- Frame allocators
- Arena allocators
- Double-ended allocators for big/small buffers
- Lock-free allocator (require helper thread)
- create and suspend coroutine frame upfront
- write your own promise type, defining its own custom operator new and operator delete
- Don't use coroutines in a low-latency/real-time scenario
Blocking the thread
- atomic == indivisible, race-free
- lock-free == at least one thread is guaranteed to make progress
- wait-free == all threads are guaranteed to make progress
- Can't block hot path (no mutexes, no spinning)
- Can't do any syscalls
- Can't do anything with unbounded/non-deterministic runtime
- Only available synchronisation mechanism:
- std::atomic
- std::atomic_ref (since C++20)
- never on hot path:
- spin on std::atomic, compare_exchange in a loop(use compare_exchange_weak,
https://vsdmars.blogspot.com/2024/03/c-compareexchangeweak.html), etc. - use std::atomic<T>::wait/notify_one/notify_all
- always:
- static_assert(std::atomic<T>::is_always_lock_free);
- Passing data to/from "hot path" thread
- wait-free queue
- Sharing data with "hot path" thread
- Hot path reads
- spinlock try_lock (involve syscall.) Solution: spinlock with try_lock.
- Use with spinlock, not std::mutex!
- Use progressive backoff to avoid wasting energy
https://vsdmars.blogspot.com/2018/09/c-something-about-spinlock.html - Tradeoffs:
- Reader always wait-free
- Single reader
- Only works if reading is allowed to fail
- Reader blocks writer(s) who needs to spin
- one writer (or multiple writers who block each other)
- "spin-on-write"
- Easy to use
- Reader always wait-free
- Tradeoffs:
- Single reader
- read is 2x faster then std::atomic::exchange; because .load read from
local CPU cache(since the value is MESI shared) - reader blocks writer(s) who need to spin
- one writer (or multiple writers who block each other)
- writer needs to heap-allocate + copy
std::unique_ptr‹biquad_coefficients> storage;
std::atomic<biquad_coefficients*> coeffs;
void process(audio_buffer& buffer) {
auto* current_coeffs = coeffs.exchange(nullptr);
process_biquad(buffer, *current_coffs) ;
coeffs.store(current_coeffs) ;
}
void update_coeffs (biquad_coefficients new_coeffs) {
auto new_coeffs = std::make_unique‹biquad_coefficients>(new_coeffs);
for (auto* expected = storage.get();
!coeffs.compare_exchange_weak(expected, new_coeffs.get());
expected = storage.get() /* spin */;)
storage = std::move(new_coffs);
}
// Or just use crill lib
crill::spin_on_write_object<biquad_coefficients> coeffs;
void process (audio_buffer& buffer) {
auto read_ptr = coeffs.lock_read();
process_biquad (buffer, *read_ptr);
}
void update_coeffs(biquad_coefficients new_coeffs) {
coeffs.update(new_coeffs);
}
- read is now a single atomic load (= no overhead on modern platforms)
- Multiple concurrent readers & writers
- Readers don't block writers
- But we need to solve deferred reclamation problem!
- hard...
- https://www.youtube.com/watch?v=7fKxIZOyBCE
crill::defer_reclaim_object<biquad_coefficients> coeffs; void process(audio_buffer& buffer) { uto read_ptr = coeffs.Lock_read() ; process_biquad (buffer, *read_ptr); } void update_coeffs (biquad_coefficients new_coeffs) { coeffs.update(new_coeffs); } void timer_callback(){ coeffs.reclaim(); } - Reading always wait-free
- Multiple readers
- algorithm simplifies greatly if single reader
- read is single atomic load (= no overhead on modern platforms)
- Readers do not block writers
- Writer needs to do heap allocation
- User needs to manage reclamation
- Variation: reclaim_on_write_object
- Reading always wait-free
- Multiple readers
- algorithm simplifies greatly if single reader
- read is single atomic load (= no overhead on modern platforms)
- Writer waits for active reader(s) to finish
- Writing is fast and does not require heap allocation
- No need to manage reclamation
- Double-buffering (just atomic SWAP to update the data.)
- Always be aware of ABA problem. Thus always having state in the single
atomic variable.std::array<frequency_spectrum, 2> slots; std::atomic<int> idx = {0}; void process (audio_buffer& audio_in) { auto spectrum = calculate_spectrum(audio_in); int write_id = id.load(); slots [write_idx] = spectrum; } void update_spectrum() { int read_id = idx.fetch_xor(1); draw_spectrum(slots[read_idx]); } // Solve the ABA problem std::array<frequency_spectrum, 2> slots; std::atomic<int> idx = {0}; enum { BIT_IDX = (1 << 0), BIT_NEWDATA = (1 << 1), BIT_BUSY = (1 << 2), }; void process (audio_buffer& audio_in) { auto spectrum = calculate_spectrum(audio_in); int write_idx = idx.fetch_or(BIT_BUSY) & BIT_IDX; slots [write_idx] = spectrum; idx.store ((write_idx & BIT_IDX) | BIT_NEWDATA); } // ... - SeqLock (HFT technique)
- Writing always wait-free
- Good solution if writing happens more rarely, data not too large
- Tradeoffs:
- Single writer, multiple readers
- Readers lock-free but not wait-free (might have to retry unbounded nr of times)
- Data must be trivially copyable
- Overhead of copying data atomically on both reader & writer
std::atomic<std::size_t> seq = 0; // single thread doing the write. void store(T t) noexcept { std::size_t old_seq = seq.fetch_add(1); /* // Faster auto old_seq = seq.load(std::memory_order_relaxed); seq.store(old_seq + 1, std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_release); */ // write data... seq.store(old_seq + 2); } bool try_load(T& t) const noexcept { std::size_t seq1 = seq.load(std::memory_order_acquired); if (seq1 % 2 != 0) return false; // read data... /* Use Byte-wise atomic memcpy to read the data; or chunk it; DO NOT USE memcpy for (size_t i = 0; i < count; ++i) { reinterpret_cast<char*>(dest)[i] = atomic_ref<char>(reinterpret_cast<char*>(source)[i]).load(memory_order_relaxed); } atomic_thread_fence(order); */ std::atomic_thread_fence(std::memory_order_acquired); std: :size_t seq2 = seq.load(std::memory_order_relaxed); return seq1 == seq2; }
I/O
- With other threads: Push message into wait-free specific queue
- With other processes: Shared memory
- With hardware: Direct Memory Access (DMA)
- Pre-load into RAM and lock address range (to prevent swap-out) / MMAP
- mlock (POSIX)
mlock(), mlock2(), and mlockall() lock part or all of the calling process's virtual address space into RAM, preventing that memory from being paged to the swap area. munlock() and munlockall() perform the converse operation, unlocking part or all of the calling process's virtual address space, so that pages in the specified virtual address range can be swapped out again if required by the kernel memory manager. Memory locking and unlocking are performed in units of whole pages. - VirtualLock (Windows)
- Disk streaming
- Pre-load into RAM first ~ 100 ms of every possible sound
- Once it starts playing, start filling in the rest from disk (on a background thread)
- Mainstream operating systems: Thread priority
- Real-time operating systems: deterministic thread scheduler
- If you control the hardware:
- Kernel bypass
- If your hot path is in a single thread,
- and you don't care about efficiency of other threads:
- Turn off hyperthreading
- Pin hot path thread to one CPU core
Jan 3, 2026
[C++26] std::execution
Reference:
https://en.cppreference.com/w/cpp/experimen
https://www.virjacode.com/papers/p3288.htm
code:
constexpr explicit operation_state(Range&& r, Receiver rcvr)
: /* ... */
states_(
std::from_range,
std::ranges::views::transform(
std::forward<Range>(r),
[&]<typename U> requires
std::constructible_from<ender, U>(U&& u)
{
return elide([&, s = std::forward<U>(u)]() mutable {
return state_(*this, std::move(s));
});
})),
/* ... */
{}
Dec 10, 2025
[C++] container.assign_range(rg)
Reference:
https://eel.is/c++draft/sequence.reqmts#64
Recommended practice:
Nov 13, 2025
[C++][concepts] compound requirements trick
However, due to this kind of usage, C++ 26 has pack_indexing
#include <utility> // index sequence
#include <iostream> // for demo only
template<typename, size_t> concept Any = true;
auto invoke_with_last_arg(auto f, auto... args) {
return [&]<size_t... Idxs>(std::index_sequence<Idxs...>) {
// compound requirements, Any<Idxs> is assigned as:
// Any<decltype(_), size_t>
return [&](Any<Idxs> auto... _, auto last) {
return f(last);
}(args...);
}(std::make_index_sequence<sizeof...(args)-1>{});
}
void demo(auto... args) {
invoke_with_last_arg(
[](auto last_arg) { std::cout << last_arg << std::endl; },
args...
);
}
int main() { demo(3.14, 42, "hello world"); }Sep 17, 2025
[C++][P2738R1] constexpr cast from void*: towards constexpr type-erasure
Purpose:
On memory constrained embedded platforms, a common approach to achieve run-time memory savings is to ensure common code paths. Type erasure is helpful to achieve common code paths. To save memory, it is desirable to evaluate code at compile time to the maximal extent. To keep code common between compile time and run-time, limited type erasure is required at compile time.
struct A {
virtual void f() {};
int a;
};
struct B {
int b;
};
struct C : B, A {}; // C inherits B first, then A
int main() {
C c;
void* v = &c;
// static_cast<B*>(v) is UB; should static_cast to most derived type then
// static_cast to base type.
assert(static_cast<B*>(v) == static_cast<B*>(static_cast<C*>(v)));
}
#include <string_view>
struct Sheep {
constexpr std::string_view speak() const noexcept {
return "Baaaaaa";
}
};
struct Cow {
constexpr std::string_view speak() const noexcept {
return "Mooo";
}
};
class Animal_View {
private:
const void* animal;
std::string_view (*speak_function)(const void*);
public:
template <typename Animal>
constexpr Animal_View(const Animal& a)
: animal{&a}, speak_function{[](const void* object) {
return static_cast<const Animal*>(object)->speak();
}} {}
constexpr std::string_view speak() const noexcept {
return speak_function(animal);
}
};
// This is the key bit here. This is a single concrete function
// that can take anything that happens to have the "Animal_View" interface
std::string_view do_speak(Animal_View av) {
return av.speak();
}
int main() {
// A Cow is a cow. The only thing that makes it special
// is that it has a "std::string_view speak() const" member
constexpr Cow cow;
// cannot be constexpr because of static_cast
[[maybe_unused]] auto result = do_speak(cow);
return static_cast<int>(result.size());
}
Apr 27, 2025
[C++] Object Lifetimes reading minute
Reference:
A Deep Dive Into C++ Object Lifetimes - Jonathan Müller - C++Now 2024
[C++] null pointer and memory laundering.
[C++] transparently replaceable
[Book]Inside the C++ Object Model
nifty counter
some move semantics wrap-up
[C++] [CppCon 2025] Implement the C++ Standard Library minute - [[no_unique_address]]
Category
Storage (i.e. either have in memory or in the instruction)
- unit, in byte. Every byte has unique address.
- What's on the storage can be anything.
- When storage for an object with automatic or dynamic storage duration is obtained,
the object has an indeterminate value, and if no initialization if performed for the object,
that object retains an indeterminate value until that value is replaced. If an indeterminate
value is produced by an evaluation, the behavior is undefined. - In C++26, read of indeterminate value is erroneous, not undefined. Ref: P2795
Duration
- minimum potential lifetime of the storage containing the object.
- Static, thread, and automatic storage durations are associated with objects introduced by declarations.
automatic storage durations
- Lasts until the block in which they are created exits.
static storage duration
- namespace scope, first declared with the static or extern keywords. Last the duration of the program.
- function-local static vs. global scope
- constinit vs. dynamic initialization
- nifty counters, module dependency graph, inline variables.
thread storage duration
- thread_local keyword. Last for the duration of the thread they are created.
Value (i.e. being initialized)
Type (determin the storage alloting size.)
- Mapping the bits to the interpretation.
Object
- a particular type and occupies a region of storage at a particular
address where its value is stored. - Function is not an object(function address can be changed.)
- Reference is not an object. However, pointer type is an object.
- any possibly cv-qualified type other than function, reference, or void types
Lifetime
- Lifetime of an object is a runtime property of the object.
- Before the lifetime of an object starts and after its lifetime ends
there are significant restrictions on the use of the object.
Object lifetime spans
- storage is allocated
- object is initialized, the lifetime starts
- object is used, its value changed or read.
- object is destroyed, the lifetime ends.
- storage is deallocated.
The lifetime of an object of type T begins when
- storage with the proper alignment and size for type T is obtained, and
- its initialization (if any) is complete.
int main() {
int* i = new int; // however, `new int()` has default value.
std::print("{}\n", *i); // UB
}
Whenever a prvalue is used in a context where an xvalue is expected, a temporary object is created
- binding a reference to a prvalue
- member-access on a prvalue
- using an array prvalue
- discarding the result of a function call that returns a prvalue.
stc::vector<std::string> get_strings();
int main() {
for (auto&& str: get_strings()) {
std::print("{}\n", str);
} // temporary destroyed here.
// lifetime expanded.
auto&& str_vec = get_strings();
// C++23, only for 'range for'
// https://en.cppreference.com/w/cpp/language/lifetime
// some move semantics wrap-up; https://vsdmars.blogspot.com/2021/12/c-some-move-semantics-wrap-up.html
for (auto&& c : get_strings()[0]) {
std::print("{}\n", c);
} // temporary destroyed here.
// this is dangling
// auto&& str = get_strings()[0];
}void* memory = ::operator new(sizeof(int));
int* ptr = ::new(memory) int(11);
std::destroy_at(ptr);
::operator delete(memory);alignas
alignas(int) unsigned char buffer[sizeof(int)];
int* ptr = ::new(static_cast<void*>(buffer)) int(11);
std::destroy_at(ptr);
int x = 11;
std::destroy_at(&x); // end lifetime
int* ptr = ::new(static_cast<void*>(&x)) int(42);You cannot legally reuse the memory of an object originally declared const to construct a new object if that construction modifies the memory. The const promise extends to the storage in this scenario.
- The C++ standard states ([dcl.type.cv] p4 in C++20, similar rules in earlier versions): "Except that any class member declared mutable can be modified, any attempt to modify an object declared with const-qualified type through a glvalue of other than const-qualified type results in undefined behavior."
- While you technically ended the lifetime of the original const int object, you are attempting to write (int(42)) into the storage that was originally allocated for an object declared const.
- The standard effectively forbids reusing the storage of a const object to create a new object if that creation involves modifying the storage. The "const-ness" is associated not just with the object's lifetime but also with the storage it occupied in this specific context.
- Attempting to write 42 into memory that the compiler might have placed in a read-only segment (because x was const) could lead to a hardware exception (like a segmentation fault).
- Even if not in read-only memory, the compiler's optimizations might rely on that memory location never changing from the value `11`. Overwriting it violates the assumption.
const int x = 11;
std::destroy_at(&x); // end lifetime; only calls the object's destructor
// UB
::new(static_cast<void*>(&x)) int(42);const int* ptr = new const int(11);
std::destroy_at(&ptr); // end lifetime
::new(static_cast<void*>(ptr)) int(42);transparently replaceable object
T is transparently replaceable by U if
- T and U use the same storage, and
- T and U have the same type (ignoring top-level cv-qualifiers)
T is not transparently replaceable if
- const objects; however, const heap objects can be fixed through std::launder due to it's on the heap, not read-only binary section.
- base classes
- [[no_unique_address]] members
// x can't be in the register.
int x = 11;
std::destroy_at(&x); // only calls the object's destructor
::new(static_cast<void*>(&x)) int(42); // transparent replacement.
std::print("{}\n", x); // ok
foo& foo::operator=(const foo& other) {
std::destroy_at(this); // only calls the object's destructor
::new(static_cast<void*>(this)) foo(other); // transparent replacement.
return *this; // ok
}non-transparent
const int* ptr = new const int(11);
std::destroy_at(ptr); // only calls the object's destructor
int* new_ptr = ::new(static_cast<void*>(ptr)) const int(42); // non-transparent
std::print("{}\n", *new_ptr); // ok
std::print("{}\n", *ptr); // UB
std::launder
- launder is for /previous/ object, not the new one. Compiler always give out right value for new one.
- launder update the provenance of an object. (see below about provenance, a compiler optimization term.)
const int* ptr = new const int(11);
std::destroy_at(ptr); // only calls the object's destructor
int* new_ptr = ::new(static_cast<void*>(ptr)) const int(42); // non-transparent
std::print("{}\n", *new_ptr); // ok
std::print("{}\n", *std::launder(ptr)); // okImplicit create object(and initialize it.)
int* ptr = static_cast<int*>(std::malloc(sizeof(int))); // create an int, not init.
*ptr = 11;2) Anything that starts the lifetime of an unsigned char/std::byte array.
alignas(int) unsigned char buffer[sizeof(int)]; // create an int, not init.
int* ptr = std::launder(reinterpret_cast<int*>(buffer)); // P3006, launder can be avoided.
*ptr = 11;The comment issue: The comment // create nothing due to it's char array, not unsigned char array is actually highlighting an ultra-pedantic quirk from older C++ standards. Historically, only unsigned char and std::byte arrays could provide raw, uninitialized storage without breaking rules. However, C++20 drastically changed this by introducing Implicit Object Creation (P0593), which explicitly grants plain char arrays the exact same power. Creating a char array does not instantiate any object inside it yet—it merely prepares the blank canvas.
// create nothing due to it's char array, not unsigned char array.
alignas(int) char buffer[sizeof(int)];
std::memcpy(buffer, &some_int, sizeof(int)); // create an int
int* ptr = std::launder(reinterpret_cast<int*>(buffer));
std::print("{}\n", *ptr);std::launder is there because the compiler is highly aggressive when optimizing based on types. If you simply did reinterpret_cast<int*>(buffer) inside the if branch, a strictly optimizing compiler could say: "Wait a minute. buffer is an unsigned char array. You're trying to write to it as an int. That violates the Strict Aliasing Rule, so I'm going to assume this code is impossible and optimize it away entirely."
std::launder tells the compiler: "Stop trying to trace the history of this memory block back to the unsigned char array. Trust me that a valid object of the type I am casting to exists right here, right now."
int* ptr = static_cast<int*>(mmap(...));
std::print("{}\n", *ptr);
// create int or float, later compiler time-traval backs here.
alignas(int) unsigned char buffer[sizeof(int)];
if(...)
*std::launder(reinterpret_cast<int*>(buffer)) = 11;
else
*std::launder(reinterpret_cast<float*>(buffer)) = 11.1;// Still UB, only unsigned char or std::byte can be cast to other type.
int i = 11;
float f = *std::launder(reinterpret_cast<float*>(&i)); // UB, we don't have float type.
struct data {
std::uint8_t op;
std::uint32_t a, b, c;
};
void process(unsigned char* buffer, std::size_t size) {
data* ptr = std::launder(reinterpret_cast<data*>(buffer));
std::print("{}\n", *ptr); // might be UB depends on how the buffer is created.
}
that is: (也就是 std::launder 只能用在object has valid lifetime.)
// Inside main:
unsigned char* buffer = new unsigned char[sizeof(data)];
std::fread(buffer, 1, sizeof(data), file); // Just raw bytes
process(buffer, sizeof(data)); // UB!
but ok if:// Inside main:
alignas(data) unsigned char buffer[sizeof(data)];
// An actual 'data' object is physically born here via placement new / construct_at
data* original = std::construct_at(reinterpret_cast<data*>(buffer), data{1, 10, 20, 30});
process(buffer, sizeof(data)); // Safe!
struct data {
std::uint8_t op;
std::uint32_t a, b, c;
};
void process(unsigned char* buffer, std::size_t size) {
data* ptr = ::new(static_cast<void*>(buffer));
// ok, but could be wrong due to new start a lifetime of new object.
// *ptr might not hold the previous buffer value.
std::print("{}\n", *ptr);
}// Fix, C++23,
// std::start_lifetime_as https://en.cppreference.com/w/cpp/memory/start_lifetime_as
// std::start_lifetime_as_array<data>(ptr, count);
// Treat these bytes as a valid object starting right now,
// without modifying the underlying data or running any code.
struct data {
std::uint8_t op;
std::uint32_t a, b, c;
};
void process(unsigned char* buffer, std::size_t size) {
// NOT calling data's constructor
data* ptr = std::start_lifetime_as<data>(buffer);
std::print("{}\n", *ptr); // ok.
}template<typename T>
T* start_lifetime_as(void* ptr) {
// https://en.cppreference.com/cpp/string/byte/memmove
// Implicitly 'creates objects' at dest
// Thus std::launder works on valid lifetime bounded object.
std::memmove(ptr, ptr, sizeof(T));
return std::launder(static_cast<T*>(ptr));
}Implicit destruction of objects
The lifetime of an object o of type T ends when
- if T is a non-class type, the object is destroyed, or
- if T is a class type, the destructor call starts, or
- the storage which the object occupies is released, or is reused
by an object that is not nested within o.
int x = 11;
::new(static_cast<void*>(&x)) int(42); // end + start new lifetime.
std::print("{}\n", x);
alignas(int) unsigned char buffer[sizeof(int)]; // start lifetime
int* ptr = ::new(static_cast<void*>(buffer)) int(11); // end + start new lifetime.
std::print("{}\n", *ptr);memory leaks are not UB, but just memory leak.
std::string str = "leaking"; // leaked after next line.
::new(static_cast<void*>(&str)) std::string("new str");Provenance
- Each object has a unique provenance.
- All objects in an array have the same provenance.
- Re-using the memory of an object changes the provenance unless
the object is transparently replaced. (std::launder)
A pointer T* is logically a pair(address, provenance)
- The address is the only thing that is physically observable.
- The provenance identifies to the object of allocation the pointer was derived from.
A pointer dereference is only valid if
- The address is in the range of allowed addresses for the provenance.
- The current provenance of that address is the same as the provenance of the pointer.
The pointer provenance cannot be changed using pointer arithmetic.
int foo() {
int x, y;
y = 11;
if(&x + 1 == &y) {
do_sth(&x);
}
return y;
}
void do_sth(int* ptr) {
*(ptr + 1) = 42; // UB, address not in range.
}
const int* ptr = new const int(11); // provenance A
std::destroy_at(ptr); // only calls the object's destructor
int* new_ptr = ::new(static_cast<void*>(ptr)) const int(42); // non-transparent, provenance B
std::print("{}\n", *new_ptr); // ok
std::print("{}\n", *ptr); // UB due to provenance does not match, launder comes into the play.
// fix
std::print("{}\n", *std::launder(ptr)); // launder updates the provenance and make it updated.Reference has provenance as well.
const int* ptr = new const int(11); // provenance A
const int& ref = *ptr; // provenance B
std::destroy_at(ptr);
::new(static_cast<void*>(ptr)) const int(42); // non-transparent, provenance C
std::print("{}\n", ref); // UB, provenance B != provenance C
// fix
std::print("{}\n", *std::launder(&ref)); // launder updates the provenance and make it updated.Type punning
reinterpret_cast between unrelated types can be done butdereferencing the cast pointer is UB.
int i = 11;
float* f_ptr = ::new(static_cast<void*>(&i)) float(3.14);
std::print("{}\n", *f_ptr); // ok
std::print("{}\n", i); // UB
int i = 11;
float* f_ptr = std::start_lifetime_as<float>(&i);
std::print("{}\n", *f_ptr); // ok
std::print("{}\n", i); // UBBe careful about getting the pointer
int i = 11;
float* f_ptr = reinterpret_cast<float*>(&i);
::new(static_cast<void*>(&i)) float(3.14); // i is no longer same provenance
std::print("{}\n", *f_ptr); // UB
int i = 11;
::new(static_cast<void*>(&i)) float(3.14);
float* f_ptr = reinterpret_cast<float*>(&i); // i is no longer same provenance
std::print("{}\n", *f_ptr); // UB
int i = 11;
float* f_ptr = ::new(static_cast<void*>(&i)) float(3.14);
std::print("{}\n", *f_ptr); // ok
int i = 11;
float* f_ptr = reinterpret_cast<float*>(&i);
::new(static_cast<void*>(&i)) float(3.14); // i is no longer same provenance
std::print("{}\n", *std::launder(f_ptr)); // ok
alignas(int) unsigned char buffer[sizeof(int)];
int* ptr = reinterpret_cast<int*>(buffer);
*ptr = 11; // currently needs to call std::launder but fixed in P3006When to use std::launder?
When want to re-use the storage of
- const heap objects; const object cannot be fixed. Once it's const, it's const for life.
- base classes
- [[no_unique_address]] members
- Or when re-using memory as storage for a different type.
There are exceptions for dereferencing from reinterpret_cast with different types.
- the dynamic type of the object,
- a type that is the signed or unsigned type corresponding to the dynamic type of the object, or
- a char, unsigned char, or std::byte type.
int i = 11;
std::print("{}\n", *reinterpret_cast<unsigned*>(&i)); // ok
std::print("{}\n", *reinterpret_cast<std::byte*>(&i)); // okObject representation
int object = 11;
std::byte* ptr = reinterpret_cast<std::byte*>(&object);
for (auto i = 0z; i != sizeof(object); ++i) {
std::print("{:02x} ", static_cast<int>(*ptr++));
}Type punning via std::memcpy
int i = 11;
float f;
std::memcpy(&f, &i, sizeof(f));
std::print("{}\n", f); // ok
std::print("{}\n", i); // ok
// C++20, std::bit_cast, doing same as std::memcpy, but constexpr
int i = 11;
float f = std::bit_cast<float>(i);
std::print("{}\n", f); // ok
std::print("{}\n", i); // ok
Another exceptions
- they are the same object, or
- one is a union object and the other is a non-static data member of that object ([class.union]), or
- one is a standard-layout class object and the other is the first non-static data member of that object or any base class sub-object of that object ([class.mem]), or
- there exists an object c such that a and c are pointer-interconvertible, and c and b are pointer-interconvertible.
struct A {
int member;
};
A a{.member = 11};
int* i_ptr = reinterpret_cast<int*>(&a);
std::print("{}\n", *i_ptr); // ok
std::print("{}\n", reinterpret_cast<A*>(i_ptr)->member); // okUnion
iff the unassigned data has the same type of the assigned data. Type is all about.
Type is how compiler consider the underneath memory layout/presentation of the object.
union U {
int i;
float f;
};
U u{.i = 11};
u.f = 3.14f; // now f is the active member of the union.
std::print("{}\n", u.f); // ok
std::print("{}\n", u.i); // UB
union U {
struct A {
int prefix;
int i;
} a;
struct B {
int prefix2;
float f;
} b;
};
U u{.a = {.prefix = 0, .i = 11}};
std::print("{}\n", u.a.prefix); // ok
std::print("{}\n", u.b.prefix2); // ok, due to same address with same /type/.
Take away
Don't rely on implicit object creation
- Use placement new to explicitly create a new object, thus new provenance.
- Use std::start_lifetime_as to re-interpret raw bytes as an object, thus new provenance.
- Whenever possible, use the pointer from placement new and std::start_lifetime_as directly, thus new provenance.
- [TRICK] Use union { char empty, T t;} instead of alignas(T) unsigned char buffer[sizeof(T)];
Dec 1, 2024
[C++26] pack_indexing
#include <type_traits>
template <typename... Ts>
using last_type_t = Ts...[sizeof...(Ts) - 1];
template<typename... T>
void run(T... t) {
decltype(t...[1]) i = 0; // float
}
int main() {
static_assert(std::is_same_v<last_type_t<int>, int>);
static_assert(std::is_same_v<last_type_t<bool, char>, char>);
static_assert(std::is_same_v<last_type_t<float, int, bool*>, bool*>);
struct tmp{};
run(1, 2.0f, tmp{});
}
[C++][cpponsea 2024] C++ Cache Friendly Data + Functional + Ranges summary
Reference:
https://www.youtube.com/watch?v=XJzs4kC9d-Y
https://github.com/rollbear/columnist
https://godbolt.org/z/a8P7oTjoh
#include <functional>
#include <iostream>
#include <tuple>
#include <vector>
template<typename... Ts>
class Table {
public:
using row = std::tuple<Ts&...>;
row operator[](size_t i) {
auto access = [&]<size_t... Is>(std::index_sequence<Is...>) {
return row{std::get<Is>(data_)[i]...};
};
return std::invoke(access, indexes);
}
Table() {
init(indexes);
}
private:
static constexpr std::index_sequence_for<Ts...> indexes{};
std::tuple<std::vector<Ts>...> data_;
private:
template<size_t... I>
constexpr void init(std::index_sequence<I...>) {
((std::get<I>(data_).push_back({42}),...));
}
};
struct Data1 {
int value;
};
struct Data2 {
int value;
};
int main() {
Table<Data1, Data2> table;
auto row = table[0];
std::cout << std::get<0>(row).value << '\n'; // print 42
};struct sentinel {}; // or using sentinal = void;
struct iterator {
using value_type = row;
using difference_type = ssize_t;
value_type operator*() const {
return std::invoke([&]<size_t... Is>(std::index_sequence<Is...>) {
return value_type{std::get<Is>(t->data_)[offset]...};
}, t->indexes);
}
iterator& operator++() { ++offset; return *this;}
iterator operator++(int) { auto copy = *this; ++*this; return copy;}
bool operator==(const iterator&) const = default;
bool operator==(sentinel) const { return offset == t->size(); }
table* t;
size_t offset;
};template<typename... Ts>
class Table {
public:
friend class iterator;
iterator begin() { return {this, 0}; }
sentinel end() { return {}; }
...
};template<typename...>
struct Table;
template<typename, typename>
struct Row;
template <typename... Ts, size_t... Cs>
struct Row<Table<Ts...>, std::index_sequence<Cs...>> {
using row_id = typename Table<Ts...>::row_id;
// Indirection to make std::index_sequence<0, 2, 4> -> I==1 means std::get<2>(...)
template<size_t I>
friend auto& get(const Row& r) {
static constexpr std::array columns{Cs...};
return std::get<columns[I]>(r.t->data_)[r.offset];
}
...
Table<Ts...>* t;
size_t offset;
};
template<typename Table, size_t... Cs>
struct std::tuple_size<Row<Table, std::index_sequence<Cs...>>>
: std::integral_constant<size_t, sizeof...(Cs)> {};
template <size_t I, typename... Ts, size_t... Cs>
struct std::tuple_element<I, Row<Table<Ts...>, std::index_sequence<Cs...>>> {
static constexpr std::array indexes = { Cs... };
using type = Ts...[indexes[I]]&;
};template<size_t... Is, typename Table, size_t... Cs>
auto select(const Row<Table, std::index_sequence<Cs...>>& r) {
static constexpr size_t columns[] { Cs... };
return Row<Table, std::index_sequence<columns[Is]...>>(r);
}
drop_if(values, [](auto r){ auto [x,z] = select<0, 2>(r); return x < z; });
template<typename R, size_t... Cs>
struct range_selector {
using r_iterator = decltype(std::declval<R&>().begin());
struct iterator : r_iterator {
using difference_type = ssize_t;
using value_type = decltype(select<Cs...>(*std::declval<r_iterator>()));
iterator(const r_iterator& i) : r_iterator(i) {}
auto operator*() const {
const r_iterator& i = *this;
return select<Cs...>(*i);
}
};
iterator begin() { return iterator{ r.begin() }; }
auto end() { return r.end(); }
R& r;
};
template<size_t... Cs>
struct range_selector_maker {
template<typename R>
friend range_selector<R, Cs...> operator|(R& r, range_selector_maker) {
return { r };
}
};
template<size_t... Cs>
range_selector_maker<Cs...> select() { return {}; }for (auto [x, d] : values | select<0, 3>()) {
std::println("d={} x={}", d, x);
}template<typename>
inline constexpr bool row_type_v = false;
template<typename T, typename Cs>
inline constexpr bool row_type_v<Row<T, Cs>> = true;
template<typename T>
concept row_type = row_type_v<T>;template<size_t... Cs, typename F>
auto select(F&& f)
requires (! row_type<std::remove_cvref_t<F>>)
{
return [f = std::forward<F>(f)]<typename T, typename Is>(row<T, Is> r) {
return f(select<Cs...>(r));
};
}
template<typename F>
auto apply(F&& f) {
return [f = std::forward<F>(f)]<row_type R>(R r) {
return std::invoke([&]<size_t... Cs>(std::index_sequence<Cs...>) {
return f(get<Cs>(r)...);
}, std::make_index_sequence<std::tuple_size_v<R>>{});
};
}drop_if(values, select<0, 2>(apply([](auto x, auto z) { return x < z; })));drop_if(values, select<0, 2>(apply(std::less{})));
Nov 22, 2024
[C++] atomic::fetch_max
Reference:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0493r3.pdf
https://en.cppreference.com/w/cpp/atomic/atomic/fetch_max
GPU platforms added instructions for this many years ago. Most platforms do not have an instruction (yet?).
