Showing posts with label cpp_iso. Show all posts
Showing posts with label cpp_iso. Show all posts

May 27, 2026

[C++] pointer arithmetic

Refernece:
[C++] Object Lifetimes reading minute
class Vec {
  public:
    double* data() { return &x; }

  private:
    double x,y,z;
};

Eigen::Map<...>(absl::Span<Vec>);

*(Vect::data() + 1) // Does not give us y

Take away: 

* Even though x, y, and z are allocated sequentially in memory without padding,
physical layout does not supersede semantic rules.
* The layout guarantees mean you can safely memcpy the data, or cast a Vec* to a double* to access the first element (x). It does not grant permission to use pointer arithmetic on double* to slide across the members.
* The pointer arithmetic is only guaranteed within the type of array.
* Pointer to variable only is considered as pointer to array of size 1.
* Thus any pointer arithmetic on single variable is considered out-of-bound; compiler is free to assume anything.

Explain:

Only char*, unsigned char*, and std::byte* are explicitly granted an exception in the standard to 
examine the raw object representation. double* enjoys no such privilege.

Fix:

class Vec {
 public:
  double* data() { return data_; } // Legal: returns pointer to element 0 of a 3-element array
 private:
  double data_[3]; // x=data_[0], y=data_[1], z=data_[2]
};

Mar 19, 2026

[C++] uintN_t guaranteed to be exactly N bits

uintN_t is guaranteed to be exactly N bits with no padding if it exists, so 

sizeof(uint8_t) != sizeof(int32_t)

is guaranteed, if they both exist.

Nov 13, 2025

[C++] Value vs. Reference type conversion overload ranking tie breaker rule.

[C++] function overload resolution in one GIF (by Jeff Preshing)

there's a missing part related to binding to value or reference.

  • Value vs. Reference:
    Creating conversion operators for both T and T& creates ambiguity because the compiler views obtaining a value directly and obtaining a reference to copy as equally valid.
  • Reference vs. Reference:
    Creating operators for T& and T&& avoids ambiguity because the C++ standard has specific tie-breaking rules to rank one reference type over the other.
C++ ISO Overloading rules link:

Failed:
#include <type_traits>

struct Any {
    template <class T> operator T();
    template <class T> operator T&();
};

// ✅ This passes
// The compiler specifically looks for a reference, so it chooses operator T&()
static_assert(std::is_convertible_v<Any, int&>); 

// ❌ This FAILS (Ambiguous)
// The compiler cannot decide between:
// 1. calling operator T() to get an int
// 2. calling operator T&() to get an int& (which creates an int via copy)
//
// static_assert(std::is_convertible_v<Any, int>);

Success:
struct Any {
    template <class T> operator T&();
    template <class T> operator T&&();
};

// ✅ This passes!
// Unlike the previous example, the compiler doesn't view this as ambiguous.
// Because 'Any' is treated as an rvalue here, the standard prefers the 
// operator T&&() (rvalue reference) binding over the T&() binding.
static_assert(std::is_convertible_v<Any, int>);


When building the list of candidates, C++ considers deleted constructors 
(and then errors or substitution-fails if the candidate is selected).

In our case, before it can select a candidate, there's an ambiguity between selecting 
T& and the copy constructor or T&& and the move constructor.

Failed:
struct Any {
    // The two conversion operators from the previous slide
    template <class T> operator T&();
    template <class T> operator T&&();
};

struct MoveOnly {
    // Declaring a user-defined move constructor causes the 
    // copy constructor to be implicitly deleted.
    MoveOnly(MoveOnly&&); 
};

