C++ Logo

std-proposals

Advanced search

Re: [std-proposals] std::atomic_pointer_pair

From: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Date: Sat, 10 Jan 2026 01:50:02 +0000
I think I pretty much have "std::lockfree_2ptrs" finished:

    https://godbolt.org/z/ExTMf8sn8

I have given it two member functions, "set_and_increment" and
"set_and_decrement" especially for lockfree containers which use
[pointer + counter].

In my own case developing atomic<chimeric_ptr>, I use it to store a
data pointer alongside a code pointer.

And here is the GodBolt copy-pasted:

#include <climits> // CHAR_BIT might be 16
#include <cstddef> // size_t
#include <cstdint> // intptr_t, uint16_t, uint32_t, uint64_t, uintptr_t
#include <atomic> // memory_order
#include <bit> // bit_cast
#include <type_traits> // is_function, is_pointer, is_reference, is_same

template< typename P = void*, typename Q = std::uintptr_t >
class lockfree_2ptr final {

  // Next two lines make sure that P and Q are either pointers or
intptr_t or uintptr_t
  static_assert( std::is_same_v<P,std::uintptr_t> || std::is_same_v<P,
std::intptr_t> || std::is_pointer_v<P> );
  static_assert( std::is_same_v<Q,std::uintptr_t> || std::is_same_v<Q,
std::intptr_t> || std::is_pointer_v<Q> );

