P3125R6: constexpr pointer tagging
This paper proposes a new library non-owning pointer and value pair which has ability to store small amount of information in a tag in alignment low-bits. This functionality is also usable in constexpr environment.
Acknowledgement
I want to thank to everyone who helped me by reviewing this paper. I want to thank especially Tomasz Kamińsky who made the wording actually make sense.
Revision history
- R5 → R6: added section discussiong
tptr.tagged_pointer()andpointer_tag_pair::from_tagged(...)impact of invalid pointers on portability and existing architectures, and how it is mitigated. Added support fornullptrin wording. Added precondition against pointers pointing past the end of an object. Constructors are now limiting what pointer types can be used (avoiding non-first base class and virtual inheritance). Added remark tomax_pointer_bits_availablewhat non-zero value means for the implementation. During review of draft of this R6 the functionpointer_bits_availablewas added and removed quarantee of min 1 bit available for tagging (always). Changed order of template arguments topointer_tag_pair<class PtrT, unsigned BitsRequested, typename TagT>(previouslypointer_tag_pair<PtrT, TagT, BitsRequested>). Added exposition only concepts, functions, and constants to make specification simpler. Added section discussing comparing tags.Added section explaining whyBitsRequestedand notBits. Added section explaining the necessity of all static checks and preconditions. Comparison operators are now conditionaly present. Fix mistake about CHERI pointers and casting to/fromuintptr_t. Added information what it would take to allow past-the-end-of-an-object pointer values. - R4 → R5: updated rest of paper to reflect changes of wording in R4 (changing pointee to pointer), reintroduce design section which was basically example implementation, this time updated with current wording, add subsection about having pointee or pointer, which LEWG should decide, wording uses pointer instead of pointee.
- R3 → R4: changes requested by LEWG (removed
nullptr_tconstructor, added support forvoid *, first template argument is now pointer and not pointee, addedpointer_tag_traitto inspect available bits inside pointers, addedtuple_size,tuple_element, andget<N>(pointer_tag_ptr)) - R2 → R3: polishing design and added wording
- R1 → R2: simplifying design by removing schemas and the heavy interface, massive simplification as discussed in SG1 (
pointer_tag_pair, requiredsizeof(pointer_tag_pair<T *, n, tag_t>) == sizeof(T*)) - R0 → R1: proposing and explaining design based on existing implementation
Introduction and motivation
Pointer tagging is widely known and used technique (Glasgow Haskell Compiler, LLVM's PointerIntPair, PointerUnion, CPython's garbage collector, Objective C / Swift, Chrome's V8 JavaScript engine, GAP, OCaml, PBRT). All major CPU vendors provides mechanism for pointer tagging (Intel's LAM linear address masking, AMD's Upper Address Ignore, ARM's TBI top byte ignore and MTE memory tagging extension). All widely used 64 bit platforms are not using more than 48 or 49 bits of the pointers.
This functionality widely supported can't be expressed in a standard conforming way.
Rust, Dlang, or Zig has an interface for pointer tagging. This is demonstrating demand for the feature and C++ should have it too and it should also work in constexpr.
Generally programmer can consider low-bits used for alignment to be safely used for storing an information. Upper bits are available on different platforms under different conditions (runtime processor setting, CPU generation, ...). This proposal only proposes interface to accessing low bits for storing / reading a tag value, not observing bits of a pointer.
This proposal doesn't propose accessing any other bits other than low-bits which are known to be zero due alignment. SG1 doesn't want to standardize access to high-bits as it's considered dangerous and non-portable. And can limit future development of security features in OS and CPUs for which high-bits are used.
Use cases
There are three basic use-cases of pointer tagging:
- marking pointer with an information (in allocators, used as a tiny refcount, or marking source of the pointer)
- pointee polymorphism (usually in data structures, eg. in trees: next node can be an internal node or a leaf)
- in-place polymorphism (similarly as variant, some bits stores an information about rest of payload, it can be a pointer, a number, a float, a small string, ...)
This paper aims to solve only first two use-cases.
Safety
Pointer tagging can be currently implemented in C++ only with reinterpret_cast and bit manipulating with shifts / bitand / bitor and this approach is prone to be unsafe and hard to debug as it's not expressed clearly in code what is the right intention, so compiler can't even diagnose incompatibility between encode/decode. By giving a name to this tool it allows programmer to express intent clearly and compiler to optimize and diagnose problems properly.
Preconditions and mandates of proposed std::pointer_tag_pair makes it unlike to use it unsafely as potentially dangerous operations (std::pointer_tag_pair<Pointer, Bits, Tag>::from_tagged(pointer) and std::pointer_tag_pair<Pointer, Bits, Tag>::template from_overaligned<PromisedAlignment>(pointer)) are verbose and visible.
Unsafe operations
Two mentioned functions are provided so user can explicitly provide over-aligned pointer which would otherwise won't be compatible with number of requested bits or interact with existing functionality for pointer tagging (which will allow gradual adoption of the feature and replace old code).
Examples
HAMT early leaves
Following example is a recursive search implementation for a HAMT (hash-array-mapped-trie) data structure. Where a tag value indicates leaf node.
// requesting only 1 bit of information
using hamt_node_pointer = std::pointer_tag_pair<const void *, 1, bool>;
static_assert(sizeof(hamt_node_pointer) == sizeof(void *));
constexpr const T * find_value_in_hamt(hamt_node_pointer tptr, uint32_t hash) {
if (tptr == nullptr) // checks only pointer part
return nullptr;
if (tptr.tag()) // we found leaf node, tag is boolean as specified
return *static_cast<const T *>(tptr.pointer());
const auto * node = static_cast<const internal_node *>(tptr.pointer());
const auto next_node = node[hash & 0b1111u];
return find_value_in_hamt(next_node, hash >> 4); // recursive descend
}
Smart (non-)owning pointer
This example shows maybe_owning_ptr type which can be both a reference or an owner:
template <typename T> class maybe_owning_ptr {
enum class ownership: unsigned {
reference,
owning,
};
std::pointer_tag_pair<T *, 1, ownership> _ptr;
public:
constexpr maybe_owning_ptr(T* && pointer) noexcept: _ptr{pointer, ownership::owning} { }
constexpr maybe_owning_ptr(T & ref) noexcept: _ptr{&ref, ownership::reference} { }
constexpr decltype(auto) operator*() const noexcept {
return *_ptr.pointer();
}
constexpr T * operator->() const noexcept {
return _ptr.pointer();
}
constexpr ~maybe_owning_ptr() noexcept {
if (_ptr.tag() == ownership::owning) {
delete _ptr.pointer();
}
}
};
static_assert(sizeof(maybe_owning_ptr<int>) == sizeof(int *));
LLVM's L-value
Following code is simplification of LLVM's representation of pointer/reference type which is implemented with LLVM's PointerIntPair and is inside constant evaluator. This type holds subnodes of different kind: pointer to local/static variable, dynamic allocation, type info pointer, result of temporary.
struct LValueBase {
using PtrTy = llvm::PointerUnion<const ValueDecl *,
const Expr *,
TypeInfoLValue,
DynamicAllocLValue>;
struct PathEntry {
uint64_t value;
};
using PathTy = std::vector<PathEntry>;
PtrTy Location;
PathEntry SubObjectPath;
};
APValue DereferencePointer(EvalInfo & Context, const LValueBase & Base) {
auto & object = Context.Visit(Base.Location);
return object.NavigateToSuboject(Base.SubObjectPath);
}
Invalid pointers
Jens had concerns about fact that function .tagged_pointer() returns technically almost always an invalid pointer value [basic.compound]. In order to understand why this can feel concerning, one must understand what it means to have an invalid pointer value according C++. So to reiterate that, an invalid pointer value:
- can't be dereferenced or deallocated, as both these operations results in explicit undefined behaviour [basic.compound],
- all other operations including the l-value to r-value conversion is implementation defined [conv.lval] and on some architectures can result in a page-fault as stated in a note in [basic.types].
Obviously the pointer resulting from .tagged_pointer() is not meant to be dereferenced or deallocated. It's there only to be able to be passed thru C and legacy interfaces (without depending on reinterpret_cast). And only safe usage for it is to be passed into ::from_tagged(...) for us to reconstruct original pointer_tag_pair and then with .pointer() access the original pointer.
auto tptr = std::pointer_tag_pair<T *, 1, bool>{&object, true};
void * opaque = tptr.tagged_pointer(); // you got an invalid pointer value, and it's fine
// example of problems:
*opaque; // you can't dereference `void *` anyway
*static_cast<T *>(opaque); // UB, dereferencing invalid pointer value,
// it may not point to a valid object due tagging, obviously
// example why `void *` is needed
curl_easy_setopt(handle, CURLOPT_PRIVATE, opaque); // implementation-defined lvalue-to-rvalue conversion
// later...
char * ptr;
curl_easy_getinfo(handle, CURLINFO_PRIVATE, &ptr); // CURL uses char** for internal reasons
// ptr is now invalid pointer value too,
// any manipulation with it is an implementation-defined behavior
// following function takes always `void *` so it is converted implicitly from char *
auto tptr2 = std::pointer_tag_pair<void *, 1, bool>::from_tagged(ptr); // implementation-defined lvalue-to-rvalue conversion
// if the implementation pass value
// identical `ptr` then `tptr2.pointer()` will yield valid pointer
assert(tptr2.pointer() == object);
assert(tptr2.tag() == true);
Why they are not problematic for tagged pointers
The term invalid pointer value is somehow generous, it can mean any value which is not pointing to an object, past-the-object, or is nullptr [basic.types]. Obviously a raw pointer containing tagged pointer with non-zero tag value won't point to its original object, hence it is an invalid pointer value. But these tagged pointer of non-invalid pointers has specific properties, and it makes them a subset of all possible invalid pointer values.
Segmented architecture
Loading a segmented pointer into a register sometimes result in CPU trying to load pointed memory page. This means if the implementation of pointer_tag_pair is using only alignment bits, user needs to request more bits than page alignment allows:
This can be easily solved by setting an implementation specified limit how many bits user can request for the pointer tagging.
Pointer signing / authentication
Some architectures are using upper free bits (alignment bits are untouched) for pointer signing, which usually takes into account pointer's own location in memory, and its value (and hash function / nonce, depending on its implementation). Providing proposed pointer_tag_pair gives ability to user to express what it's happening, and implementations can provide facility which will keep valid signature even thru .tagged_pointer(). The tagged raw pointer still can't be dereferenced or deleted, and its use is only to be put back to pointer_tag_pair, and then the original pointer can be recovered, all without breaking the chain of trust. This is giving us another motivational example why we need the proposed facility, because normal (or existing) pointer tagging approaches thru uintptr_t loos the pointer authentication.
CHERI architecture
CHERI architecture uses wider pointers, which contains multiple information, but most importantly a base address, offset, available range, and capability bits (RISC-V Specification for CHERI Extensions). In a sense they are similar to internal representation of pointers in compiler during constant evaluation.
Section 2.10.1. Capability Encoding of the linked document it shows CHERI pointers are of size also it shows existence of two bits marked SDP (Software-Defined Permission). These two bits are available to be cleared by an applications, which means once these bits are cleared, there is no valid way how to set them back up, so these are not usable for this proposal.
It is possible to cast CHERI pointers to/from uintptr_t (thanks David Chisnall for correcting me here).
Fortunately in the CHERI C/C++ programming guide you can find functions to manipulate pointers as a structure, like void * cheri_address_set(void *, ptraddr_t). This allows you to rederive new pointer from existing one respecting its provenance. This means new pointer must be within boundary allowed to access from an existing pointer. This means a pointer with tagging can't go beyond past-the-end-of-an-object pointer, which leaves us with at least one bit usable for pointer tagging (with alignment larger than 1).
Proposed limits for pointer tagging
Based on informations from previous subsections, this paper proposes setting two implementation-defined limits: first based on memory page alignment size and second based on pointer provenance with guaranteed size at least one for pointers aligned by two or more. Also tagged pointer must be only valid pointer to object, not the past-the-end of an object.
Impact on core wording around invalid pointer values
This paper doesn't need any change in core wording around behavior of invalid pointers, it's still implementation defined, the two functions (.tagged_pointer() and ::from_tagged(...)) essentially becomes invalid pointer factories. But with mentioned newly impossed limits on implementations, these will be safe.
Implementation experience
Old version of this proposal has been implemented within libc++ & clang and it is accessible on github and compiler explorer. This functionality can't be implemented as a pure library (reinterpret_cast is not allowed during constant evaluation) and needs compiler support in some form.
Implementation in the library
Library is providing a special pair-like type containing pointer and small tag type, and user requested number of bits. The number of bits is by default deduced by default alignment of requested pointee type. Requesting more bits than alignment will disable normal constructor, and force user to use ::from_overaligned function as overalignment is not part of C++'s type system.
In terms of library design there is nothing surprising, and it's pretty straightforward wrapper which encode and decode tagged pointer on its boundaries and provides basic pointer functionality.
Accessing raw tagged pointers
The pointer int pair has support to access raw tagged pointer. Which is a pointer which can't be dereferenced or otherwise manipulated with, it's an opaque value usable only with legacy tagging interface or it can be used to construct pointer_tag_pair back from it. Existence of this interface allows ability to store such pointers in existing interfaces (atomic, other smart pointers). Question is if it should be uintptr_t or void*. I prefer void* as roundtriping thru an integer looses information about provenance and can disable some optimization.
Implementation in the compiler
The implementation is providing builtins to manipulating raw pointers and isn't meant to be used by end-users, only to allow this library functionality.
Compiler builtins
Implementation needs to manipulate pointers without casting them to integers and back. To do so the provided set of builtins is designed to store/load a value (with size of few bits) into/from unimportant/unused bits of a pointer without observing actual pointer representation.
Constant evaluation
With these builtins it's trivial to implement semantically identical behaviour for the constant evaluation. In case of my implementation, pointers in clang are not represented as addresses but as symbols (original AST variable, allocation, static object + path to subobject, its provenance) and there is no address to manipulate. Actual tag value in such "pointer" is then stored in a metadata of the pointer itself and builtins only provide access to it. Technically such storage can provide more bits than pointer "size", but there are internal checks which make sure it allows only bits which would be accessible in runtime based on alignment of individual pointer.
Any attempt to deference or otherwise manipulate such pointer, which would be unsafe in runtime, is detected and reported by the interpreter. Only the provided builtins can recover original pointer and tag value.
Pointer provenance and optimization
Easiest way to implement builtins for pointer tagging is to do the same thing reinterpret_cast is doing, which was my first implementation approach. But this approach leads to loosing pointer's provenance and compiler loosing information which otherwise should be accessible for optimizer to use.
For unmasking there is already ptr.mask LLVM's builtin, but there is no similar intrinsic to do the tagging. Hence the builtins needs to interact with backend and be implemented with a low level backend intrinsic to do the right thing. This shows how actually unimplementable pointer tagging is in existing language.
Alternative constexpr compatible implementation
Alternative way to implement constexpr support (for compiler which don't have heavy pointer representation in their interprets) is inserting a hidden intermediate object holding the metadata and pointer to original object. This allows exactly same semantic as the metadata approach, and can be completely implemented in library using if consteval, but it will need allocation during constant evaluation.
Design
The std::pointer_int_pair is simple pair-like template providing only necessory interface and is not meant to provide heavy interface as it's preferable to not hide pointer tagging / untagging from users. This is mean to be a low-level facility. Main requirement on the type is it must be always same size as stored pointer and not more.
constexpr unsigned max_pointer_bits_available = std::countr_zero(4096u); // tagging must never overflow page alignment
constexpr unsigned pointer_bits_available(size_t alignment) noexcept {
assert(std::popcount(alignment) == 1);
return std::min(std::countr_zero(alignment), max_pointer_bits_available);
}
// helper
template <typename T> struct copy_cv {
template <typename Y> using result_t = Y;
};
template <typename T> struct copy_cv<const T> {
template <typename Y> using result_t = const Y;
};
template <typename T> struct copy_cv<volatile T> {
template <typename Y> using result_t = volatile Y;
};
template <typename T, typename Y> using copy_cv_t = copy_cv<T>::template result_t<Y>;
template <typename Ptr, typename TagT, unsigned BitsRequested = pointer_bits_available(alignof(std::pointer_traits<Ptr>::element_type))>
class pointer_tag_pair {
static_assert(std::is_same_v<remove_cvref_t<Ptr>, Ptr>);
static_assert(std::is_same_v<remove_cvref_t<TagT>, TagT>);
static_assert(std::is_pointer_v<Ptr>);
static_assert(!std::is_function_v<remove_pointer_t<Ptr>>);
using UT = std::conditional_t<std::is_enum_v<TagT>, std::underlying_type_t<TagT>, T>;
static_assert(std::is_unsigned_t<UT>);
static_assert(sizeof(TagT) <= sizeof(void*));
static_assert(BitsRequested <= max_pointer_bits_available);
public:
using pointer_type = Ptr;
using tagged_pointer_type = copy_cv_t<Ptr, void *>; // I prefer `void *` to avoid loosing provenance
using tag_type = TagT;
static constexpr unsigned bits_requested = BitsRequested;
private:
static constexpr uintptr_t tag_mask = (uintptr_t{1} << bits_requested) - 1u;
static constexpr uintptr_t pointer_mask = ~tag_mask;
Pointer internal_pointer;
public:
constexpr pointer_tag_pair() noexcept {
internal_pointer = nullptr;
}
pointer_tag_pair(const pointer_tag_pair&) = default;
pointer_tag_pair(pointer_tag_pair&&) = default;
pointer_tag_pair& operator=(const pointer_tag_pair&) = default;
pointer_tag_pair& operator=(pointer_tag_pair&&) = default;
~pointer_tag_pair() = default;
constexpr pointer_tag_pair(nullptr_t, tag_type tag)
{
assert(bit_width(tag) <= bits_requested);
__builtin_tag_pointer_mask_or(nullptr, static_cast<uintptr_t>(tag), tag_mask)
}
template <typename U>
constexpr pointer_tag_pair(U * ptr, tag_type tag)
requires(std::convertible_to<U*, pointer_type> && pointer_bits_available(alignof(U)) >= bits_requested): internal_pointer{
__builtin_tag_pointer_mask_or(ptr, static_cast<uintptr_t>(tag), tag_mask)
} {
assert(bit_width(tag) <= bits_requested);
}
template <unsigned PromisedAlignment, typename U>
requires(std::convertible_to<U*, pointer_type> && pointer_bits_available(PromisedAlignment) >= bits_requested)
static constexpr pointer_tag_pair from_overaligned(P ptr, tag_type t) {
assert(ptr == null || std::is_sufficiently_aligned<PromisedAlignment>(ptr));
assert(bit_width(t) <= bits_requested);
auto out = pointer_tag_pair{};
out.internal_pointer = __builtin_tag_pointer_mask_or(ptr, static_cast<uintptr_t>(tag), tag_mask);
return out;
}
template <unsigned PromisedAlignment>
static constexpr pointer_tag_pair from_overaligned(pointer_type ptr, tag_type tag)
requires(pointer_bits_available(PromisedAlignment) >= bits_requested)
{
auto out = pointer_tag_pair{};
out.internal_pointer = __builtin_tag_pointer_mask_or(ptr, static_cast<uintptr_t>(tag), tag_mask);
}
// Precondition: valid pointer if untagged
static pointer_tag_pair from_tagged(tagged_pointer_type ptr) { // no-constexpr
auto out = pointer_tag_pair{};
out.internal_pointer = ptr;
return out;
}
// access tagged pointer (for interaction with existing tagging mechanisms)
tagged_pointer_type tagged_pointer() const noexcept {
return reinterpret_cast<tagged_pointer_type>(internal_pointer);
}
// access untagged pointer
constexpr pointer_type pointer() const noexcept {
return static_cast<pointer_type>(__builtin_tag_pointer_mask(internal_pointer, pointer_mask));
}
// access tag value
constexpr tag_type tag() const noexcept {
return static_cast<tag_type>(__builtin_tag_pointer_mask_as_int(internal_pointer, tag_mask));
}
// swap
constexpr void swap(pointer_tag_pair& lhs) noexcept {
std::swap(internal_pointer, rhs.internal_pointer);
}
// comparing {pointer(), tag()} <=> {pointer(), tag()} for consistency
friend constexpr auto operator<=>(pointer_tag_pair lhs, pointer_tag_pair rhs) noexcept {
return std::tuple(lhs.pointer(), lhs.tag()) <=> std::tuple(rhs.pointer(), rhs.tag());
}
friend bool operator==(pointer_tag_pair lhs, pointer_tag_pair rhs) noexcept {
return lhs.pointer() == rhs.pointer() && lhs.tag() == rhs.tag();
}
};
Pointee or pointer
One question is if template argument of pointer_tag_type should be pointer or pointee. LLVM has design with a pointer, but this design is not symmetric with rest of standard library. Their design is to support wrapping other pointer-like types inside pointer_tag_type or support structure of a pointer_tag_type inside another pointer_tag_type.
I came to conclusion it should be pointer type, so in future we can maybe add other types.
Order of template arguments
Originally I thought order of template arguments: Pointer, Tag, BitsRequested will be best, but then I played with idea of making Tag defaulted to unsigned, as the parameter BitsRequested is already defaulted. And I think when user wants specific type as TagT then it must also know how many bits is really needed, but if user wants to have default TagT = unsigned, than it cares about BitsRequested. Hence the R6 of this paper switches the order.
Why BitsRequested
Design of this type is intended express of how many bits user needs to store, not how much pointer_type must have. There is a subtle difference:
// with requirement of bits the following instantiation of type would fail
// instead we communicate "we take pointer to char, which has 2 bits of space"
using overaligned_char_tptr = pointer_tag_pair<char *, 2>;
char a = ...;
alignas(4) char b = ...;
unsigned tag = ...;
auto pa = overaligned_char_tptr{&a, tag}; // fails to instantiate constructor only, `a` doesn't have enough alignment
auto pb = overaligned_char_tptr{&b, tag}; // fails too, overalignment is not part of type-system
// instead for over-alignment we know type doesn't provide guarantees,
// so it must be explicit operation...
// failure to provide over-aligned pointer is precondition violation
auto pb = overaligned_char_tptr::from_overaligned<4>(b, tag);
This subtle difference is a philosophical, the wrapper will instantiate almost always (unless you pass non-pointer type, ...), but its constructors will be enabled only if the construction is safe. The pointer_tag_pair must always return same pointer and tag which was passed into it, and this is archieved preferrably with static checks based on type, runtime checks (preconditions) are less-preferred and such construction must be explicitly visible.
What it would take to support pointer past-the-end-of-an-object?
Current design supports only valid pointer values (pointer to object, not past-the-end-of-an-object) to be used with normal constructors. To support such pointers on platform like CHERI, we would need to know size of the object pointed at, to calculate number of bits available precisely (size of the object can impact memory range the pointer can reach) and because the design also supports form pointer_tag_pair<void *, N> to which we can pass also overaligned void *, there is no type known from which we can deduce size of the pointee, and therefore operation at the pointer past the end can't be guaranteed (based on discussion with David Chisnall). And it would complicate the API. This change of the API (adding optional Size template parameters) is possible to be done later, if we decide to do it.
Theoretically size of object 0 could be used for a pessimistic calculation of bits when the information is not known, but this needs further research, and the API design allows to do this (I have designed, and then decided to roll it back).
Why all static checks and precondition checks on from_overaligned
The ::from_overaligned function has check if passed pointer type has alignment enough space, if pointer value is aligned properly (precondition check) and if tag value can bit fit the number of requested bits (precondition check). All three checks are important, removing any of them will result in possible dangerous situations, especially with mentioned ::from_overaligned function:
Type alignment static check
If the bits available from the alignment were not checked against passed pointer, type would be checked only in precondition if it is sufficiently aligned (as user provided template parameter of the same function) and user would be able to write:
using tptr_t = pointer_tag_pair<char *, 16>; // user naively ask for 16 bits
alignas(16) char c = ...; // align 16, not 2^16
// no failure on next line (if the static check is removed)
auto x = tptr_t::from_overaligned<16>(&c, 0b1111'1111'1111'1111u);
assert(x.pointer() != &c); // wrong pointer value
assert(x.tag() != 0b1111'1111'1111'1111u); // wrong tag value
Pointer value alignment precondition
If remove the precondition check for real pointer value alignment, user will be able to lie about alignment:
using tptr_t = pointer_tag_pair<char *, 16>; // user naively ask for 16 bits
char c = ...;
// no runtime failure on next line (user is lying!)
auto x = tptr_t::from_overaligned<(2 << 16)>(&c, 0b1111'1111'1111'1111u); // works on my machine #YOLO
assert(x.pointer() != &c);
assert(x.tag() != 0b1111'1111'1111'1111u);
Bit width of tag precondition
If bit width of tag value is not checked, user can easily damage the pointer:
using tptr_t = pointer_tag_pair<char *, 2>; // user wants this to work with any char
alignas(4) char c = ...;
// no runtime failure on next line (pointer has enough alignment for what is requested)
auto x = tptr_t(&c, 0x10u); // accidentally passing 0b1'0000 to space of two bits
assert(x.pointer() != &c); // wrong pointer value
assert(x.tag() != 0x10u); // wrong tag value
Comparison of tag values
Type of the tag can be an enumeration type, and such type can have deleted operator== and/or operator<=>. Supporting disabling of such comparison will result in need to do the decoding of pointer value and tag, before calling comparisons, but such requirement would result in unnecessory code for a quick comparison of internal raw value of the tagged pointer (for equality). This paper proposes an implementation-defined comparison operator for pointer_tag_pair unless comparison operator is user-defined.
Preconditions and eligibility of constructors
Constructor taking a pointer and tag value is only available if alignment of the pointee type is enough to store requested number of bits.
Both the constructor and ::from_overaligned function have preconditions checking if pointer is aligned enough as expected (in the constructor) or promised overaligned (in the ::from_overaligned function). In addition to this there is a precondition to make sure value of tag type is representible with RequestedBits.
Representation of tag value
Tag value can only be unsigned integral type or an enum type with underlying unsigned integral type. The value is converted to the underlying or kept original unsigned integral type and then it is bit masked with mask based on BitsRequested bits (1u << BitsRequested - 1u).
It's precondition failure for the value after this conversion different than original value. An attempt was made to support signed type, but unfortunetely storing unrepresentable value into a bitfield is implementation-defined. And this can be added later as extension to current design.
Tuple protocol
pointer_tag_pair supports being destructured, but it doesn't model tuple-like (as it would open whole can of worms, as told by STL). But following code should work:
auto [ptr, tag] = a_pointer_tag_pair;
Things it's not doing and why
- modeling pointer — this is not a pointer type, access to the pointer should be explicitly visible,
- convertible to bool — it's not sure if it means
.pointer() == nullptror also.tag() == 0, - manage lifetime — this is not a owning pointer, it's a tool to build one,
- using other than non-alignment bits — these are not portable and are subject of being enabled/disabled as an OS setting, this would create at best ABI problems,
- changing size based on bits requested — intention of this type to be same size of pointer, not be generic pair of pointer and any value,
- supporting function pointers — these doesn't need to be real pointer, but handlers, and can even have bigger size than normal pointer.
- supporting signed tag types — it's tricky and using bitfield is implementation-defined.
Impact on existing code
None, this is purely an API extension. It allows to express semantic clearly for a compiler instead of using an unsafe reinterpret_cast based techniques. Integral part of the proposed design is ability to interact with such existing code and migrate away from it.
Proposed changes to wording
20 Memory management library [mem]
20.1 General [mem.general]
Subclause | Header | |
Memory | <cstdlib>, <memory> | |
Smart pointers | <memory> | |
Pointer tagging | <memory> | |
Memory resources | <memory_resource> | |
Scoped allocators | <scoped_allocator> |
20.2 Memory [memory]
20.2.2 Header <memory> synopsis [memory.syn]
20.?.1 pointer tagging [ptrtag.bits]
20.?.1.1 pointer tagging limits [ptrtag.limit]
inline constexpr unsigned max_pointer_bits_available = see below;
max_pointer_bits_available indicates that non-arithmetic operations on invalid pointers with implementation-defined behavior ([basic.compound]) preserve sufficient information to guarantee that for an object tp of a type TP that is a specialization of tagged_pointer_pair and a pointer value q, that is a result of applying a sequence of such operations to the result of tp.tagged(), tp is equal to TP::from_tagged(q).constexpr unsigned pointer_bits_available(size_t alignment);
alignment is a power of two.alignment.alignment values larger than 1.countr_zero(alignment) and max_pointer_bits_available.20.?.2
Class template pointer_tag_pair [ptrtag.pair]
20.?.2.1 General [ptrtag.pair.general]
pointer_tag_pair provides a type to store an object pointer together with a tag value.namespace std {
template <class U, class PtrT, unsigned BitsRequested, size_t Alignment = alignof(U)>
concept tagging-compatible-pointee // exposition only
= convertible_to<U*, PtrT> && pointer_bits_available(Alignment) >= BitsRequested
&& (is_void_v<element-of<PtrT>>
|| is_scalar_v<element-of<PtrT>>
|| is_union_v<element-of<PtrT>>
|| is_pointer_interconvertible_base_of_v<element-of<PtrT>, U>);
template <class TagT> constexpr unsigned tag-bit-width(TagT value) noexcept; // exposition only
template <class Ptr,
unsigned BitsRequested = bits-available<element-of<Ptr>>,
class TagT = unsigned>
class pointer_tag_pair { // freestanding public:
using pointer_type = Ptr;
using element_type = pointer_traits<Ptr>::element_type;
using tagged_pointer_type = see below;
using tag_type = TagT;
static constexpr unsigned bits_requested = BitsRequested;
// Constructors and assignment
constexpr pointer_tag_pair() noexcept;
template <tagging-compatible-pointee<pointer_type, bits_requested> U>
constexpr pointer_tag_pair(U* p, tag_type t); constexpr pointer_tag_pair(nullptr_t p, tag_type t);
// Special construction helpers
template <size_t PromisedAlignment,
tagging-compatible-pointee<pointer_type, bits_requested, PromisedAlignment> U>
static constexpr pointer_tag_pair from_overaligned(U* p, tag_type t);
static pointer_tag_pair from_tagged(tagged_pointer_type p) noexcept;
// Accessors
tagged_pointer_type tagged_pointer() const noexcept;
constexpr pointer_type pointer() const noexcept;
constexpr tag_type tag() const noexcept;
// Swap
constexpr void swap(pointer_tag_pair& o) noexcept;
// Comparisons
friend constexpr see-below operator<=>(pointer_tag_pair lhs, pointer_tag_pair rhs) noexcept requires three_way_comparable<tag_type>;
friend constexpr bool operator==(pointer_tag_pair, pointer_tag_pair) noexcept requires equality_comparable<tag_type>;
};
template <class Ptr>
pointer_tag_pair(Ptr *) -> pointer_tag_pair<Ptr *>;
template <class Ptr, class TagT>
pointer_tag_pair(Ptr *, TagT tag) -> pointer_tag_pair<Ptr *, bits-available<element-of<Ptr>>, TagT>;
}
An object of class pointer_tag_pair<Ptr, BitsRequested, TagT> represents a pair of pointer value of type Ptr and tag value of type TagT.
Each specialization PT of pointer_tag_pair is a trivially copyable type that models copyable such that sizeof(PT) is equal to sizeof(Ptr) and alignof(PT) is equal to alignof(Ptr).
Mandates:
is_same_v<remove_cvref_t<Ptr>, Ptr>istrueis_same_v<remove_cvref_t<TagT>, TagT>istrueis_pointer_v<Ptr> && !is_function_v<remove_pointer_t<Ptr>>istrueis_unsigned_v<UT>istrue, whereUTisunderlying_type_t<TagT>ifTagTis an enumeration type, andTagTotherwisesizeof(TagT) <= sizeof(void*)istrueBitsRequested <= max_pointer_bits_availableistrue
template <class T> constexpr unsigned tag-bit-width(T v) noexcept;
static_cast<unsigned>(bit_width(to_underlying(v))) if is_enum_v<T> is true, otherwise static_cast<unsigned>(bit_width(v)).20.?.2.2 Constructors [ptrtag.pair.cons]
constexpr pointer_tag_pair() noexcept;
pointer() is equal to nullptr and tag() is equal to TagT().template <tagging-compatible-pointee<pointer_type, bits_requested> U>
constexpr pointer_tag_pair(U* p, tag_type t);
constexpr pointer_tag_pair(nullptr_t p, tag_type t);
pis not a pointer past the end of an object ([basic.compound])tag-bit-width(t) <= bits_requestedistrue
pointer() is equal to p and tag() is equal to t.20.?.2.3 Support for overaligned pointers [ptrtag.pair.overalign]
template <size_t PromisedAlignment,
tagging-compatible-pointee<pointer_type, bits_requested, PromisedAlignment> U>
static constexpr pointer_tag_pair from_overaligned(U* p, tag_type t);
pis not a pointer past the end of an object ([basic.compound])p == nullptr || is_sufficiently_aligned<PromisedAlignment>(p)istruetag-bit-width(t) <= bits_requestedistrue
ptp of type pointer_tag_pair such that both ptp.pointer() is equal to p and ptp.tag() is equal to t.20.?.2.4 Tagged pointer operations ([ptrtag.pair.tagops])
using tagged_pointer_type = see below;
tagged_pointer_type denotes cv void*, for cv such that pointer denotes cv U* for some type U.
tagged_pointer_type tagged_pointer() const noexcept;
tp of tagged_pointer_type type such that for any specialization DP of pointer_tag_pair and alignment value A for which:
pointer_bits_available(A) >= DP::bits_requestedistrue,is_sufficiently_aligned<A>(ptr)istrue, andtag-bit-width(tag) <= DP::bits_requestedistrue,
DP::from_tagged(tp) produces an object dp such that reinterpret_cast<pointer_type>(dp.pointer()) is equal to ptr and static_cast<TagT>(dp.tag()) is equal to tag.
static pointer_tag_pair from_tagged(tagged_pointer_type p) noexcept;
dp of type pointer_tag_type, such that:
-
reinterpret_cast<pointer_type>(sp.pointer())is equal todp.pointer()andstatic_cast<TagT>(sp.tag())is equal todp.tag(), ifpis equal tosp.tagged_pointer()for some objectspof a type that is a specialization ofpointer_tag_type, such that for some alignment valueA:pointer_bits_available(A) >= bits_requestedistrue,is_sufficiently_aligned<A>(sp.pointer())istrue, andtag-bit-width(sp.tag()) <= bits_requestedistrue,
- otherwise, the values of
dp.pointer()anddp.tag()are unspecified.
20.?.2.5 Accessors [ptrtag.pair.accessors]
constexpr pointer_type pointer() const noexcept;
*this.constexpr tag_type tag() const noexcept;
*this.20.?.2.6 Swap [ptrtag.pair.swap]
constexpr void swap(pointer_tag_pair& o) noexcept;
*this and o.20.?.2.7 Comparison [ptrtag.pair.comp]
friend constexpr auto operator<=>(pointer_tag_pair lhs, pointer_tag_pair rhs) noexcept requires three_way_comparable<tag_type>;
return pair(lhs.pointer(), lhs.tag()) <=> pair(rhs.pointer(), rhs.tag());lhs.tag() <=> rhs.tag() results in a call to a built-in operator <=> comparing values of type tag_type.friend constexpr bool operator==(pointer_tag_pair lhs, pointer_tag_pair rhs) noexcept requires equality_comparable<tag_type>;
return pair(lhs.pointer(), lhs.tag()) == pair(rhs.pointer(), rhs.tag());lhs.tag() == rhs.tag() results in a call to a built-in operator == comparing values of type tag_type.20.?.2.8 Tuple interface [ptrtag.pair.get]
template<class Ptr, unsigned BitsRequested, class TagT>
constexpr tuple_element_t<I, pointer_tag_pair<Ptr, BitsRequested, TagT>>
get(pointer_tag_pair<Ptr, BitsRequested, TagT> p) noexcept;
I < 2p.pointer()ifIis equal to zero,p.tag()otherwise.
Feature test macro
17.3.2 Header <version> synopsis [version.syn]
#define __cpp_lib_pointer_tag_pair 2026??L // freestanding, also in <memory>