// ❌ This FAILS (Ambiguous)
//
// Even though the Copy Constructor is deleted, the compiler considers it
// a "candidate" during overload resolution. 
//
// The compiler sees two ways to turn 'Any' into 'MoveOnly':
// 1. Call Any::operator T&()  -> Then call MoveOnly(const MoveOnly&) [Copy]
// 2. Call Any::operator T&&() -> Then call MoveOnly(MoveOnly&&)      [Move]
//
// C++ rules state that different user-defined conversion sequences are 
// not comparable. The compiler cannot determine which path is "better,"
// so it errors out with "ambiguous conversion" before noticing the copy
// constructor is deleted.
//
// static_assert(std::is_convertible_v<Any, MoveOnly>);

Success:
This solution works by using const-correctness rules as a tie-breaker.
Here is the step-by-step breakdown of why this specific change fixes the ambiguity:
1. The Setup
When you perform std::is_convertible_v<Any, MoveOnly>, you are effectively creating a temporary (rvalue) Any object and trying to turn it into a MoveOnly. Importantly, this temporary Any object is mutable (non-const).

2. The Two Candidates
The compiler looks at the two conversion operators to see which one fits the Any object better. This comparison happens on the implicit object parameter (the this pointer).

Candidate A (operator T&() const): To call this function, the compiler must treat the non-const Any object as const. In C++ terms, this requires a qualification conversion (adding const).

Candidate B (operator T&&()): To call this function, the compiler uses the Any object exactly as it is (non-const). No qualification conversion is needed.

3. The Tie-Breaker
C++ overload resolution rules state that an exact match is better than a match that requires adding const.

Because Candidate B matches the "const-ness" of the object perfectly, it is strictly considered a better function than Candidate A.

4. The Result
Since Candidate B is strictly better, Candidate A is discarded entirely.
The compiler picks only operator T&&().
operator T&&() returns an rvalue reference (T&&).
This rvalue reference perfectly matches the MoveOnly(MoveOnly&&) constructor.
The code compiles successfully, avoiding the deleted copy constructor entirely.

struct Any {
    template <class T> operator T&() const;
    template <class T> operator T&&();
};

struct MoveOnly {
    // Declaring a user-defined move constructor causes the 
    // copy constructor to be implicitly deleted.
    MoveOnly(MoveOnly&&); 
};

static_assert(std::is_convertible_v<Any, MoveOnly>); // ✅

struct Immovable { Immovable(Immovable&&) = delete; };
static_assert(std::is_convertible_v<Any, Immovable>); // ❌

This works due to
Template Argument Deduction for Conversion Functions In C++.
When a template is used as a conversion operator, the template parameter T is deduced from the type that is required by the context. 

This is the opposite of a normal function template where T is deduced from the arguments passed in. 
  • The Rule: This is officially called Template argument deduction - conversion function (defined in the C++ standard under [temp.deduct.conv]). 
  • The Process: The compiler sees that it needs a MoveOnly object. It looks at Any and finds the conversion templates.
    It attempts to match T such that the result of the operator is compatible with MoveOnly. It deduces T = MoveOnly.

Jul 21, 2025

[C++] P1787R6: Declarations and where to find them

P1787R6: Declarations and where to find them


https://timsong-cpp.github.io/cppwp/n4868/temp.local#7

https://timsong-cpp.github.io/cppwp/n4950/temp.local#7


https://godbolt.org/z/hrE3YEzPd

<source>:12:53: error: 'Write' is not a type

#include <cstdint>

class Foo {
  template <typename Write>
  void WriteNestedMessage(uint32_t field_number, Write write_message);

 protected:
  void Write();
};

template <typename Write>
void Foo::WriteNestedMessage(uint32_t field_number, Write write_message) {}

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

  1. storage is allocated
  2. object is initialized, the lifetime starts
  3. object is used, its value changed or read.
  4. object is destroyed, the lifetime ends.
  5. storage is deallocated.
Object can be created: This does not necessarily start the lifetime yet.
Object can be destroyed: This ends the lifetime.

The lifetime of an object of type T begins when

  1. storage with the proper alignment and size for type T is obtained, and
  2. 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

  1. binding a reference to a prvalue
  2. member-access on a prvalue
  3. using an array prvalue
  4. discarding the result of a function call that returns a prvalue.