  // It's possible for one or both pointers to be a pointer
  // to a function, and in this scenario, we must ensure
  // that a code pointer is compatible with a data pointer.
  // The following two static_assert's ensure this:
  static_assert( (!std::is_function_v< std::remove_pointer_t<P> > &&
!std::is_function_v< std::remove_pointer_t<Q> >)
                 || sizeof(void*) == sizeof(void(*)(void)),
                 "Code pointers cannot be bigger or smaller than data
pointers");
  static_assert( (!std::is_function_v< std::remove_pointer_t<P> > &&
!std::is_function_v< std::remove_pointer_t<Q> >)
                 || alignof(void*) >= alignof(void(*)(void)),
                 "Code pointers cannot be more strictly aligned than
data pointers");

  // Figure out what integer size we need to store two pointers
  static consteval auto DetermineInt2Ptrs(void) noexcept
  {
    constexpr std::size_t bits_per_pointer = CHAR_BIT * sizeof(void*);
    /**/ if constexpr ( 64u == bits_per_pointer ) return __uint128_t();
    else if constexpr ( 32u == bits_per_pointer ) return std::uint64_t();
    else if constexpr ( 16u == bits_per_pointer ) return std::uint32_t();
    else if constexpr ( 8u == bits_per_pointer ) return std::uint16_t();
    else static_assert(false, "Sorry cannot accommodate your
Andromedan computer");
  }

  // Int2Ptrs is the integer type we use for storing the two pointers
  typedef decltype(DetermineInt2Ptrs()) Int2Ptrs;

  // 'n' is the member object containing the data.
  // We keept it aligned to 2 * pointer_size for
  // when we do exchanges
  alignas(sizeof(Int2Ptrs)) Int2Ptrs n;

public:

  // This is the Pair struct we use for
  // getting and setting the data, easier
  // to keep it aligned to 2 * pointer_size
  // for when we have to do exchanges
  struct alignas(sizeof(Int2Ptrs)) PtrPair {
    P first;
    Q second;

    _GLIBCXX_ALWAYS_INLINE
    constexpr PtrPair(P const p, Q const q) noexcept
      : first(p), second(q) {}

    _GLIBCXX_ALWAYS_INLINE
    constexpr PtrPair(Int2Ptrs const arg) noexcept
    {
      *this = std::bit_cast<PtrPair>(arg);
    }

    _GLIBCXX_ALWAYS_INLINE
    constexpr auto AsInt(void) const noexcept // returns by value
    {
      return std::bit_cast<Int2Ptrs>(*this);
    }
  };

  static_assert( sizeof(PtrPair) == sizeof(Int2Ptrs),
                 "Integer type 'Int2Ptrs' must be exactly the same
size as two pointers");

  static_assert( __atomic_always_lock_free(sizeof(Int2Ptrs), 0),
                 "The target CPU cannot do lockfree access on two pointers");

public:

  // Two constructors:
  _GLIBCXX_ALWAYS_INLINE constexpr lockfree_2ptr(void) noexcept : n(0) {}
  _GLIBCXX_ALWAYS_INLINE constexpr lockfree_2ptr(PtrPair const arg)
noexcept : n( arg.AsInt() ) {}

  // Delete copy-constructor and assignment operator
  lockfree_2ptr(lockfree_2ptr const &) = delete;
  lockfree_2ptr &operator=(lockfree_2ptr const &) = delete;

  _GLIBCXX_ALWAYS_INLINE
  void store(PtrPair const arg, std::memory_order const m =
std::memory_order_seq_cst) noexcept
  {
    __glibcxx_assert(m != std::memory_order_acquire);
    __glibcxx_assert(m != std::memory_order_acq_rel);
    __glibcxx_assert(m != std::memory_order_consume);
    __atomic_store_n(&n, arg.AsInt(), int(m));
  }

  _GLIBCXX_ALWAYS_INLINE
  PtrPair load(std::memory_order const m = std::memory_order_seq_cst)
const noexcept
  {
    __glibcxx_assert(m != std::memory_order_release);
    __glibcxx_assert(m != std::memory_order_acq_rel);
    return __atomic_load_n(&n, int(m));
  }

  // Implicit conversion to PtrPair
  _GLIBCXX_ALWAYS_INLINE operator PtrPair(void) const noexcept {
return load(); }

  // Assignment
  _GLIBCXX_ALWAYS_INLINE PtrPair operator=(PtrPair const arg) noexcept
{ store(arg); return arg; }

  _GLIBCXX_ALWAYS_INLINE
  PtrPair exchange(PtrPair const arg, std::memory_order const m =
std::memory_order_seq_cst) noexcept
  {
    return __atomic_exchange_n(&n, arg.AsInt(), int(m));
  }

  _GLIBCXX_ALWAYS_INLINE
  bool compare_exchange_strong(PtrPair &expected, PtrPair const desired,
                               std::memory_order const m1,
std::memory_order const m2) noexcept
  {
    __glibcxx_assert(__is_valid_cmpexch_failure_order(m2));
    alignas(sizeof(Int2Ptrs)) Int2Ptrs x = expected.AsInt(); // don't
make 'x' const
    bool const ok = __atomic_compare_exchange_n(
                      &n,
                      &x,
                      desired.AsInt(),
                      0, int(m1), int(m2));

    if ( false == ok ) expected = x;

    return ok;
  }

  _GLIBCXX_ALWAYS_INLINE
  bool compare_exchange_strong(PtrPair &expected, PtrPair const desired,
                               std::memory_order const m =
std::memory_order_seq_cst) noexcept
  {
    return compare_exchange_strong(expected, desired, m,
__cmpexch_failure_order(m));
  }

  _GLIBCXX_ALWAYS_INLINE
  bool compare_exchange_weak(PtrPair &expected, PtrPair const desired,
                             std::memory_order const m1,
std::memory_order const m2) noexcept
  {
    __glibcxx_assert(__is_valid_cmpexch_failure_order(m2));
    alignas(sizeof(Int2Ptrs)) Int2Ptrs x = expected.AsInt(); // don't
make 'x' const
    bool const ok = __atomic_compare_exchange_n(
                      &n,
                      &x,
                      desired.AsInt(),
                      1, int(m1), int(m2));

    if ( false == ok ) expected = x;

    return ok;
  }

  _GLIBCXX_ALWAYS_INLINE
  bool compare_exchange_weak(PtrPair &expected, PtrPair const desired,
                             std::memory_order const m =
std::memory_order_seq_cst) noexcept
  {
    return compare_exchange_weak(expected, desired, m,
__cmpexch_failure_order(m));
  }

#if __glibcxx_atomic_wait
  _GLIBCXX_ALWAYS_INLINE
  void wait(PtrPair const old, std::memory_order const m =
std::memory_order_seq_cst) const noexcept
  {
    std::__atomic_wait_address_v(
      &n,
      old.AsInt(),
      [m, this] { return __atomic_load_n(&n, int(m)); });
  }

  _GLIBCXX_ALWAYS_INLINE
  void notify_one(void) noexcept
  {
    std::__atomic_notify_address(&n, false);
  }

  _GLIBCXX_ALWAYS_INLINE
  void notify_all(void) noexcept
  {
    std::__atomic_notify_address(&n, true);
  }
#endif

private:
  // For lockfree containers, you can set the
  // first datum and increment/decrement the second datum
  template<bool up = true>
  _GLIBCXX_ALWAYS_INLINE
  Q set_and_IncOrDec(P const arg) noexcept
  {
    std::memory_order const m1 = std::memory_order_seq_cst,
                            m2 = __cmpexch_failure_order(m1);

    alignas(sizeof(Int2Ptrs)) Int2Ptrs expected = load().AsInt();
    for (; /* ever */; )
    {
      PtrPair desired = expected;
      desired.first = arg;
      if constexpr ( up ) ++desired.second; else --desired.second;
      if ( __atomic_compare_exchange_n(
             &n,
             &expected,
             desired.AsInt(),
             0, int(m1), int(m2) ) ) return desired.second;
    }
  }

public:

  // For lockfree containers, you can set the
  // first datum and increment the second datum
  _GLIBCXX_ALWAYS_INLINE
  Q set_and_increment(P const arg) noexcept
  {
    return set_and_IncOrDec<true >(arg);
  }

  // For lockfree containers, you can set the
  // first datum and decrement the second datum
  _GLIBCXX_ALWAYS_INLINE
  Q set_and_decrement(P const arg) noexcept
  {
    return set_and_IncOrDec<false>(arg);
  }
};

// ================================= Here comes the test code =================

#include <iostream>
#include <typeinfo>

struct chimeric_ptr {
  lockfree_2ptr< void*, void*(*)(std::type_info const&) > ptrs;

  template<typename T>
  chimeric_ptr(T *const parg)
  {
    ptrs = { parg, +[](std::type_info const&){ return (void*)nullptr; } };
  }
};

struct container {
  lockfree_2ptr< void*, std::uintptr_t > ptrs;

  void add( void *const arg )
  {
    ptrs.set_and_increment( arg );
  }

  void remove( void *const arg )
  {
    ptrs.set_and_decrement( arg );
  }
};

struct something_strange {
  lockfree_2ptr< void*, char* > ptrs;

  void add( void *const arg )
  {
    ptrs.set_and_increment( arg );
  }

  void remove( void *const arg )
  {
    ptrs.set_and_decrement( arg );
  }
};

int main(void)
{
  chimeric_ptr var( &std::cout );

  container myc;
  myc.add( nullptr );
  myc.remove( nullptr );

  something_strange ss;
  ss.add( nullptr );
  ss.remove( nullptr );

  lockfree_2ptr< void*, void* > vv;
  //vv.set_and_increment(nullptr); -- won't work because cannot
increment a void*
}

Received on 2026-01-10 01:49:10