[libcxx-commits] [libcxx] [libcxx][test-support] Implement fancy_pointer_allocator (PR #196187)

via libcxx-commits libcxx-commits at lists.llvm.org
Wed May 6 23:14:10 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-libcxx

Author: Nikita Belenkiy (kitsnet)

<details>
<summary>Changes</summary>

In order to make sure that our further modifications of basic_string will make
it able to handle fancy pointers such as boost::interprocess::offset_ptr,
we need some testable fancy pointers. Unfortunately, offset pointers
themselves are a bad idea for that, as they cannot be constant evaluated.

Instead, we are implementing a test fancy pointer that has nontrivial
special member functions that are C++20 constexpr and have side effects that
can be checked on correctness even in constant evaluated test contexts,
backed by a similarly C++20 constexpr fancy pointer allocator.

The idea is that the allocator has a state that counts how many pointer
instances originated from the allocator exist at the moment, the pointers
in their special methods update this shared state, and the state checks
at its destruction that there exactly 0 pointers are recorded as still
existing. The state is unique per instance of default-constructed
allocator but shared between the instances of copy/move-constructed
allocators.

The resulting test fancy pointer is more demanding than an offset pointer:
an offset pointer may have a trivial destructor, but our test pointer
has a destructor that relies on still existing state. Unfortunately,
our containers are generally laid out in such a way that their allocator
member is destructed earlier than their storage pointer members. To use
the fancy_pointer_allocator in testing, we either need to modify the
containers that they null the storage pointers at the end of destructor
(which will be naturally done for basic_string, which is our main target)
or write our tests in such a way that there is a copy of container's
allocator that survives the container's lifetime.

---
Full diff: https://github.com/llvm/llvm-project/pull/196187.diff


3 Files Affected:

- (modified) libcxx/test/std/strings/basic.string/string.cons/move_noexcept.pass.cpp (-4) 
- (modified) libcxx/test/support/module.modulemap (+4-1) 
- (modified) libcxx/test/support/test_allocator.h (+399-12) 


``````````diff
diff --git a/libcxx/test/std/strings/basic.string/string.cons/move_noexcept.pass.cpp b/libcxx/test/std/strings/basic.string/string.cons/move_noexcept.pass.cpp
index d807cac2c72c8..89502f6652655 100644
--- a/libcxx/test/std/strings/basic.string/string.cons/move_noexcept.pass.cpp
+++ b/libcxx/test/std/strings/basic.string/string.cons/move_noexcept.pass.cpp
@@ -32,11 +32,7 @@ int main(int, char**) {
   }
   {
     typedef std::basic_string<char, std::char_traits<char>, limited_allocator<char, 10>> C;
-#if TEST_STD_VER <= 14
-    static_assert(!std::is_nothrow_move_constructible<C>::value, "");
-#else
     static_assert(std::is_nothrow_move_constructible<C>::value, "");
-#endif
   }
 
   return 0;
diff --git a/libcxx/test/support/module.modulemap b/libcxx/test/support/module.modulemap
index 0af147d75ee5d..07acbb6b2e2fd 100644
--- a/libcxx/test/support/module.modulemap
+++ b/libcxx/test/support/module.modulemap
@@ -5,7 +5,10 @@ module test_config {
 
 module test {
   module double_move_tracker    { header "double_move_tracker.h" }
-  module test_allocator         { header "test_allocator.h" }
+  module test_allocator {
+    header "test_allocator.h"
+    export * // TODO: Workaround for https://llvm.org/PR120108
+  }
   module test_iterators         { header "test_iterators.h" }
   module type_algorithms        { header "type_algorithms.h" }
 }
diff --git a/libcxx/test/support/test_allocator.h b/libcxx/test/support/test_allocator.h
index f8b622d7f9520..09c27e72e7e0e 100644
--- a/libcxx/test/support/test_allocator.h
+++ b/libcxx/test/support/test_allocator.h
@@ -10,6 +10,7 @@
 #define TEST_ALLOCATOR_H
 
 #include <type_traits>
+#include <iterator>
 #include <new>
 #include <memory>
 #include <utility>
@@ -393,21 +394,30 @@ namespace detail {
 template <class T>
 class thread_unsafe_shared_ptr {
 public:
-  thread_unsafe_shared_ptr() = default;
-
-  TEST_CONSTEXPR_CXX14 thread_unsafe_shared_ptr(const thread_unsafe_shared_ptr& other) : block(other.block) {
+  // as it's internal and technically not nullable, we don't care about null pointer state
+  // and don't need destructive move
+  TEST_CONSTEXPR_CXX14 thread_unsafe_shared_ptr(const thread_unsafe_shared_ptr& other) TEST_NOEXCEPT
+      : block(other.block) {
     ++block->ref_count;
   }
-
-  TEST_CONSTEXPR_CXX20 ~thread_unsafe_shared_ptr() {
-    --block->ref_count;
-    if (block->ref_count != 0)
-      return;
-    typedef std::allocator_traits<std::allocator<control_block> > allocator_traits;
-    std::allocator<control_block> alloc;
-    allocator_traits::destroy(alloc, block);
-    allocator_traits::deallocate(alloc, block, 1);
+  TEST_CONSTEXPR_CXX14 thread_unsafe_shared_ptr(thread_unsafe_shared_ptr&& other) TEST_NOEXCEPT : block(other.block) {
+    ++block->ref_count;
+  }
+  TEST_CONSTEXPR_CXX14 thread_unsafe_shared_ptr& operator=(const thread_unsafe_shared_ptr& other) TEST_NOEXCEPT {
+    // self-assignment safe order
+    ++other.block->ref_count;
+    detach_control_block();
+    block = other.block;
+    return *this;
+  }
+  TEST_CONSTEXPR_CXX20 thread_unsafe_shared_ptr& operator=(thread_unsafe_shared_ptr&& other) TEST_NOEXCEPT {
+    // self-assignment safe order
+    ++other.block->ref_count;
+    detach_control_block();
+    block = other.block;
+    return *this;
   }
+  TEST_CONSTEXPR_CXX20 ~thread_unsafe_shared_ptr() TEST_NOEXCEPT { detach_control_block(); }
 
   TEST_CONSTEXPR const T& operator*() const { return block->content; }
   TEST_CONSTEXPR const T* operator->() const { return &block->content; }
@@ -424,6 +434,33 @@ class thread_unsafe_shared_ptr {
     T content;
   };
 
+  thread_unsafe_shared_ptr() = default;
+
+  TEST_CONSTEXPR_CXX20 void detach_control_block() {
+    --block->ref_count;
+    if (block->ref_count != 0)
+      return;
+    typedef std::allocator_traits<std::allocator<control_block> > allocator_traits;
+    std::allocator<control_block> alloc;
+    allocator_traits::destroy(alloc, block);
+#ifdef COUNT_NEW_H
+    // We are called from test code instrumented by counting operator new/operator delete,
+    // which might be surprised by unexpected deallocations we introduce.
+    // Instead of changing all the potentially affected tests, we will just temporarily
+    // disable that counting here.
+    bool disable_allocations = false;
+    if (!TEST_IS_CONSTANT_EVALUATED) {
+      disable_allocations                  = globalMemCounter.disable_allocations;
+      globalMemCounter.disable_allocations = false;
+    }
+#endif
+    allocator_traits::deallocate(alloc, block, 1);
+#ifdef COUNT_NEW_H
+    if (!TEST_IS_CONSTANT_EVALUATED)
+      globalMemCounter.disable_allocations = disable_allocations;
+#endif
+  }
+
   control_block* block = nullptr;
 
   template <class U, class... Args>
@@ -515,4 +552,354 @@ struct SocccAllocator {
   using propagate_on_container_swap            = std::false_type;
 };
 
+template <class T>
+class fancy_pointer {
+public:
+  // For the std::pointer_traits interface.
+  using pointer         = T*;
+  using element_type    = T;
+  using difference_type = std::ptrdiff_t;
+
+  // For the std::iterator_traits interface.
+  using value_type        = typename std::remove_cv<T>::type;
+  using reference         = typename std::add_lvalue_reference<T>::type;
+  using iterator_category = std::random_access_iterator_tag;
+
+  TEST_CONSTEXPR_CXX20 fancy_pointer() TEST_NOEXCEPT : ptr_(nullptr), ctr_(nullptr) {}
+  TEST_CONSTEXPR_CXX20 fancy_pointer(nullptr_t) TEST_NOEXCEPT : ptr_(nullptr), ctr_(nullptr) {}
+  TEST_CONSTEXPR_CXX20 explicit fancy_pointer(T* p, int* c = nullptr) TEST_NOEXCEPT : ptr_(p), ctr_(c) {
+    if (ctr_)
+      ++*ctr_;
+  }
+  template <typename T2>
+  TEST_CONSTEXPR_CXX20 explicit fancy_pointer(const fancy_pointer<T2>& other) TEST_NOEXCEPT
+      : ptr_(static_cast<T*>(other.ptr_)),
+        ctr_(other.ctr_) {
+    if (ctr_)
+      ++*ctr_;
+  }
+  TEST_CONSTEXPR_CXX20 fancy_pointer(const fancy_pointer& other) TEST_NOEXCEPT : ptr_(other.ptr_), ctr_(other.ctr_) {
+    if (ctr_)
+      ++*ctr_;
+  }
+  TEST_CONSTEXPR_CXX20 fancy_pointer(fancy_pointer&& other) TEST_NOEXCEPT : ptr_(other.ptr_), ctr_(other.ctr_) {
+    if (ctr_)
+      ++*ctr_;
+  }
+  TEST_CONSTEXPR_CXX20 fancy_pointer& operator=(const fancy_pointer& other) TEST_NOEXCEPT {
+    if (ctr_)
+      --*ctr_;
+    ptr_ = other.ptr_;
+    ctr_ = other.ctr_;
+    if (ctr_)
+      ++*ctr_;
+    return *this;
+  }
+  TEST_CONSTEXPR_CXX20 fancy_pointer& operator=(fancy_pointer&& other) TEST_NOEXCEPT {
+    if (ctr_)
+      --*ctr_;
+    ptr_ = other.ptr_;
+    ctr_ = other.ctr_;
+    if (ctr_)
+      ++*ctr_;
+    return *this;
+  }
+  TEST_CONSTEXPR_CXX20 ~fancy_pointer() TEST_NOEXCEPT {
+    if (ctr_)
+      --*ctr_;
+  }
+
+  TEST_CONSTEXPR_CXX20 operator fancy_pointer<const T>() const TEST_NOEXCEPT {
+    return fancy_pointer<const T>(ptr_, ctr_);
+  }
+
+  TEST_CONSTEXPR_CXX20 operator T*() const TEST_NOEXCEPT { return ptr_; }
+
+  TEST_CONSTEXPR_CXX20 T& operator*() const TEST_NOEXCEPT { return *ptr_; }
+  TEST_CONSTEXPR_CXX20 T* operator->() const TEST_NOEXCEPT { return ptr_; }
+  TEST_CONSTEXPR_CXX20 explicit operator bool() const TEST_NOEXCEPT { return ptr_; }
+  TEST_CONSTEXPR_CXX20 T* get() const TEST_NOEXCEPT { return ptr_; }
+
+  TEST_CONSTEXPR_CXX20 fancy_pointer& operator++() TEST_NOEXCEPT {
+    ++ptr_;
+    return *this;
+  }
+  TEST_CONSTEXPR_CXX20 fancy_pointer operator++(int) TEST_NOEXCEPT {
+    fancy_pointer tmp(*this);
+    ++ptr_;
+    return tmp;
+  }
+
+  TEST_CONSTEXPR_CXX20 fancy_pointer& operator--() TEST_NOEXCEPT {
+    --ptr_;
+    return *this;
+  }
+  TEST_CONSTEXPR_CXX20 fancy_pointer operator--(int) TEST_NOEXCEPT {
+    fancy_pointer tmp(*this);
+    --ptr_;
+    return tmp;
+  }
+
+  TEST_CONSTEXPR_CXX20 fancy_pointer& operator+=(difference_type n) TEST_NOEXCEPT {
+    ptr_ += n;
+    return *this;
+  }
+
+  TEST_CONSTEXPR_CXX20 fancy_pointer& operator-=(difference_type n) TEST_NOEXCEPT {
+    ptr_ -= n;
+    return *this;
+  }
+
+  TEST_CONSTEXPR_CXX20 fancy_pointer operator+(difference_type n) const TEST_NOEXCEPT {
+    fancy_pointer tmp(*this);
+    tmp += n;
+    return tmp;
+  }
+
+  TEST_CONSTEXPR_CXX20 fancy_pointer operator-(difference_type n) const TEST_NOEXCEPT {
+    fancy_pointer tmp(*this);
+    tmp -= n;
+    return tmp;
+  }
+
+  TEST_CONSTEXPR_CXX20 reference operator[](difference_type n) const TEST_NOEXCEPT { return ptr_[n]; }
+
+  friend TEST_CONSTEXPR_CXX20 fancy_pointer operator+(difference_type x, fancy_pointer y) TEST_NOEXCEPT {
+    return y + x;
+  }
+
+  static TEST_CONSTEXPR_CXX20 fancy_pointer pointer_to(reference ref) TEST_NOEXCEPT { return fancy_pointer(&ref); }
+
+  T* ptr_;
+  int* ctr_;
+};
+
+template <>
+class fancy_pointer<void> {
+public:
+  using T = void;
+
+  // For the std::pointer_traits interface.
+  using pointer         = T*;
+  using element_type    = T;
+  using difference_type = std::ptrdiff_t;
+
+  // For the std::iterator_traits interface.
+  using value_type        = typename std::remove_cv<T>::type;
+  using reference         = typename std::add_lvalue_reference<T>::type;
+  using iterator_category = std::random_access_iterator_tag;
+
+  TEST_CONSTEXPR_CXX20 fancy_pointer() TEST_NOEXCEPT : ptr_(nullptr), ctr_(nullptr) {}
+  TEST_CONSTEXPR_CXX20 fancy_pointer(nullptr_t) TEST_NOEXCEPT : ptr_(nullptr), ctr_(nullptr) {}
+  TEST_CONSTEXPR_CXX20 explicit fancy_pointer(T* p, int* c = nullptr) TEST_NOEXCEPT : ptr_(p), ctr_(c) {
+    if (ctr_)
+      ++*ctr_;
+  }
+  template <typename T2>
+  TEST_CONSTEXPR_CXX20 explicit fancy_pointer(const fancy_pointer<T2>& other) TEST_NOEXCEPT
+      : ptr_(static_cast<T*>(other.ptr_), other.ctr_) {
+    if (ctr_)
+      ++*ctr_;
+  }
+  TEST_CONSTEXPR_CXX20 fancy_pointer(const fancy_pointer& other) TEST_NOEXCEPT : ptr_(other.ptr_), ctr_(other.ctr_) {
+    if (ctr_)
+      ++*ctr_;
+  }
+  TEST_CONSTEXPR_CXX20 fancy_pointer(fancy_pointer&& other) TEST_NOEXCEPT : ptr_(other.ptr_), ctr_(other.ctr_) {
+    if (ctr_)
+      ++*ctr_;
+  }
+  TEST_CONSTEXPR_CXX20 fancy_pointer& operator=(const fancy_pointer& other) TEST_NOEXCEPT {
+    if (ctr_)
+      --*ctr_;
+    ptr_ = other.ptr_;
+    ctr_ = other.ctr_;
+    if (ctr_)
+      ++*ctr_;
+    return *this;
+  }
+  TEST_CONSTEXPR_CXX20 fancy_pointer& operator=(fancy_pointer&& other) TEST_NOEXCEPT {
+    if (ctr_)
+      --*ctr_;
+    ptr_ = other.ptr_;
+    ctr_ = other.ctr_;
+    if (ctr_)
+      ++*ctr_;
+    return *this;
+  }
+  TEST_CONSTEXPR_CXX20 ~fancy_pointer() TEST_NOEXCEPT {
+    if (ctr_)
+      --*ctr_;
+  }
+
+  TEST_CONSTEXPR_CXX14 explicit operator bool() const TEST_NOEXCEPT { return ptr_; }
+  TEST_CONSTEXPR_CXX14 T* get() const TEST_NOEXCEPT { return ptr_; }
+
+  T* ptr_;
+  int* ctr_;
+};
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 typename fancy_pointer<T>::difference_type
+operator-(fancy_pointer<T> x, fancy_pointer<T> y) TEST_NOEXCEPT {
+  return x.ptr_ - y.ptr_;
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 typename fancy_pointer<T>::difference_type
+operator-(fancy_pointer<const T> x, fancy_pointer<T> y) TEST_NOEXCEPT {
+  return x.ptr_ - y.ptr_;
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 typename fancy_pointer<T>::difference_type
+operator-(fancy_pointer<T> x, fancy_pointer<const T> y) TEST_NOEXCEPT {
+  return x.ptr_ - y.ptr_;
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 typename fancy_pointer<T>::difference_type
+operator-(fancy_pointer<const T> x, fancy_pointer<const T> y) TEST_NOEXCEPT {
+  return x.ptr_ - y.ptr_;
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 bool operator==(fancy_pointer<T> x, fancy_pointer<T> y) TEST_NOEXCEPT {
+  return x.get() == y.get();
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 bool operator==(fancy_pointer<const T> x, fancy_pointer<T> y) TEST_NOEXCEPT {
+  return x.get() == y.get();
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 bool operator==(fancy_pointer<T> x, fancy_pointer<const T> y) TEST_NOEXCEPT {
+  return x.get() == y.get();
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 bool operator==(fancy_pointer<const T> x, fancy_pointer<const T> y) TEST_NOEXCEPT {
+  return x.get() == y.get();
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 bool operator!=(fancy_pointer<T> x, fancy_pointer<T> y) TEST_NOEXCEPT {
+  return !(x == y);
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 bool operator!=(fancy_pointer<const T> x, fancy_pointer<T> y) TEST_NOEXCEPT {
+  return !(x == y);
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 bool operator!=(fancy_pointer<T> x, fancy_pointer<const T> y) TEST_NOEXCEPT {
+  return !(x == y);
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 bool operator!=(fancy_pointer<const T> x, fancy_pointer<const T> y) TEST_NOEXCEPT {
+  return !(x == y);
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 bool operator>(fancy_pointer<const T> x, fancy_pointer<const T> y) TEST_NOEXCEPT {
+  return x.get() == y.get();
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 bool operator<(fancy_pointer<const T> x, fancy_pointer<const T> y) TEST_NOEXCEPT {
+  return y < x;
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 bool operator<=(fancy_pointer<const T> x, fancy_pointer<const T> y) TEST_NOEXCEPT {
+  return !(y < x);
+}
+
+template <typename T>
+TEST_CONSTEXPR_CXX20 bool operator>=(fancy_pointer<const T> x, fancy_pointer<const T> y) TEST_NOEXCEPT {
+  return !(x < y);
+}
+
+struct fancy_pointer_statistics {
+  int alive_count = 0;
+
+  TEST_CONSTEXPR_CXX20 fancy_pointer_statistics() {};
+  fancy_pointer_statistics(const fancy_pointer_statistics&)            = delete;
+  fancy_pointer_statistics(fancy_pointer_statistics&&)                 = delete;
+  fancy_pointer_statistics& operator=(const fancy_pointer_statistics&) = delete;
+  fancy_pointer_statistics& operator=(fancy_pointer_statistics&&)      = delete;
+  TEST_CONSTEXPR_CXX20 ~fancy_pointer_statistics() {
+    assert(alive_count == 0 && "unmatched constructor/destructor/assignment?");
+  }
+};
+
+template <class T>
+class fancy_pointer_allocator : public std::allocator<T> {
+  using base = std::allocator<T>;
+
+public:
+  using value_type      = T;
+  using pointer         = fancy_pointer<T>;
+  using const_pointer   = fancy_pointer<const T>;
+  using reference       = value_type&;
+  using const_reference = const value_type&;
+  using size_type       = std::size_t;
+  using difference_type = std::ptrdiff_t;
+
+  template <class U>
+  struct rebind {
+    using other = fancy_pointer_allocator<U>;
+  };
+
+  using propagate_on_container_copy_assignment = std::true_type;
+  using propagate_on_container_move_assignment = std::true_type;
+  using propagate_on_container_swap            = std::true_type;
+
+  TEST_CONSTEXPR_CXX20 fancy_pointer_allocator() TEST_NOEXCEPT
+      : stats_(detail::make_thread_unsafe_shared<fancy_pointer_statistics>()) {}
+
+  template <class U>
+  TEST_CONSTEXPR_CXX20 explicit fancy_pointer_allocator(fancy_pointer_allocator<U> other) TEST_NOEXCEPT
+      : stats_(other.stats_) {}
+
+  TEST_CONSTEXPR_CXX20 fancy_pointer_allocator(const fancy_pointer_allocator&)            = default;
+  TEST_CONSTEXPR_CXX20 fancy_pointer_allocator(fancy_pointer_allocator&&)                 = default;
+  TEST_CONSTEXPR_CXX20 fancy_pointer_allocator& operator=(const fancy_pointer_allocator&) = default;
+  TEST_CONSTEXPR_CXX20 fancy_pointer_allocator& operator=(fancy_pointer_allocator&&)      = default;
+  TEST_CONSTEXPR_CXX20 ~fancy_pointer_allocator()                                         = default;
+
+  TEST_CONSTEXPR_CXX20 pointer allocate(size_t n) { return pointer(base::allocate(n), &stats_->alive_count); }
+  TEST_CONSTEXPR_CXX20 void deallocate(pointer p, size_t n) { base::deallocate(p, n); }
+
+#if TEST_STD_VER > 20
+  TEST_CONSTEXPR_CXX20 std::allocation_result<pointer> allocate_at_least(std::size_t n) {
+    return {pointer{base::allocate(n), &stats_->alive_count}, n};
+  }
+#endif
+
+  detail::thread_unsafe_shared_ptr<fancy_pointer_statistics> stats_;
+};
+
+template <class T, class U>
+TEST_CONSTEXPR_CXX20 inline bool
+operator==(fancy_pointer_allocator<T> const& LHS, fancy_pointer_allocator<U> const& RHS) {
+  return LHS.stats_.get() == RHS.stats_.get();
+}
+
+template <class T, class U>
+TEST_CONSTEXPR_CXX20 inline bool
+operator!=(fancy_pointer_allocator<T> const& LHS, fancy_pointer_allocator<U> const& RHS) {
+  return !(LHS == RHS);
+}
+
+#if 0 // to be enabled after merge of https://github.com/llvm/llvm-project/pull/195839
+template <typename T>
+struct are_default_allocators_always_equal<fancy_pointer_allocator<T> > {
+  enum { value = 0 };
+};
+#endif
+
 #endif // TEST_ALLOCATOR_H

``````````

</details>


https://github.com/llvm/llvm-project/pull/196187


More information about the libcxx-commits mailing list