Temporary objects are destroyed as the last step in evaluating the full-expression that contains the point where they were created.
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];
}

std::construct_at Creates and initializes the object on an allocated memory.
std::destroy_at Destroy the object on an allocated memory. Does not in charge of reclaiming the memory. It only calls the object's destructor. Its sole job is to end the lifetime of the object at that specific memory location, running any cleanup code defined inside the object's destructor
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);

UB:
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);

OK(because `ptr` itself is not const, and new with int(42), not const 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
When replacing sub-objects, (member variables or array elements), the rules apply
recursively to the parent object.
// 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  

  1. launder is for /previous/ object, not the new one. Compiler always give out right value for new one. 
  2. 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)); // ok
Ref: P3006/Launder less 


Implicit create object(and initialize it.)

1) std::malloc and variants, ::operator new, std::allocator::allocate and other allocation functions.
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;

3) std::memcpy(does not handle memory overlap), std::memmove
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);

4) Implementation-defined set of operations like mmap or VirtualAlloc(a M$ thing).

To evaluate this code, the C++ Abstract Machine relies on hindsight. It says:
If the runtime condition evaluates to true, the program attempts to use an int. Therefore, the array retroactively "always contained an int" to keep the program valid.
If the condition evaluates to false, the program attempts to use a float. Therefore, the array retroactively "always contained a float".
Because creating a trivial object (like an int or float) requires exactly zero CPU instructions, the physical computer doesn't actually have to do anything when an object is implicitly created. It's purely a compile-time bookkeeping rule.
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;

Only unsigned char or std::byte can be cast to other type.
// 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.
}

So how is start_lifetime_as implemented?
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

  1. if T is a non-class type, the object is destroyed, or
  2. if T is a class type, the destructor call starts, or
  3. 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

  1. Each object has a unique provenance.
  2. All objects in an array have the same provenance.
  3. 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)

  1. The address is the only thing that is physically observable.
  2. The provenance identifies to the object of allocation the pointer was derived from.

A pointer dereference is only valid if

  1. The address is in the range of allowed addresses for the provenance.
  2. The current provenance of that address is the same as the provenance of the pointer.

The pointer provenance cannot be changed using pointer arithmetic.

Thus e.g.
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 but
dereferencing 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); // UB

Be 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 P3006


When to use std::launder?

When want to re-use the storage of

  1. const heap objects; const object cannot be fixed. Once it's const, it's const for life.
  2. base classes
  3. [[no_unique_address]] members
  4. Or when re-using memory as storage for a different type.

There are exceptions for dereferencing from reinterpret_cast with different types.

i.e.
If a program attempts to address the stored value of an object through a glvalue whose type is not similar to one of the following types the behavior is undefined:
  1. the dynamic type of the object,
  2. a type that is the signed or unsigned type corresponding to the dynamic type of the object, or
  3. 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)); // ok



Object representation

Allow access to the object representation, the sequence of bytes the object represents in memory.

Code below currently doesn't work but fixed in p1839.
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

std::bit_cast (introduced in C++20) is a compile-time safe, ultra-fast way to reinterpret the bits of one object as another type.

Think of it as the modern, type-safe replacement for old-school tricks like reinterpret_cast or using a union to pun types, or using std::memcpy to copy bytes between two variables.

Unlike std::start_lifetime_as, which lets you point to a buffer and re-interpret

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

If two objects are pointer-interconvertible, then they have the same address,
and it is possible to obtain a pointer to one from a pointer to the other via a
reinterpret_cast.

Two objects a and b are pointer-interconvertible if
  •  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.
If two objects are pointer-interconvertible, then they have the same address, and it is possible to obtain a pointer to one from a pointer to the other via a reinterpret_cast

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); // ok


Union

Even though the data in the union is assigned, it is valid to access the unassigned data
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)];





Jan 31, 2025

[C++] transparently replaceable

