[PATCH v2 3/3] libstdc++: Add tests for std::hazard_pointer
Paul Xi Cao <[email protected]>
| Newsgroups | gmane.comp.gcc.patches,gmane.comp.gcc.libstdc++.devel |
|---|---|
| Message-ID | <[email protected]> |
Cover the full C++26 std::hazard_pointer API surface with DejaGnu
tests under 30_threads/hazard_pointer/.
Functional tests (one file per API / concern):
* make_hazard_pointer.cc -- factory, record accounting, pool growth.
* ctor.cc -- default ctor, move ctor, destructor, record accounting.
* empty.cc -- empty() across every state transition.
* move_assign.cc -- transfers ownership, releases prior record, self-
assignment is a no-op.
* swap.cc -- member swap, free swap, protection follows the record.
* protect.cc -- returns current value, null source, ownership,
re-protect updates the hazard.
* try_protect.cc -- success / failure return, ptr output, agreement
with protect() on a stable pointer.
* reset_protection.cc -- both overloads, idempotence, typed-pointer
overload publishes a hazard directly.
* retire.cc -- single retire, multiple retires, threshold auto-
synchronize fires when retired count exceeds 2 * active records.
* custom_deleter.cc -- functor deleter invoked, default-constructed
deleter, deleter suppressed while protected.
Compile-time property tests:
* noexcept.cc -- static_asserts for every noexcept-annotated API.
* type_constraints.cc -- _Protectable concept for derived / non-
derived types, retire() deleter constraints.
Multi-threaded tests:
* concurrent.cc -- readers + writer, multi-writer multi-reader,
high-contention.
* thread_exit.cc -- ~_RetireListNode drains the retire list on
thread exit; protected survivors offload to the orphan list.
Negative compile tests (dg-error + dg-prune-output for cascade
diagnostics past the static_assert / -Werror=unused-result):
* protect_neg.cc -- protect<int>() and protect<NonHazard>()
rejected by the _Protectable concept.
* retire_neg.cc -- retire() rejected when hazard_pointer_obj_base
is a private base.
* retire_virtual_neg.cc -- retire() rejected when
hazard_pointer_obj_base is a virtual base; gated on
__cpp_lib_is_virtual_base_of.
* nodiscard_neg.cc -- discarding the return of
make_hazard_pointer(), empty(), protect(), or try_protect()
trips -Werror=unused-result.
Changed since v1:
* concurrent.cc was rewritten, because it could not fail. Its
readers checked `p->value < 0` while its writers only ever
published `++counter`, so no use-after-free could be detected.
Reclaimed nodes now go through a deleter that poisons them and
parks them in a quarantine drained at process exit, so a reader
that dereferences an already-reclaimed node observes the poison
on every target, with no dependence on a sanitizer and without
reading freed memory. Writing the poison from ~Node() instead
does not work: glibc overwrites the first 16 bytes of the chunk
with tcache bookkeeping immediately after the destructor runs, and
the writer's next allocation gets the same chunk back. Every
converted test also asserts it observed at least one node, since a
run that protected nothing would otherwise pass green -- the same
defect as the original. One test keeps the plain operator delete
path so that a sanitizer run still has genuinely freed memory to
catch. Verified by negative control: breaking the protected-set
check in _M_synchronize() makes the test abort on the error count,
rather than pass.
* New retire_no_alloc.cc: replaces the global allocation functions
and asserts that retire() allocates nothing, and that a
steady-state reclamation allocates nothing either. It first
checks that its own counter fires, since a probe that cannot fire
would repeat the concurrent.cc mistake, and it was validated by
reinstating an allocation on the retire path. __gnu_test::counter
from testsuite/util/replacement_memory_operators.h was considered
and does not fit: its destructor throws when the live count is
non-zero at exit, and the records are freed in ~_Domain during
static-storage destruction, which need not run before it.
* New layout.cc: the ABI size and alignment assertions, so the
layout freeze on hazard_pointer_obj_base and hazard_pointer is
visible in the testsuite and not only in the header.
All tests pass at -std=gnu++26 on x86_64-pc-linux-gnu (42/42).
libstdc++-v3/ChangeLog:
* testsuite/30_threads/hazard_pointer/concurrent.cc: New test.
* testsuite/30_threads/hazard_pointer/ctor.cc: New test.
* testsuite/30_threads/hazard_pointer/custom_deleter.cc: New test.
* testsuite/30_threads/hazard_pointer/empty.cc: New test.
* testsuite/30_threads/hazard_pointer/layout.cc: New test.
* testsuite/30_threads/hazard_pointer/make_hazard_pointer.cc: New test.
* testsuite/30_threads/hazard_pointer/move_assign.cc: New test.
* testsuite/30_threads/hazard_pointer/nodiscard_neg.cc: New test.
* testsuite/30_threads/hazard_pointer/noexcept.cc: New test.
* testsuite/30_threads/hazard_pointer/protect.cc: New test.
* testsuite/30_threads/hazard_pointer/protect_neg.cc: New test.
* testsuite/30_threads/hazard_pointer/reset_protection.cc: New test.
* testsuite/30_threads/hazard_pointer/retire.cc: New test.
* testsuite/30_threads/hazard_pointer/retire_neg.cc: New test.
* testsuite/30_threads/hazard_pointer/retire_no_alloc.cc: New test.
* testsuite/30_threads/hazard_pointer/retire_virtual_neg.cc: New test.
* testsuite/30_threads/hazard_pointer/swap.cc: New test.
* testsuite/30_threads/hazard_pointer/thread_exit.cc: New test.
* testsuite/30_threads/hazard_pointer/try_protect.cc: New test.
* testsuite/30_threads/hazard_pointer/type_constraints.cc: New test.
---
.../30_threads/hazard_pointer/concurrent.cc | 406 ++++++++++++++++++
.../30_threads/hazard_pointer/ctor.cc | 102 +++++
.../hazard_pointer/custom_deleter.cc | 126 ++++++
.../30_threads/hazard_pointer/empty.cc | 105 +++++
.../30_threads/hazard_pointer/layout.cc | 85 ++++
.../hazard_pointer/make_hazard_pointer.cc | 84 ++++
.../30_threads/hazard_pointer/move_assign.cc | 91 ++++
.../hazard_pointer/nodiscard_neg.cc | 58 +++
.../30_threads/hazard_pointer/noexcept.cc | 56 +++
.../30_threads/hazard_pointer/protect.cc | 96 +++++
.../30_threads/hazard_pointer/protect_neg.cc | 43 ++
.../hazard_pointer/reset_protection.cc | 116 +++++
.../30_threads/hazard_pointer/retire.cc | 119 +++++
.../30_threads/hazard_pointer/retire_neg.cc | 39 ++
.../hazard_pointer/retire_no_alloc.cc | 213 +++++++++
.../hazard_pointer/retire_virtual_neg.cc | 40 ++
.../30_threads/hazard_pointer/swap.cc | 133 ++++++
.../30_threads/hazard_pointer/thread_exit.cc | 98 +++++
.../30_threads/hazard_pointer/try_protect.cc | 132 ++++++
.../hazard_pointer/type_constraints.cc | 56 +++
20 files changed, 2198 insertions(+)
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/concurrent.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/ctor.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/custom_deleter.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/empty.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/layout.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/make_hazard_pointer.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/move_assign.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/nodiscard_neg.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/noexcept.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/protect.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/protect_neg.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/reset_protection.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/retire.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/retire_neg.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/retire_no_alloc.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/retire_virtual_neg.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/swap.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/thread_exit.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/try_protect.cc
create mode 100644 libstdc++-v3/testsuite/30_threads/hazard_pointer/type_constraints.cc
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/concurrent.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/concurrent.cc
new file mode 100644
index 00000000000..74c062b3a38
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/concurrent.cc
@@ -0,0 +1,406 @@
+// { dg-do run { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <chrono>
+#include <functional>
+#include <thread>
+#include <vector>
+#include <testsuite_hooks.h>
+
+namespace hpd = std::__hazard_pointer;
+
+namespace
+{
+ // --- Quarantined nodes: making reclamation observable -------------------
+ //
+ // The reader check here used to be `p->value < 0` while writers only ever
+ // published `++counter`, so it could never fire: these tests could not
+ // detect a use-after-free at all.
+ //
+ // The obvious repair, having ~Node() write a negative sentinel, does not
+ // work either. `delete p` runs the destructor and then operator delete,
+ // and glibc immediately writes its tcache next/key over the first 16 bytes
+ // of the chunk. Node::value sits at offset 4, so the sentinel is replaced
+ // by allocator bookkeeping -- which reads back as a small *positive* int --
+ // nanoseconds after it is written. The writer's next allocation then hands
+ // the same chunk straight back, tcache being LIFO. Measured on
+ // x86_64/glibc: the sentinel was not visible once in five trials.
+ //
+ // So reclaimed nodes must not go back to the allocator. The deleter
+ // poisons the node and parks it here; the quarantine is drained only at
+ // process exit. The memory stays valid, so a reader that dereferences a
+ // node the domain has already reclaimed reliably observes kPoisoned -- on
+ // every target, with no dependence on a sanitizer, and without reading
+ // freed memory (which would be UB the compiler is entitled to exploit).
+ constexpr int kPoisoned = -1;
+
+ // Quarantined nodes are never freed during a run, so the writers below are
+ // bounded by a node budget as well as by the stop flag.
+ constexpr int kMaxNodesPerWriter = 50000;
+
+ struct QuarantinedNode;
+
+ // An intrusive lock-free stack, not a mutex and a vector. The deleter runs
+ // from _M_synchronize(), which is noexcept, so a vector push_back that
+ // threw bad_alloc would turn an OOM inside the test's own quarantine into
+ // std::terminate() -- the exact failure mode the intrusive retire list
+ // exists to remove. Pushing onto a link the node already carries cannot
+ // fail, and needs no mutex, so there is no lock-ordering argument to make
+ // against the domain's own locks.
+ struct Quarantine
+ {
+ std::atomic<QuarantinedNode*> head{nullptr};
+
+ void reclaim(QuarantinedNode* ptr) noexcept;
+ ~Quarantine();
+ };
+
+ // constinit is load-bearing, not decoration. ~_Domain() drains the orphan
+ // list through this deleter during static-storage destruction, so the
+ // quarantine has to still be alive at that point. Constant initialization
+ // puts its construction before any dynamic initialization, and therefore
+ // its destruction after the function-local static domain in
+ // _S_default_domain(). Spelling it constinit makes the compiler enforce
+ // that: a member without a constexpr default constructor breaks the build
+ // here rather than the test at exit.
+ constinit Quarantine quarantine;
+
+ // A separate stateless type is required: retire() needs D to be default
+ // constructible and move assignable, and Quarantine is neither invocable
+ // nor movable.
+ struct QuarantineDeleter
+ {
+ void operator()(QuarantinedNode* ptr) const { quarantine.reclaim(ptr); }
+ };
+
+ struct QuarantinedNode
+ : std::hazard_pointer_obj_base<QuarantinedNode, QuarantineDeleter>
+ {
+ int value;
+ // Quarantine link. Only ever touched after the domain has handed the
+ // node to the deleter, i.e. once the domain is done with it, so it cannot
+ // alias the intrusive retire link inside the protected base.
+ QuarantinedNode* qnext = nullptr;
+ explicit QuarantinedNode(int v) : value(v) {}
+ };
+
+ // `value` is a plain int on purpose. If the reclaim-side ordering is ever
+ // wrong, this store races the reader's load of the same int, so a run under
+ // -fsanitize=thread reports it and one bug has two independent detectors.
+ //
+ // The poison is written before the node is published to the stack, and the
+ // release/acquire pair on head carries it to the destructor. Nothing here
+ // can throw, which is the point -- see the note on Quarantine above.
+ void Quarantine::reclaim(QuarantinedNode* ptr) noexcept
+ {
+ ptr->value = kPoisoned;
+ QuarantinedNode* next = head.load(std::memory_order_relaxed);
+ do
+ ptr->qnext = next;
+ while (!head.compare_exchange_weak(next, ptr, std::memory_order_release,
+ std::memory_order_relaxed));
+ }
+
+ Quarantine::~Quarantine()
+ {
+ for (const QuarantinedNode* ptr = head.load(std::memory_order_acquire);
+ ptr != nullptr;)
+ {
+ const QuarantinedNode* const next = ptr->qnext; // read before delete
+ delete ptr;
+ ptr = next;
+ }
+ }
+
+ // reads counts successful protects, so a test cannot pass by never
+ // observing a node at all -- that vacuity is exactly what the original
+ // version of this test got wrong.
+ void quarantined_reader_fn(std::atomic<QuarantinedNode*>& shared,
+ std::atomic<bool>& stop,
+ std::atomic<int>& errors,
+ std::atomic<int>& reads)
+ {
+ auto hp = std::make_hazard_pointer();
+ while (!stop.load(std::memory_order_acquire))
+ {
+ const QuarantinedNode* const p = hp.protect(shared);
+ if (p)
+ {
+ reads.fetch_add(1, std::memory_order_relaxed);
+ if (p->value == kPoisoned)
+ errors.fetch_add(1, std::memory_order_relaxed);
+ }
+ hp.reset_protection();
+ }
+ }
+
+ void quarantined_writer_fn(std::atomic<QuarantinedNode*>& shared,
+ std::atomic<bool>& stop)
+ {
+ int counter = 0;
+ while (!stop.load(std::memory_order_acquire)
+ && counter < kMaxNodesPerWriter)
+ {
+ QuarantinedNode* next = new QuarantinedNode(++counter);
+ QuarantinedNode* const old
+ = shared.exchange(next, std::memory_order_seq_cst);
+ if (old)
+ old->retire();
+ }
+ QuarantinedNode* final_
+ = shared.exchange(nullptr, std::memory_order_seq_cst);
+ if (final_)
+ final_->retire();
+ }
+
+ // --- Plain nodes: the real operator delete path -------------------------
+ //
+ // Kept on the default deleter so that one test still frees for real while
+ // readers are running. Its detector is a sanitizer, not a value check: per
+ // the note above, no sentinel can survive the free, so the dereference
+ // itself is the point -- a failed protection surfaces as
+ // heap-use-after-free under -fsanitize=address.
+ struct Node : std::hazard_pointer_obj_base<Node>
+ {
+ int value;
+ explicit Node(int v) : value(v) {}
+ };
+
+ // observed is a store, not an accumulator: summing values would overflow
+ // int over millions of iterations. It starts at kNothingObserved, which
+ // writers never publish, so the test can still tell "read nothing" from
+ // "read something".
+ constexpr int kNothingObserved = -1;
+
+ void reader_fn(std::atomic<Node*>& shared, std::atomic<bool>& stop,
+ std::atomic<int>& observed)
+ {
+ auto hp = std::make_hazard_pointer();
+ while (!stop.load(std::memory_order_acquire))
+ {
+ const Node* const p = hp.protect(shared);
+ if (p)
+ observed.store(p->value, std::memory_order_relaxed);
+ hp.reset_protection();
+ }
+ }
+
+ void writer_fn(std::atomic<Node*>& shared, std::atomic<bool>& stop)
+ {
+ int counter = 0;
+ while (!stop.load(std::memory_order_acquire))
+ {
+ Node* next = new Node(++counter);
+ Node* const old = shared.exchange(next, std::memory_order_seq_cst);
+ if (old)
+ old->retire();
+ }
+ Node* final_ = shared.exchange(nullptr, std::memory_order_seq_cst);
+ if (final_)
+ final_->retire();
+ }
+}
+
+// Readers and one writer: no reader ever sees a reclaimed node.
+void test01()
+{
+ std::atomic<QuarantinedNode*> shared{new QuarantinedNode(1)};
+ std::atomic<bool> stop{false};
+ std::atomic<int> errors{0};
+ std::atomic<int> reads{0};
+
+ constexpr int kReaders = 4;
+ std::vector<std::thread> threads;
+ threads.reserve(kReaders + 1);
+
+ for (int i = 0; i < kReaders; ++i)
+ threads.emplace_back(quarantined_reader_fn, std::ref(shared),
+ std::ref(stop), std::ref(errors), std::ref(reads));
+ threads.emplace_back(quarantined_writer_fn, std::ref(shared),
+ std::ref(stop));
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(200));
+ stop.store(true, std::memory_order_release);
+ for (auto& t : threads)
+ t.join();
+
+ hpd::_S_default_domain()._M_synchronize();
+ // A reader dereferenced a node the domain had already reclaimed.
+ VERIFY( errors.load() == 0 );
+ // No reader ever observed a node; the test proved nothing.
+ VERIFY( reads.load() > 0 );
+}
+
+// protect() sees the latest value across stores.
+void test02()
+{
+ auto hp = std::make_hazard_pointer();
+ Node a{10}, b{20};
+ std::atomic<Node*> src{&a};
+
+ const Node* const p1 = hp.protect(src);
+ VERIFY( p1 == &a );
+
+ src.store(&b, std::memory_order_release);
+ const Node* const p2 = hp.protect(src);
+ VERIFY( p2 == &b );
+}
+
+// Multiple writers, multiple readers.
+void test03()
+{
+ constexpr int kReaders = 4;
+ constexpr int kWriters = 4;
+
+ std::vector<std::atomic<QuarantinedNode*>> srcs(kWriters);
+ for (auto& s : srcs)
+ s.store(new QuarantinedNode(1), std::memory_order_relaxed);
+
+ std::atomic<bool> stop{false};
+ std::atomic<int> errors{0};
+ std::atomic<int> reads{0};
+
+ auto multi_reader = [&] {
+ std::vector<std::hazard_pointer> hps;
+ hps.reserve(kWriters);
+ for (int i = 0; i < kWriters; ++i)
+ hps.emplace_back(std::make_hazard_pointer());
+ while (!stop.load(std::memory_order_acquire))
+ for (int i = 0; i < kWriters; ++i)
+ {
+ const QuarantinedNode* const p = hps[i].protect(srcs[i]);
+ if (p)
+ {
+ reads.fetch_add(1, std::memory_order_relaxed);
+ if (p->value == kPoisoned)
+ errors.fetch_add(1, std::memory_order_relaxed);
+ }
+ hps[i].reset_protection();
+ }
+ };
+
+ auto single_writer = [&](int idx) {
+ int counter = 1;
+ while (!stop.load(std::memory_order_acquire)
+ && counter < kMaxNodesPerWriter)
+ {
+ QuarantinedNode* next = new QuarantinedNode(++counter);
+ QuarantinedNode* old
+ = srcs[idx].exchange(next, std::memory_order_seq_cst);
+ if (old)
+ old->retire();
+ }
+ QuarantinedNode* final_
+ = srcs[idx].exchange(nullptr, std::memory_order_seq_cst);
+ if (final_)
+ final_->retire();
+ };
+
+ std::vector<std::thread> threads;
+ threads.reserve(kReaders + kWriters);
+ for (int i = 0; i < kReaders; ++i)
+ threads.emplace_back(multi_reader);
+ for (int i = 0; i < kWriters; ++i)
+ threads.emplace_back(single_writer, i);
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(200));
+ stop.store(true, std::memory_order_release);
+ for (auto& t : threads)
+ t.join();
+
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( errors.load() == 0 );
+ VERIFY( reads.load() > 0 );
+}
+
+// Retire from many threads: all reclaimed by the thread-exit drain.
+void test04()
+{
+ constexpr int kThreads = 8;
+ constexpr int kObjectsPerThread = 10;
+ std::atomic<int> total_deleted{0};
+
+ struct CountedNode : std::hazard_pointer_obj_base<CountedNode>
+ {
+ std::atomic<int>& counter;
+ explicit CountedNode(std::atomic<int>& c) : counter(c) {}
+ ~CountedNode() { counter.fetch_add(1, std::memory_order_relaxed); }
+ };
+
+ std::vector<std::thread> threads;
+ threads.reserve(kThreads);
+ for (int i = 0; i < kThreads; ++i)
+ threads.emplace_back([&] {
+ for (int j = 0; j < kObjectsPerThread; ++j)
+ (new CountedNode(total_deleted))->retire();
+ });
+ for (auto& t : threads)
+ t.join();
+
+ VERIFY( total_deleted.load() == kThreads * kObjectsPerThread );
+}
+
+// High contention, on the real operator delete path.
+//
+// What this can and cannot prove on x86, since a green run here is easy to
+// over-read: it is a sampling test, so it can only ever produce weak positive
+// evidence. The reason it is weak is *not* "x86 is TSO, so the reordering
+// cannot happen" -- that is false. TSO permits exactly the StoreLoad
+// reordering the bug needs, and the litmus shape does reproduce on x86_64
+// (measured 1 and 127 positives per 10^6 with litmus7). What plausibly hides
+// it in the *real* code is narrower: the reclaim path's mutex performs locked
+// RMWs between the removal store and the record scan. So the honest claim is
+// "unlikely to reproduce on x86, for a reason specific to the lock".
+void test05()
+{
+ constexpr int kReaders = 6;
+ std::atomic<Node*> shared{new Node(42)};
+ std::atomic<bool> stop{false};
+ std::atomic<int> observed{kNothingObserved};
+
+ std::vector<std::thread> threads;
+ threads.reserve(kReaders + 1);
+
+ for (int i = 0; i < kReaders; ++i)
+ threads.emplace_back(reader_fn, std::ref(shared), std::ref(stop),
+ std::ref(observed));
+ threads.emplace_back(writer_fn, std::ref(shared), std::ref(stop));
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(300));
+ stop.store(true, std::memory_order_release);
+ for (auto& t : threads)
+ t.join();
+
+ hpd::_S_default_domain()._M_synchronize();
+ // No reader ever observed a node; the test proved nothing.
+ VERIFY( observed.load() != kNothingObserved );
+}
+
+int main()
+{
+ test01();
+ test02();
+ test03();
+ test04();
+ test05();
+}
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/ctor.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/ctor.cc
new file mode 100644
index 00000000000..791b3cdbadc
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/ctor.cc
@@ -0,0 +1,102 @@
+// { dg-do run { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+#include <hazard_pointer>
+#include <utility>
+#include <testsuite_hooks.h>
+
+namespace hpd = std::__hazard_pointer;
+
+// Default-constructed handle is empty.
+void test01()
+{
+ const std::hazard_pointer hp;
+ VERIFY( hp.empty() );
+}
+
+// Default-constructed handle does not increment active slot count.
+void test02()
+{
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ const std::hazard_pointer hp;
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before );
+}
+
+// Move ctor transfers slot ownership: count unchanged.
+void test03()
+{
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ auto a = std::make_hazard_pointer();
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before + 1 );
+ const std::hazard_pointer b = std::move(a);
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before + 1 );
+}
+
+// Move ctor: source becomes empty.
+void test04()
+{
+ auto a = std::make_hazard_pointer();
+ const std::hazard_pointer b = std::move(a);
+ VERIFY( a.empty() );
+ VERIFY( !b.empty() );
+}
+
+// Destructor releases slot.
+void test05()
+{
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ {
+ auto hp = std::make_hazard_pointer();
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before + 1 );
+ }
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before );
+}
+
+// Destructor on default-constructed handle is a no-op.
+void test06()
+{
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ {
+ const std::hazard_pointer hp;
+ }
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before );
+}
+
+// Multiple hazard_pointers are distinct slots.
+void test07()
+{
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ auto a = std::make_hazard_pointer();
+ auto b = std::make_hazard_pointer();
+ auto c = std::make_hazard_pointer();
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before + 3 );
+}
+
+int main()
+{
+ test01();
+ test02();
+ test03();
+ test04();
+ test05();
+ test06();
+ test07();
+}
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/custom_deleter.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/custom_deleter.cc
new file mode 100644
index 00000000000..36d143945a6
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/custom_deleter.cc
@@ -0,0 +1,126 @@
+// { dg-do run { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <testsuite_hooks.h>
+
+namespace hpd = std::__hazard_pointer;
+
+namespace
+{
+ struct LoggingNode;
+
+ struct LoggingDeleter
+ {
+ int* delete_count = nullptr;
+ LoggingDeleter() = default;
+ explicit LoggingDeleter(int* c) : delete_count(c) {}
+ void operator()(LoggingNode* p) const;
+ };
+
+ struct LoggingNode : std::hazard_pointer_obj_base<LoggingNode, LoggingDeleter> {};
+
+ inline void LoggingDeleter::operator()(LoggingNode* p) const
+ {
+ if (delete_count)
+ ++(*delete_count);
+ delete p;
+ }
+
+ struct TrackedNode : std::hazard_pointer_obj_base<TrackedNode>
+ {
+ explicit TrackedNode(int& c) : counter(c) {}
+ ~TrackedNode() { ++counter; }
+ int& counter;
+ };
+}
+
+// Default deleter calls delete.
+void test01()
+{
+ int dtor_count = 0;
+ auto* node = new TrackedNode(dtor_count);
+ node->retire();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor_count == 1 );
+}
+
+// Functor deleter is invoked.
+void test02()
+{
+ int delete_count = 0;
+ auto* node = new LoggingNode();
+ node->retire(LoggingDeleter{&delete_count});
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( delete_count == 1 );
+}
+
+// Functor deleter not called while protected.
+void test03()
+{
+ int delete_count = 0;
+ auto* node = new LoggingNode();
+ const std::atomic<LoggingNode*> src{node};
+
+ auto hp = std::make_hazard_pointer();
+ (void)hp.protect(src);
+ node->retire(LoggingDeleter{&delete_count});
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( delete_count == 0 );
+
+ hp.reset_protection();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( delete_count == 1 );
+}
+
+// retire() with no arg uses default-constructed D.
+void test04()
+{
+ int dtor_count = 0;
+ auto* node = new TrackedNode(dtor_count);
+ node->retire();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor_count == 1 );
+}
+
+// Multiple nodes with custom deleter all invoked.
+void test05()
+{
+ constexpr int kCount = 5;
+ int delete_count = 0;
+ for (int i = 0; i < kCount; ++i)
+ {
+ auto* node = new LoggingNode();
+ node->retire(LoggingDeleter{&delete_count});
+ }
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( delete_count == kCount );
+}
+
+int main()
+{
+ test01();
+ test02();
+ test03();
+ test04();
+ test05();
+}
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/empty.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/empty.cc
new file mode 100644
index 00000000000..afd53c8bd8a
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/empty.cc
@@ -0,0 +1,105 @@
+// { dg-do run { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <utility>
+#include <testsuite_hooks.h>
+
+namespace
+{
+ struct Node : std::hazard_pointer_obj_base<Node> {};
+}
+
+void test01()
+{
+ const std::hazard_pointer hp;
+ VERIFY( hp.empty() );
+}
+
+void test02()
+{
+ auto hp = std::make_hazard_pointer();
+ VERIFY( !hp.empty() );
+}
+
+// After move ctor: source empty, dest non-empty.
+void test03()
+{
+ auto a = std::make_hazard_pointer();
+ const std::hazard_pointer b = std::move(a);
+ VERIFY( a.empty() );
+ VERIFY( !b.empty() );
+}
+
+// After move-assign: source empty.
+void test04()
+{
+ auto a = std::make_hazard_pointer();
+ std::hazard_pointer b;
+ b = std::move(a);
+ VERIFY( a.empty() );
+ VERIFY( !b.empty() );
+}
+
+// After protect(): still owns slot.
+void test05()
+{
+ auto hp = std::make_hazard_pointer();
+ Node x;
+ const std::atomic<Node*> src{&x};
+ (void)hp.protect(src);
+ VERIFY( !hp.empty() );
+}
+
+// After reset_protection(): hazard cleared, slot kept.
+void test06()
+{
+ auto hp = std::make_hazard_pointer();
+ Node x;
+ const std::atomic<Node*> src{&x};
+ (void)hp.protect(src);
+ hp.reset_protection();
+ VERIFY( !hp.empty() );
+}
+
+// After swap: ownership exchanged.
+void test07()
+{
+ auto a = std::make_hazard_pointer();
+ std::hazard_pointer b;
+ VERIFY( !a.empty() );
+ VERIFY( b.empty() );
+ a.swap(b);
+ VERIFY( a.empty() );
+ VERIFY( !b.empty() );
+}
+
+int main()
+{
+ test01();
+ test02();
+ test03();
+ test04();
+ test05();
+ test06();
+ test07();
+}
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/layout.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/layout.cc
new file mode 100644
index 00000000000..3380ab4f99c
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/layout.cc
@@ -0,0 +1,85 @@
+// { dg-do compile { target c++26 } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+// hazard_pointer_obj_base is a standard-specified type that users derive
+// from, so its size is baked into user binaries rather than only into
+// libstdc++.so, and doc/xml/manual/abi.xml lists changing the layout of a
+// standard-specified type as a prohibited change. hazard_pointer is frozen
+// for the same reason, and must stay one word: the reserved domain pointer
+// deliberately lives in the record rather than in the handle, so that adding
+// custom domains later never has to widen it (P2530R3 sec. 1.5 item 3).
+//
+// The header carries the same assertions, so this file is not what stops an
+// accidental change -- it is what makes the freeze visible in the testsuite
+// rather than only in a header a reader may not open.
+//
+// The numbers are per-ABI, so they are guarded on 8-byte pointers rather than
+// generalised: any formula portable enough to hold everywhere would have to
+// be derived from the members, which would make the assertion follow the code
+// instead of pinning it.
+
+#include <hazard_pointer>
+#include <memory>
+#include <type_traits>
+
+namespace
+{
+ struct Probe : std::hazard_pointer_obj_base<Probe> { };
+
+ struct FunctorProbe;
+
+ struct Deleting
+ {
+ void operator()(FunctorProbe* p) const;
+ };
+
+ // A stateless deleter must not cost a word: [[__no_unique_address__]] on
+ // the deleter member is what pays for the space P2530R3 sec. 1.5 reserves.
+ struct FunctorProbe
+ : std::hazard_pointer_obj_base<FunctorProbe, Deleting> { };
+
+ using Base = std::hazard_pointer_obj_base<Probe>;
+ using FunctorBase = std::hazard_pointer_obj_base<FunctorProbe, Deleting>;
+}
+
+#if __SIZEOF_POINTER__ == 8
+static_assert(sizeof(Base) == 32);
+static_assert(alignof(Base) == 8);
+static_assert(sizeof(FunctorBase) == sizeof(Base));
+static_assert(sizeof(std::hazard_pointer) == 8);
+#endif
+
+// Not trivially copyable, and deliberately so. Two independent causes:
+// reserving anything makes the default constructor non-trivial, and the
+// not-retired sentinel needs user-provided copy and move, or retiring a copy
+// would look like a double retire. No layout is both trivially copyable and
+// correct: a trivial copy necessarily copies the retirement state.
+static_assert(!std::is_trivially_copyable_v<Base>);
+
+// Still nothrow default-constructible and nothrow destructible, which the
+// synopsis requires of the protected base.
+static_assert(std::is_nothrow_destructible_v<Probe>);
+static_assert(std::is_nothrow_default_constructible_v<Probe>);
+
+// The handle is movable but not copyable.
+static_assert(std::is_nothrow_move_constructible_v<std::hazard_pointer>);
+static_assert(std::is_nothrow_move_assignable_v<std::hazard_pointer>);
+static_assert(!std::is_copy_constructible_v<std::hazard_pointer>);
+static_assert(!std::is_copy_assignable_v<std::hazard_pointer>);
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/make_hazard_pointer.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/make_hazard_pointer.cc
new file mode 100644
index 00000000000..9055f3a57f2
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/make_hazard_pointer.cc
@@ -0,0 +1,84 @@
+// { dg-do run { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+#include <hazard_pointer>
+#include <cstddef>
+#include <vector>
+#include <testsuite_hooks.h>
+
+namespace hpd = std::__hazard_pointer;
+
+// Returns non-empty handle.
+void test01()
+{
+ auto hp = std::make_hazard_pointer();
+ VERIFY( !hp.empty() );
+}
+
+// Increments active slot count by 1.
+void test02()
+{
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ auto hp = std::make_hazard_pointer();
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before + 1 );
+}
+
+// Multiple calls return distinct slots.
+void test03()
+{
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ auto a = std::make_hazard_pointer();
+ auto b = std::make_hazard_pointer();
+ auto c = std::make_hazard_pointer();
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before + 3 );
+}
+
+// Slot reused after handle destruction.
+void test04()
+{
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ {
+ auto hp = std::make_hazard_pointer();
+ }
+ auto hp2 = std::make_hazard_pointer();
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before + 1 );
+}
+
+// Pool grows: there is no initial capacity, records are created on demand.
+void test05()
+{
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ constexpr std::size_t kExtra = 8 + 4;
+ std::vector<std::hazard_pointer> hps;
+ hps.reserve(kExtra);
+ for (std::size_t i = 0; i < kExtra; ++i)
+ hps.emplace_back(std::make_hazard_pointer());
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before + kExtra );
+}
+
+int main()
+{
+ test01();
+ test02();
+ test03();
+ test04();
+ test05();
+}
\ No newline at end of file
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/move_assign.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/move_assign.cc
new file mode 100644
index 00000000000..7f4e40c1cda
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/move_assign.cc
@@ -0,0 +1,91 @@
+// { dg-do run { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+#include <hazard_pointer>
+#include <utility>
+#include <testsuite_hooks.h>
+
+namespace hpd = std::__hazard_pointer;
+
+// Transfers slot ownership.
+void test01()
+{
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ auto a = std::make_hazard_pointer();
+ std::hazard_pointer b;
+ VERIFY( !a.empty() );
+ VERIFY( b.empty() );
+ b = std::move(a);
+ VERIFY( a.empty() );
+ VERIFY( !b.empty() );
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before + 1 );
+}
+
+// Source becomes empty after move-assign.
+void test02()
+{
+ auto a = std::make_hazard_pointer();
+ std::hazard_pointer b;
+ b = std::move(a);
+ VERIFY( a.empty() );
+}
+
+// Destination releases prior slot.
+void test03()
+{
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ auto a = std::make_hazard_pointer();
+ auto b = std::make_hazard_pointer();
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before + 2 );
+ b = std::move(a);
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before + 1 );
+}
+
+// Self-assignment is a no-op.
+void test04()
+{
+ auto hp = std::make_hazard_pointer();
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ auto& alias = hp;
+ hp = std::move(alias);
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before );
+ VERIFY( !hp.empty() );
+}
+
+// Move-assign empty to empty: both stay empty, no slot change.
+void test05()
+{
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ std::hazard_pointer a;
+ std::hazard_pointer b;
+ b = std::move(a);
+ VERIFY( b.empty() );
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before );
+}
+
+int main()
+{
+ test01();
+ test02();
+ test03();
+ test04();
+ test05();
+}
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/nodiscard_neg.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/nodiscard_neg.cc
new file mode 100644
index 00000000000..eb725da6790
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/nodiscard_neg.cc
@@ -0,0 +1,58 @@
+// { dg-do compile { target c++26 } }
+// { dg-additional-options "-pthread -Werror=unused-result" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+// Negative tests: each [[nodiscard]] return discard must trip
+// -Werror=unused-result.
+
+#include <hazard_pointer>
+#include <atomic>
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+void test_make_hazard_pointer()
+{
+ std::make_hazard_pointer(); // { dg-error "ignoring return value" }
+}
+
+void test_empty()
+{
+ auto hp = std::make_hazard_pointer();
+ hp.empty(); // { dg-error "ignoring return value" }
+}
+
+void test_protect()
+{
+ auto hp = std::make_hazard_pointer();
+ Node x;
+ std::atomic<Node*> src{&x};
+ hp.protect(src); // { dg-error "ignoring return value" }
+}
+
+void test_try_protect()
+{
+ auto hp = std::make_hazard_pointer();
+ Node x;
+ std::atomic<Node*> src{&x};
+ Node* ptr = &x;
+ hp.try_protect(ptr, src); // { dg-error "ignoring return value" }
+}
+
+// { dg-prune-output "some warnings being treated as errors" }
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/noexcept.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/noexcept.cc
new file mode 100644
index 00000000000..cc19bc309cd
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/noexcept.cc
@@ -0,0 +1,56 @@
+// { dg-do compile { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <type_traits>
+#include <utility>
+
+namespace
+{
+ struct Node : std::hazard_pointer_obj_base<Node> {};
+ using HP = std::hazard_pointer;
+
+ static_assert(std::is_nothrow_default_constructible_v<HP>);
+ static_assert(std::is_nothrow_move_constructible_v<HP>);
+ static_assert(std::is_nothrow_move_assignable_v<HP>);
+
+ static_assert(noexcept(std::declval<const HP&>().empty()));
+
+ static_assert(noexcept(std::declval<HP&>().protect(
+ std::declval<std::atomic<Node*>&>())));
+
+ static_assert(noexcept(std::declval<HP&>().try_protect(
+ std::declval<Node*&>(),
+ std::declval<const std::atomic<Node*>&>())));
+
+ static_assert(noexcept(std::declval<HP&>().reset_protection()));
+ static_assert(noexcept(std::declval<HP&>().reset_protection(nullptr)));
+
+ static_assert(noexcept(std::declval<HP&>().reset_protection(
+ std::declval<const Node*>())));
+
+ static_assert(noexcept(std::declval<HP&>().swap(std::declval<HP&>())));
+
+ static_assert(noexcept(std::swap(std::declval<HP&>(), std::declval<HP&>())));
+
+ static_assert(noexcept(std::declval<Node&>().retire()));
+}
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/protect.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/protect.cc
new file mode 100644
index 00000000000..19f38292263
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/protect.cc
@@ -0,0 +1,96 @@
+// { dg-do run { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <testsuite_hooks.h>
+
+namespace
+{
+ struct Node : std::hazard_pointer_obj_base<Node>
+ {
+ int value = 0;
+ };
+}
+
+// Returns current value of the atomic.
+void test01()
+{
+ auto hp = std::make_hazard_pointer();
+ Node x;
+ x.value = 42;
+ const std::atomic<Node*> src{&x};
+ const Node* const p = hp.protect(src);
+ VERIFY( p == &x );
+ VERIFY( p->value == 42 );
+}
+
+// Returns nullptr for null atomic.
+void test02()
+{
+ auto hp = std::make_hazard_pointer();
+ const std::atomic<Node*> src{nullptr};
+ VERIFY( hp.protect(src) == nullptr );
+}
+
+// Slot not empty after protect.
+void test03()
+{
+ auto hp = std::make_hazard_pointer();
+ Node x;
+ const std::atomic<Node*> src{&x};
+ (void)hp.protect(src);
+ VERIFY( !hp.empty() );
+}
+
+// protect() matches atomic load.
+void test04()
+{
+ auto hp = std::make_hazard_pointer();
+ Node vals[3];
+ vals[0].value = 1;
+ vals[1].value = 2;
+ vals[2].value = 3;
+ const std::atomic<Node*> src{&vals[0]};
+ const Node* const p = hp.protect(src);
+ VERIFY( p == src.load() );
+}
+
+// Re-protect on the same handle updates the slot.
+void test05()
+{
+ auto hp = std::make_hazard_pointer();
+ Node a, b;
+ std::atomic<Node*> src{&a};
+ (void)hp.protect(src);
+ src.store(&b, std::memory_order_relaxed);
+ const Node* const p = hp.protect(src);
+ VERIFY( p == &b );
+}
+
+int main()
+{
+ test01();
+ test02();
+ test03();
+ test04();
+ test05();
+}
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/protect_neg.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/protect_neg.cc
new file mode 100644
index 00000000000..87c2e26b50f
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/protect_neg.cc
@@ -0,0 +1,43 @@
+// { dg-do compile { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+// Negative tests: protect<T>() must be rejected when T is not _Protectable.
+
+#include <hazard_pointer>
+#include <atomic>
+
+struct Plain {};
+
+void test_int()
+{
+ auto hp = std::make_hazard_pointer();
+ std::atomic<int*> src{nullptr};
+ (void)hp.protect(src); // int is not _Protectable
+}
+
+void test_nonhazard_class()
+{
+ auto hp = std::make_hazard_pointer();
+ std::atomic<Plain*> src{nullptr};
+ (void)hp.protect(src); // Plain has no _Hazptr_obj base
+}
+
+// { dg-error "hazard-protectable type" "" { target *-*-* } 0 }
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/reset_protection.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/reset_protection.cc
new file mode 100644
index 00000000000..5dddef71611
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/reset_protection.cc
@@ -0,0 +1,116 @@
+// { dg-do run { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <testsuite_hooks.h>
+
+namespace hpd = std::__hazard_pointer;
+
+namespace
+{
+ struct Node : std::hazard_pointer_obj_base<Node> {};
+
+ struct Tracked : std::hazard_pointer_obj_base<Tracked>
+ {
+ explicit Tracked(int& c) : counter(c) {}
+ ~Tracked() { ++counter; }
+ int& counter;
+ };
+}
+
+// reset_protection clears hazard, slot still owned.
+void test01()
+{
+ auto hp = std::make_hazard_pointer();
+ Node x;
+ const std::atomic<Node*> src{&x};
+ (void)hp.protect(src);
+ hp.reset_protection();
+ VERIFY( !hp.empty() );
+}
+
+// nullptr overload clears hazard.
+void test02()
+{
+ auto hp = std::make_hazard_pointer();
+ Node x;
+ const std::atomic<Node*> src{&x};
+ (void)hp.protect(src);
+ hp.reset_protection(nullptr);
+ VERIFY( !hp.empty() );
+}
+
+// After reset, object can be reclaimed.
+void test03()
+{
+ auto hp = std::make_hazard_pointer();
+ int dtor_count = 0;
+ auto* obj = new Tracked(dtor_count);
+ const std::atomic<Tracked*> src{obj};
+ (void)hp.protect(src);
+
+ obj->retire();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor_count == 0 );
+
+ hp.reset_protection();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor_count == 1 );
+}
+
+// Typed-pointer overload publishes a hazard.
+void test04()
+{
+ auto hp = std::make_hazard_pointer();
+ int dtor = 0;
+ auto* obj = new Tracked(dtor);
+ hp.reset_protection(obj);
+
+ obj->retire();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor == 0 );
+
+ hp.reset_protection();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor == 1 );
+}
+
+// Double reset is safe (idempotent).
+void test05()
+{
+ auto hp = std::make_hazard_pointer();
+ Node x;
+ const std::atomic<Node*> src{&x};
+ (void)hp.protect(src);
+ hp.reset_protection();
+ hp.reset_protection();
+ VERIFY( !hp.empty() );
+}
+
+int main()
+{
+ test01();
+ test02();
+ test03();
+ test04();
+ test05();
+}
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/retire.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/retire.cc
new file mode 100644
index 00000000000..afcc97f2ca2
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/retire.cc
@@ -0,0 +1,119 @@
+// { dg-do run { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <testsuite_hooks.h>
+
+namespace hpd = std::__hazard_pointer;
+
+namespace
+{
+ struct Tracked : std::hazard_pointer_obj_base<Tracked>
+ {
+ explicit Tracked(int& counter) : counter_(counter) {}
+ ~Tracked() { ++counter_; }
+ int& counter_;
+ };
+}
+
+// Retired object deleted after synchronize.
+void test01()
+{
+ // Hold one active slot so the auto-sync threshold (2 * active_count) >= 2,
+ // ensuring the single retire below does NOT auto-sync.
+ auto hp = std::make_hazard_pointer();
+ int dtor_count = 0;
+ (new Tracked(dtor_count))->retire();
+ VERIFY( dtor_count == 0 );
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor_count == 1 );
+}
+
+// Multiple retired objects all deleted on synchronize.
+void test02()
+{
+ int dtor_count = 0;
+ for (int i = 0; i < 5; ++i)
+ (new Tracked(dtor_count))->retire();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor_count == 5 );
+}
+
+// Protected object survives synchronize.
+void test03()
+{
+ int dtor_count = 0;
+ auto* obj = new Tracked(dtor_count);
+ const std::atomic<Tracked*> src{obj};
+
+ auto hp = std::make_hazard_pointer();
+ const Tracked* const protected_obj = hp.protect(src);
+ VERIFY( protected_obj == obj );
+
+ obj->retire();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor_count == 0 );
+
+ hp.reset_protection();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor_count == 1 );
+}
+
+// Retire list grows before synchronize.
+void test04()
+{
+ auto hp = std::make_hazard_pointer();
+ const auto before = hpd::_S_default_domain()._M_retire_list_size();
+ int dtor_count = 0;
+ (new Tracked(dtor_count))->retire();
+ (new Tracked(dtor_count))->retire();
+ VERIFY( hpd::_S_default_domain()._M_retire_list_size() == before + 2 );
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( hpd::_S_default_domain()._M_retire_list_size() == 0u );
+ VERIFY( dtor_count == 2 );
+}
+
+// Threshold auto-triggers synchronize: 1 active slot -> threshold 2 -> 3 retires trip it.
+void test05()
+{
+ auto hp = std::make_hazard_pointer();
+ int dummy = 0;
+ Tracked x{dummy};
+ const std::atomic<Tracked*> src{&x};
+ const Tracked* const protected_x = hp.protect(src);
+ VERIFY( protected_x == &x );
+
+ int dtor_count = 0;
+ for (int i = 0; i < 3; ++i)
+ (new Tracked(dtor_count))->retire();
+
+ VERIFY( dtor_count > 0 );
+}
+
+int main()
+{
+ test01();
+ test02();
+ test03();
+ test04();
+ test05();
+}
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/retire_neg.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/retire_neg.cc
new file mode 100644
index 00000000000..9b1c952f3af
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/retire_neg.cc
@@ -0,0 +1,39 @@
+// { dg-do compile { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+// Negative test: retire() must be rejected when hazard_pointer_obj_base
+// is a private base of T (is_convertible_v<T*, base*> fails).
+
+#include <hazard_pointer>
+
+struct Node : private std::hazard_pointer_obj_base<Node>
+{
+ using hazard_pointer_obj_base::retire;
+};
+
+void test()
+{
+ auto* p = new Node();
+ p->retire();
+}
+
+// { dg-error "must be a public base of T" "" { target *-*-* } 0 }
+// { dg-prune-output "inaccessible base of" }
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/retire_no_alloc.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/retire_no_alloc.cc
new file mode 100644
index 00000000000..a99c89cc90f
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/retire_no_alloc.cc
@@ -0,0 +1,213 @@
+// { dg-do run { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+// [saferecl.hp.base] declares retire() noexcept, so it must not be able to
+// fail. An implementation that pushes onto a std::vector makes OOM inside a
+// noexcept function -- i.e. std::terminate(). This test replaces the global
+// allocation functions and asserts that the retire path performs no
+// allocation at all.
+//
+// Only the four unsized/sized plain overloads are replaced; the aligned forms
+// are left alone, so the cache-line-aligned hazard pointer records are not
+// counted. That is deliberate and does not weaken the claim: records are
+// created only by make_hazard_pointer(), which [saferecl.hp.holder.ctor]/3
+// explicitly allows to throw bad_alloc, and never by retire().
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cstddef>
+#include <cstdlib>
+#include <new>
+#include <vector>
+#include <testsuite_hooks.h>
+
+namespace hpd = std::__hazard_pointer;
+
+namespace
+{
+ std::atomic<bool> armed{false};
+ std::atomic<int> allocations{0};
+
+ void note_allocation() noexcept
+ {
+ if (armed.load(std::memory_order_relaxed))
+ allocations.fetch_add(1, std::memory_order_relaxed);
+ }
+
+ struct Node : std::hazard_pointer_obj_base<Node>
+ {
+ int value = 0;
+ };
+}
+
+// Replaced globals. malloc/free are paired consistently across all four, so
+// mixing with the untouched aligned overloads is safe.
+//
+// __gnu_test::counter from testsuite/util/replacement_memory_operators.h was
+// considered and does not fit: its destructor throws when the live count is
+// non-zero at exit, and the hazard pointer records are freed in ~_Domain
+// during static-storage destruction, which need not run before it.
+void* operator new(std::size_t n)
+{
+ note_allocation();
+ void* const p = std::malloc(n == 0 ? 1 : n);
+ if (p == nullptr)
+ throw std::bad_alloc();
+ return p;
+}
+
+void* operator new[](std::size_t n)
+{ return ::operator new(n); }
+
+void operator delete(void* p) noexcept
+{ std::free(p); }
+
+void operator delete[](void* p) noexcept
+{ std::free(p); }
+
+void operator delete(void* p, std::size_t) noexcept
+{ std::free(p); }
+
+void operator delete[](void* p, std::size_t) noexcept
+{ std::free(p); }
+
+// The counter is only evidence if it can actually count. A green run of the
+// tests below proves nothing unless the harness is known to fire. Note that
+// the pair below is ::operator new / ::operator delete rather than a
+// new-expression: the compiler is allowed to elide the latter, and an elided
+// control proves nothing.
+void test01()
+{
+ static std::atomic<void*> sink{nullptr};
+
+ allocations.store(0, std::memory_order_relaxed);
+ armed.store(true, std::memory_order_relaxed);
+ sink.store(::operator new(64), std::memory_order_relaxed);
+ armed.store(false, std::memory_order_relaxed);
+ ::operator delete(sink.exchange(nullptr, std::memory_order_relaxed));
+
+ VERIFY( allocations.load(std::memory_order_relaxed) == 1 );
+}
+
+// retire() is a pointer splice and allocates nothing.
+void test02()
+{
+ // Hold hazard pointers so the auto-synchronize threshold (> 2 * active
+ // count) is not crossed by the retires below. Acquiring them may allocate;
+ // that happens before arming.
+ constexpr int kHandles = 4;
+ std::vector<std::hazard_pointer> hps;
+ hps.reserve(kHandles);
+ for (int i = 0; i < kHandles; ++i)
+ hps.push_back(std::make_hazard_pointer());
+
+ // Drain leftovers and force this thread's retire-list node to register, so
+ // that neither happens inside the measured region.
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( hpd::_S_default_domain()._M_retire_list_size() == 0 );
+
+ constexpr int kRetires = 2 * kHandles; // threshold is >, so this stays under
+ std::vector<Node*> nodes;
+ nodes.reserve(kRetires);
+ for (int i = 0; i < kRetires; ++i)
+ nodes.push_back(new Node());
+
+ allocations.store(0, std::memory_order_relaxed);
+ armed.store(true, std::memory_order_relaxed);
+ for (Node* n : nodes)
+ n->retire();
+ armed.store(false, std::memory_order_relaxed);
+
+ // retire() allocated; the retire list is not purely intrusive.
+ VERIFY( allocations.load(std::memory_order_relaxed) == 0 );
+ // Proves the retires actually landed, rather than the count being zero
+ // because nothing happened.
+ VERIFY( hpd::_S_default_domain()._M_retire_list_size()
+ == std::size_t(kRetires) );
+
+ hps.clear();
+ hpd::_S_default_domain()._M_synchronize();
+}
+
+// The other half of the guarantee: retire() may auto-synchronize, so a
+// reclamation that allocates puts the allocation back on retire()'s path.
+// Records live in a list the scan walks directly, and the protected-set
+// buffer is owned by the thread and reused, so once it has been sized a
+// reclamation allocates nothing at all.
+void test03()
+{
+ constexpr int kHandles = 4;
+ std::vector<std::hazard_pointer> hps;
+ hps.reserve(kHandles);
+ for (int i = 0; i < kHandles; ++i)
+ hps.push_back(std::make_hazard_pointer());
+
+ // Warm-up: the first scan on this thread sizes the buffer, and the records
+ // themselves are created on demand. Both are allowed to allocate -- the
+ // claim is about the steady state, so reach it before measuring.
+ for (int i = 0; i < 4; ++i)
+ (new Node())->retire();
+ hpd::_S_default_domain()._M_synchronize();
+ hpd::_S_default_domain()._M_synchronize();
+
+ std::vector<Node*> nodes;
+ nodes.reserve(8);
+ for (int i = 0; i < 8; ++i)
+ nodes.push_back(new Node());
+
+ allocations.store(0, std::memory_order_relaxed);
+ armed.store(true, std::memory_order_relaxed);
+ for (Node* n : nodes)
+ n->retire();
+ hpd::_S_default_domain()._M_synchronize();
+ armed.store(false, std::memory_order_relaxed);
+
+ // Reclamation allocated in the steady state.
+ VERIFY( allocations.load(std::memory_order_relaxed) == 0 );
+ // Nothing was actually reclaimed.
+ VERIFY( hpd::_S_default_domain()._M_retire_list_size() == 0 );
+}
+
+// And reclamation still happens at all.
+void test04()
+{
+ int destroyed = 0;
+ struct Counted : std::hazard_pointer_obj_base<Counted>
+ {
+ int* sink;
+ explicit Counted(int* s) : sink(s) {}
+ ~Counted() { ++*sink; }
+ };
+
+ for (int i = 0; i < 3; ++i)
+ (new Counted(&destroyed))->retire();
+ hpd::_S_default_domain()._M_synchronize();
+
+ VERIFY( destroyed == 3 );
+}
+
+int main()
+{
+ test01();
+ test02();
+ test03();
+ test04();
+}
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/retire_virtual_neg.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/retire_virtual_neg.cc
new file mode 100644
index 00000000000..7275a7b3f10
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/retire_virtual_neg.cc
@@ -0,0 +1,40 @@
+// { dg-do compile { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+// Negative test: retire() must be rejected when hazard_pointer_obj_base
+// is a virtual base of T. Requires C++26 std::is_virtual_base_of.
+
+#include <hazard_pointer>
+
+#ifndef __cpp_lib_is_virtual_base_of
+# error "Requires __cpp_lib_is_virtual_base_of for this negative check"
+#endif
+
+struct Node : virtual std::hazard_pointer_obj_base<Node> {};
+
+void test()
+{
+ auto* p = new Node();
+ p->retire();
+}
+
+// { dg-error "must be a non-virtual base" "" { target *-*-* } 0 }
+// { dg-prune-output "cannot convert from pointer to base class" }
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/swap.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/swap.cc
new file mode 100644
index 00000000000..2d516d0bfc7
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/swap.cc
@@ -0,0 +1,133 @@
+// { dg-do run { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <testsuite_hooks.h>
+
+namespace hpd = std::__hazard_pointer;
+
+namespace
+{
+ struct Tracked : std::hazard_pointer_obj_base<Tracked>
+ {
+ explicit Tracked(int& c) : counter(c) {}
+ ~Tracked() { ++counter; }
+ int& counter;
+ };
+}
+
+// Member swap exchanges non-empty and empty.
+void test01()
+{
+ auto a = std::make_hazard_pointer();
+ std::hazard_pointer b;
+ VERIFY( !a.empty() );
+ VERIFY( b.empty() );
+ a.swap(b);
+ VERIFY( a.empty() );
+ VERIFY( !b.empty() );
+}
+
+// Member swap both non-empty: slot count unchanged.
+void test02()
+{
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ auto a = std::make_hazard_pointer();
+ auto b = std::make_hazard_pointer();
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before + 2 );
+ a.swap(b);
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before + 2 );
+}
+
+// After swap, the protecting slot moves to the other handle.
+void test03()
+{
+ auto hp1 = std::make_hazard_pointer();
+ auto hp2 = std::make_hazard_pointer();
+
+ int dtor = 0;
+ auto* obj = new Tracked(dtor);
+ const std::atomic<Tracked*> src{obj};
+ (void)hp1.protect(src);
+
+ hp1.swap(hp2);
+
+ obj->retire();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor == 0 );
+
+ hp2.reset_protection();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor == 1 );
+}
+
+// Free swap exchanges slots.
+void test04()
+{
+ auto a = std::make_hazard_pointer();
+ std::hazard_pointer b;
+ VERIFY( !a.empty() );
+ VERIFY( b.empty() );
+ std::swap(a, b);
+ VERIFY( a.empty() );
+ VERIFY( !b.empty() );
+}
+
+// Free swap both non-empty: count unchanged.
+void test05()
+{
+ const auto before = hpd::_S_default_domain()._M_active_slots();
+ auto a = std::make_hazard_pointer();
+ auto b = std::make_hazard_pointer();
+ std::swap(a, b);
+ VERIFY( hpd::_S_default_domain()._M_active_slots() == before + 2 );
+}
+
+// Member self-swap is a no-op.
+void test06()
+{
+ auto hp = std::make_hazard_pointer();
+ VERIFY( !hp.empty() );
+ hp.swap(hp);
+ VERIFY( !hp.empty() );
+}
+
+// Free swap both empty: remains empty.
+void test07()
+{
+ std::hazard_pointer a;
+ std::hazard_pointer b;
+ std::swap(a, b);
+ VERIFY( a.empty() );
+ VERIFY( b.empty() );
+}
+
+int main()
+{
+ test01();
+ test02();
+ test03();
+ test04();
+ test05();
+ test06();
+ test07();
+}
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/thread_exit.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/thread_exit.cc
new file mode 100644
index 00000000000..cabf95eaeef
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/thread_exit.cc
@@ -0,0 +1,98 @@
+// { dg-do run { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <thread>
+#include <testsuite_hooks.h>
+
+namespace hpd = std::__hazard_pointer;
+
+namespace
+{
+ struct Tracked : std::hazard_pointer_obj_base<Tracked>
+ {
+ explicit Tracked(int& c) : counter(c) {}
+ ~Tracked() { ++counter; }
+ int& counter;
+ };
+}
+
+// Retired objects in a worker thread reclaimed on thread exit.
+void test01()
+{
+ int dtor_count = 0;
+ std::thread t([&] {
+ for (int i = 0; i < 3; ++i)
+ (new Tracked(dtor_count))->retire();
+ });
+ t.join();
+ VERIFY( dtor_count == 3 );
+}
+
+// Protected survivors offloaded to orphan list, reclaimed by later synchronize.
+void test02()
+{
+ int dtor_count = 0;
+ auto* obj = new Tracked(dtor_count);
+ const std::atomic<Tracked*> src{obj};
+
+ auto hp = std::make_hazard_pointer();
+ (void)hp.protect(src);
+
+ std::thread t([&] {
+ obj->retire();
+ });
+ t.join();
+
+ VERIFY( dtor_count == 0 );
+
+ hp.reset_protection();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor_count == 1 );
+}
+
+// Many threads each drain own retire list on exit.
+void test03()
+{
+ constexpr int kThreads = 4;
+ constexpr int kObjectsPerThread = 5;
+ int dtor_counts[kThreads] = {};
+
+ std::thread threads[kThreads];
+ for (int i = 0; i < kThreads; ++i)
+ threads[i] = std::thread([&dtor_counts, i] {
+ for (int j = 0; j < kObjectsPerThread; ++j)
+ (new Tracked(dtor_counts[i]))->retire();
+ });
+ for (auto& t : threads)
+ t.join();
+
+ for (int i = 0; i < kThreads; ++i)
+ VERIFY( dtor_counts[i] == kObjectsPerThread );
+}
+
+int main()
+{
+ test01();
+ test02();
+ test03();
+}
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/try_protect.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/try_protect.cc
new file mode 100644
index 00000000000..4af3192a2ad
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/try_protect.cc
@@ -0,0 +1,132 @@
+// { dg-do run { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <testsuite_hooks.h>
+
+namespace hpd = std::__hazard_pointer;
+
+namespace
+{
+ struct Node : std::hazard_pointer_obj_base<Node>
+ {
+ int value = 0;
+ };
+
+ struct Tracked : std::hazard_pointer_obj_base<Tracked>
+ {
+ explicit Tracked(int& c) : counter(c) {}
+ ~Tracked() { ++counter; }
+ int& counter;
+ };
+}
+
+// Returns true when pointer is stable.
+void test01()
+{
+ auto hp = std::make_hazard_pointer();
+ Node x;
+ const std::atomic<Node*> src{&x};
+ Node* ptr = src.load(std::memory_order::relaxed);
+ VERIFY( hp.try_protect(ptr, src) );
+}
+
+// Sets ptr on success.
+void test02()
+{
+ auto hp = std::make_hazard_pointer();
+ Node x;
+ x.value = 7;
+ const std::atomic<Node*> src{&x};
+ Node* ptr = src.load(std::memory_order::relaxed);
+ const bool ok = hp.try_protect(ptr, src);
+ VERIFY( ok );
+ VERIFY( ptr == &x );
+}
+
+// Returns true for null source.
+void test03()
+{
+ auto hp = std::make_hazard_pointer();
+ const std::atomic<Node*> src{nullptr};
+ Node* ptr = nullptr;
+ VERIFY( hp.try_protect(ptr, src) );
+ VERIFY( ptr == nullptr );
+}
+
+// Slot still owned after success.
+void test04()
+{
+ auto hp = std::make_hazard_pointer();
+ Node x;
+ const std::atomic<Node*> src{&x};
+ Node* ptr = &x;
+ (void)hp.try_protect(ptr, src);
+ VERIFY( !hp.empty() );
+}
+
+// Protected object not reclaimed after success.
+void test05()
+{
+ auto hp = std::make_hazard_pointer();
+ int dtor_count = 0;
+ auto* obj = new Tracked(dtor_count);
+ const std::atomic<Tracked*> src{obj};
+ Tracked* ptr = src.load(std::memory_order::relaxed);
+ VERIFY( hp.try_protect(ptr, src) );
+
+ obj->retire();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor_count == 0 );
+
+ hp.reset_protection();
+ hpd::_S_default_domain()._M_synchronize();
+ VERIFY( dtor_count == 1 );
+}
+
+// try_protect and protect agree on a stable pointer.
+void test06()
+{
+ Node x;
+ const std::atomic<Node*> src{&x};
+
+ auto hp1 = std::make_hazard_pointer();
+ auto hp2 = std::make_hazard_pointer();
+
+ Node* ptr = src.load(std::memory_order::relaxed);
+ const bool ok = hp1.try_protect(ptr, src);
+ const Node* const via_protect = hp2.protect(src);
+
+ VERIFY( ok );
+ VERIFY( ptr == via_protect );
+ VERIFY( ptr == &x );
+}
+
+int main()
+{
+ test01();
+ test02();
+ test03();
+ test04();
+ test05();
+ test06();
+}
diff --git a/libstdc++-v3/testsuite/30_threads/hazard_pointer/type_constraints.cc b/libstdc++-v3/testsuite/30_threads/hazard_pointer/type_constraints.cc
new file mode 100644
index 00000000000..60040bc3c71
--- /dev/null
+++ b/libstdc++-v3/testsuite/30_threads/hazard_pointer/type_constraints.cc
@@ -0,0 +1,56 @@
+// { dg-do compile { target c++26 } }
+// { dg-additional-options "-pthread" { target pthread } }
+// { dg-require-gthreads "" }
+
+// Copyright (C) 2026 Free Software Foundation, Inc.
+//
+// This file is part of the GNU ISO C++ Library. This library is free
+// software; you can redistribute it and/or modify it under the
+// terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 3, or (at your option)
+// any later version.
+
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License along
+// with this library; see the file COPYING3. If not see
+// <http://www.gnu.org/licenses/>.
+
+#include <hazard_pointer>
+#include <memory>
+#include <string>
+#include <type_traits>
+
+namespace
+{
+ struct SimpleNode : std::hazard_pointer_obj_base<SimpleNode> {};
+ struct CustomDelNode
+ : std::hazard_pointer_obj_base<CustomDelNode,
+ std::default_delete<CustomDelNode>> {};
+
+ // Indirect derivation must still satisfy _Protectable.
+ struct FurtherDerived : SimpleNode {};
+
+ // Not hazard-protectable.
+ struct PlainClass {};
+}
+
+// _Protectable concept (libstdc++ internal name for HazardProtectable).
+static_assert(std::__hazard_pointer::_Protectable<SimpleNode>);
+static_assert(std::__hazard_pointer::_Protectable<CustomDelNode>);
+static_assert(std::__hazard_pointer::_Protectable<FurtherDerived>);
+
+static_assert(!std::__hazard_pointer::_Protectable<int>);
+static_assert(!std::__hazard_pointer::_Protectable<void>);
+static_assert(!std::__hazard_pointer::_Protectable<int*>);
+
+static_assert(!std::__hazard_pointer::_Protectable<PlainClass>);
+static_assert(!std::__hazard_pointer::_Protectable<std::string>);
+
+// retire() deleter constraints.
+static_assert(std::is_invocable_v<std::default_delete<SimpleNode>, SimpleNode*>);
+static_assert(std::is_default_constructible_v<std::default_delete<SimpleNode>>);
+static_assert(std::is_move_assignable_v<std::default_delete<SimpleNode>>);
--
2.54.0