https://eel.is/c++draft/basic.life#9

An object o1 is transparently replaceable by an object o2 if 

(9.1) the storage that o2 occupies exactly overlays the storage that o1 occupied,

(9.2) o1 and o2 are of the same type (ignoring the top-level cv-qualifiers)

(9.3) o1 is not a const, complete object

(9.4) neither o1 nor o2 is a potentially-overlapping subobject ([intro.object])

(9.5) either o1 and o2 are both complete objects, or o1 and o2 are direct subobjects of objects p1 and p2 , respectively, and p1 is transparently replaceable by p2 .


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:
(Use std::launder if need to reuse the address, i.e. const heap objects)

  • const objects; const heap objects for using std::launder
  • base classes
  • [[no_unique_address]] members

When replacing subobjects, (member variables or array elements), the rules apply
recursively to the parent object.


Thus, corner case of std::optional<T>:

If optional<T> holds its T subobject using a [[no_unique_address]] member (in order to pack its bool into the tail padding of T), then you can't use a pointer to the old object to transparently point to the new one.

Dec 17, 2023

[C++] all expressions that are references to functions are lvalues

A special rule in the C++ standard states that all expressions that are references to functions are lvalues.

Thus we can bind a non-const lvalue reference to a function marked with std::move() because a function marked with std::move() is still an lvalue.

Reference:

Special Rule for Casting to Rvalue References to Functions: 

The C++ standard has a specific rule regarding the value category of a cast expression when the target type is an rvalue reference to a function type. According to the standard, a cast expression to an lvalue reference type or an rvalue reference to a function type results in an lvalue.

This is a key exception to the general rule that casting to an rvalue reference to an object type yields an xvalue.

Why this special rule?

The concept of "moving" is primarily applicable to objects that own resources (like dynamic memory). Moving an object involves transferring ownership of these resources from one object to another, often leaving the source object in a valid but empty or default state. This is a meaningful operation for objects where copying is expensive.

Functions, on the other hand, do not own such resources that can be transferred in this manner. A function's identity is its entry point in memory, which is fixed. There's no state within a function itself that can be "moved" to another location or entity in the way an object's data members might be.

Therefore, casting a function to an rvalue reference to a function doesn't enable any form of resource transfer or change the fundamental lvalue nature of the function itself. The C++ standard reflects this by specifying that such a cast still results in an lvalue. The expression still refers to the same identifiable function entity.
 

void f(int) {}

void(&fref1)(int) = f; // fref1 is an lvalue
void(&&fref2)(int) = f; // fref2 is also an lvalue

auto& ar = std::move(f); // OK: ar is lvalue of type void(&)(int)

Dec 8, 2022

[C++/Rust] use of thread_local in code.

Reference:
  1. All about thread-local storage by MaskRay
  2. A Deep dive into (implicit) Thread Local Storage; in detail about use cases for thread_local.
  3. ELF Handling For Thread-Local Storage by Ulrich Drepper
  4. clang attribute 'tls-model'
  5. How fast is thread local variable access on Linux
  6. Mastering x86 Memory Segmentation
  7. x86 and amd64 instruction reference

This note is focused on C++ coding practice with thread_local; knowledge are collected from daily engineering and references above.


C++ Language definitions:
  1. Zero-initialization
    https://en.cppreference.com/w/cpp/language/zero_initialization
    https://vsdmars.blogspot.com/2014/04/c11-zero-initialisation-for-classes.html
  2. Constant initialization
    https://en.cppreference.com/w/cpp/language/constant_initialization
    Init. Rule memorize: C.Z , Constant first if possible, then Zero init.
  3. constinit specifier
    https://en.cppreference.com/w/cpp/language/constinit
  4. Potentially-evaluated expressions
    https://en.cppreference.com/w/cpp/language/expressions#Potentially-evaluated_expressions
  5. [C++20] consteval / constexpr
    https://vsdmars.blogspot.com/2022/06/cc20-consteval-constexpr.html


  6. The thread_local keyword is only allowed for objects declared at namespace scope, objects declared at block scope, and static data members.
    It indicates that the object has thread storage duration.
    If thread_local is the only storage class specifier applied to a block scope variable, static is also implied.
    It can be combined with static or extern to specify internal or external linkage (except for static data members which always have external linkage) respectively.
    It can be combined with constinit to reduce overhead that would otherwise be incurred by a hidden guard variable. (i.e. static is thread safe guarded)
  7. thread storage duration. The storage for the object is allocated when the thread begins and deallocated when the thread ends. Each thread has its own instance of the object(i.e. clone()). Only objects declared thread_local have this storage duration. thread_local can appear together with static or extern to adjust linkage.
  8. thread_local is init. ordered in C.Z; i.e first init. with const-init; if can't, do zero-init.
When variable decorated with static or thread_local it will be constant initialized if possible than a runtime zero initialization. [[basic.start.static]]
i.e.
#include <iostream>
using namespace std;

bool runtimeFunc() {
  return std::is_constant_evaluated(); // always false
}

constexpr bool constexprFunc() {
  return std::is_constant_evaluated(); // may be false or true
}

consteval bool constevalFunc() {
  return std::is_constant_evaluated(); // always true
}

void foo() {
  static bool v1 = constexprFunc();       // T

  // implicit static
  thread_local bool v2 = constexprFunc(); // T
  thread_local bool v3 = constevalFunc(); // T

  int y = 42;
  static int v4 = y + runtimeFunc();         // 42
  static int v5 = y + constexprFunc();       // 42
  static int v6 = y + constevalFunc();       // 43

  // implicit static
  thread_local int v7 = y + runtimeFunc();   // 42
  thread_local int v8 = y + constexprFunc(); // 42
  thread_local int v9 = y + constevalFunc(); // 43
}

int main() { foo(); }

Usage
  • thread_local should not be used in signal handler; while signal handler can be called in different threads, thus the fact that thread_local is not sync between threads can introduce buggy logic.
  • thread_local is relatively slow in DSO use cases, use local caching instead.
        1 instruction in Windows, Linux
        3-4 in OSX
  • in dlopen; DSO interacts with thread_local as follows:
    • When a thread starts(i.e. clone()), init. thread_local objects with thread storage duration at namespace scope.
      When a thread exits, destruct objects with thread storage duration.
  • What happens if the library is unloaded before all threads exit?
    • In glibc, use RTLD_NODELETE, this will have DF_1_NODELETE set in ELF, thus does not unload the shared object during dlclose().
    • Consequently, the object's static and global variables are not reinitialized if the object is reloaded with dlopen() at a later time.
    • Also,  dlclose() in the middle of destructing thread_local objects is a no-op when RTLD_NODELETE is used.
    • Use cases for thread_local in DSO can be slow due to __tls_get_addr@plt to get the address of the thread_local variable out of the DSO.
  • Thread local variables should not be used in coroutines to prevent buggy logic.
    https://rules.sonarsource.com/cpp/RSPEC-6367
    If you have to use thread local inside a signal handler function, read:
    https://vsdmars.blogspot.com/2025/11/c-avoid-compiler-reordering-statements.html (std::atomic_signal_fence)




Dec 12, 2017

[C++] defining a namespace inside std is undefined behavior

Defining a namespace inside std is undefined behavior.

Reference:
cppref extending_std

§17.6.4.2.1
The behavior of a C++ program is undefined if it adds declarations or definitions to namespace std or to a namespace within namespace std unless otherwise specified.
A program may add a template specialization for any standard library template to namespace std only if the declaration depends on a user-defined type and the specialization meets the standard library requirements for the original template and is not explicitly prohibited.
[...] A program may explicitly instantiate a template defined in the standard library only if the declaration depends on the name of a user-defined type and the instantiation meets the standard library requirements for the original template.