[libc-commits] [libc] [libc] Implement CheckedPtr utility (PR #234646)

via libc-commits libc-commits at lists.llvm.org
Thu Oct 22 07:49:42 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-libc

Author: Pavel Labath (labath)

<details>
<summary>Changes</summary>

This adds CheckedPtr, a wrapper around cpp::unique_ptr (along with a make_unique helper) that asserts in !NDEBUG builds that the pointer was checked for nullness before it is dereferenced, destroyed or overwritten (e.g., move assignment). This way, a missing check is caught even if the allocation succeeds.

The idea is replace AllocChecker (which was meant to serve a similar purpose, but the checking part didn't materialize so far). See <https://discourse.llvm.org/t/rfc-error-checking-apis-for-memory-allocation-and-management/92511> for more details.

A few notable design choices:
- operator bool() is non-const so the checked flag does not need to be mutable (avoiding data races on concurrent const reads)
- moving a CheckedPtr disarms the source and leaves the destination unchecked (similar to llvm::Error), forcing the new owner to check it
- the class wraps unique_ptr rather than replacing it, as I expect we will use for a non-checked owning pointer (CheckedPtr isn't particularly suitable for long term storage in a member variable).
- the API is intentionally minimal for now (no default constructor, get, release, reset, swap, or nullptr comparisons).

Assisted-by: Gemini

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


4 Files Affected:

- (modified) libc/src/__support/CMakeLists.txt (+15) 
- (added) libc/src/__support/checked_ptr.h (+200) 
- (modified) libc/test/src/__support/CMakeLists.txt (+14) 
- (added) libc/test/src/__support/checked_ptr_test.cpp (+297) 


``````````diff
diff --git a/libc/src/__support/CMakeLists.txt b/libc/src/__support/CMakeLists.txt
index dfb1ed0e13ff3..2324af89ca479 100644
--- a/libc/src/__support/CMakeLists.txt
+++ b/libc/src/__support/CMakeLists.txt
@@ -171,6 +171,21 @@ add_header_library(
     libc.src.__support.macros.config
 )
 
+add_header_library(
+  checked_ptr
+  HDRS
+    checked_ptr.h
+  DEPENDS
+    .alloc_checker
+    .libc_assert
+    libc.hdr.types.size_t
+    libc.src.__support.CPP.type_traits
+    libc.src.__support.CPP.unique_ptr
+    libc.src.__support.CPP.utility
+    libc.src.__support.macros.attributes
+    libc.src.__support.macros.config
+)
+
 add_header_library(
   ctype_utils
   HDRS
diff --git a/libc/src/__support/checked_ptr.h b/libc/src/__support/checked_ptr.h
new file mode 100644
index 0000000000000..3597ca7b05e3e
--- /dev/null
+++ b/libc/src/__support/checked_ptr.h
@@ -0,0 +1,200 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Smart pointer wrapper around cpp::unique_ptr enforcing null checks.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_CHECKED_PTR_H
+#define LLVM_LIBC_SRC___SUPPORT_CHECKED_PTR_H
+
+#include "hdr/types/size_t.h"
+#include "src/__support/CPP/type_traits.h"
+#include "src/__support/CPP/unique_ptr.h"
+#include "src/__support/CPP/utility.h"
+#include "src/__support/alloc-checker.h"
+#include "src/__support/libc_assert.h"
+#include "src/__support/macros/attributes.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+namespace detail {
+
+class LIBC_TRIVIAL_ABI CheckedState {
+#ifndef NDEBUG
+  bool checked = false;
+
+public:
+  LIBC_INLINE constexpr CheckedState() = default;
+
+  // Moving disarms `other` and starts `this` unchecked so the new owner must
+  // explicitly check its value.
+  LIBC_INLINE constexpr CheckedState(CheckedState &&other) {
+    other.checked = true;
+  }
+
+  // Overwriting an existing CheckedState requires it to have been checked,
+  // disarms `other`, and resets `this` to unchecked.
+  LIBC_INLINE constexpr CheckedState &operator=(CheckedState &&other) {
+    assert_checked();
+    // Self-move-assignment leaves the (checked) state unchanged.
+    if (this == &other)
+      return *this;
+    other.checked = true;
+    checked = false;
+    return *this;
+  }
+
+  LIBC_INLINE ~CheckedState() { assert_checked(); }
+
+  LIBC_INLINE constexpr void mark_checked() { checked = true; }
+  LIBC_INLINE constexpr void assert_checked() const { LIBC_ASSERT(checked); }
+#else
+public:
+  LIBC_INLINE constexpr CheckedState() = default;
+  LIBC_INLINE constexpr CheckedState(CheckedState &&) = default;
+  LIBC_INLINE constexpr CheckedState &operator=(CheckedState &&) = default;
+
+  LIBC_INLINE constexpr void mark_checked() {}
+  LIBC_INLINE constexpr void assert_checked() const {}
+#endif
+};
+
+} // namespace detail
+
+/// A smart pointer wrapper around cpp::unique_ptr that enforces (in !NDEBUG
+/// builds) that callers check pointer nullness before accessing or destroying
+/// the pointer.
+///
+/// A pointer (including a null one) counts as checked once its operator bool
+/// has been called. In !NDEBUG builds, the following operations assert that the
+/// pointer has been checked:
+/// - dereferencing (operator*, operator->, operator[]),
+/// - extracting the pointer via take_unique(),
+/// - destruction,
+/// - being overwritten by a move assignment.
+///
+/// Moving from a CheckedPtr disarms the source and leaves the destination
+/// unchecked, forcing the new owner to check it.
+///
+/// In NDEBUG builds, nothing is tracked and CheckedPtr behaves like
+/// cpp::unique_ptr.
+template <typename T, typename Deleter = cpp::default_delete<T>>
+class [[nodiscard]] LIBC_TRIVIAL_ABI CheckedPtr {
+  using PtrType = cpp::unique_ptr<T, Deleter>;
+  LIBC_NO_UNIQUE_ADDRESS detail::CheckedState state;
+  PtrType ptr;
+
+  template <typename U, typename E> friend class CheckedPtr;
+
+public:
+  using deleter_type = typename PtrType::deleter_type;
+  using element_type = typename PtrType::element_type;
+  using pointer = typename PtrType::pointer;
+
+  LIBC_INLINE constexpr explicit CheckedPtr(pointer p) : ptr(p) {}
+  LIBC_INLINE constexpr CheckedPtr(pointer p, const deleter_type &d)
+      : ptr(p, d) {}
+  LIBC_INLINE constexpr CheckedPtr(pointer p, deleter_type &&d)
+      : ptr(p, cpp::move(d)) {}
+
+  LIBC_INLINE constexpr CheckedPtr(CheckedPtr &&other) = default;
+  LIBC_INLINE constexpr CheckedPtr &operator=(CheckedPtr &&other) = default;
+
+  // Conversion move constructor (available iff the underlying cpp::unique_ptr
+  // conversion is)
+  template <typename U, typename E,
+            cpp::enable_if_t<
+                cpp::is_convertible_v<cpp::unique_ptr<U, E>, PtrType>, int> = 0>
+  LIBC_INLINE constexpr CheckedPtr(CheckedPtr<U, E> &&other)
+      : state(cpp::move(other.state)), ptr(cpp::move(other.ptr)) {}
+
+  // Conversion move assignment (available iff the underlying cpp::unique_ptr
+  // assignment is)
+  template <
+      typename U, typename E,
+      cpp::enable_if_t<
+          cpp::is_assignable_v<PtrType &, cpp::unique_ptr<U, E> &&>, int> = 0>
+  LIBC_INLINE constexpr CheckedPtr &operator=(CheckedPtr<U, E> &&other) {
+    state = cpp::move(other.state);
+    ptr = cpp::move(other.ptr);
+    return *this;
+  }
+
+  /// Returns true if the pointer is non-null, and marks it as checked.
+  ///
+  /// This is intentionally non-const: recording the check in a const member
+  /// function would require a mutable flag, and concurrent checks through const
+  /// references would then race on it. As a consequence, a CheckedPtr cannot be
+  /// checked through a const reference (but it can be used through one once it
+  /// has been checked).
+  LIBC_INLINE constexpr explicit operator bool() {
+    state.mark_checked();
+    return static_cast<bool>(ptr);
+  }
+
+  template <typename U = T, cpp::enable_if_t<!cpp::is_array_v<U>, int> = 0>
+  LIBC_INLINE constexpr cpp::add_lvalue_reference_t<U> operator*() const {
+    state.assert_checked();
+    return *ptr;
+  }
+
+  template <typename U = T, cpp::enable_if_t<!cpp::is_array_v<U>, int> = 0>
+  LIBC_INLINE constexpr pointer operator->() const {
+    state.assert_checked();
+    return ptr.get();
+  }
+
+  // The return type depends on U (rather than using element_type) so that it is
+  // not formed for non-array types, e.g. CheckedPtr<void, D>.
+  template <typename U = T, cpp::enable_if_t<cpp::is_array_v<U>, int> = 0>
+  LIBC_INLINE constexpr cpp::remove_extent_t<U> &operator[](size_t i) const {
+    state.assert_checked();
+    return ptr[i];
+  }
+
+  /// Moves the owned pointer into a plain cpp::unique_ptr, leaving this
+  /// CheckedPtr null. Asserts that the pointer has been checked.
+  [[nodiscard]] LIBC_INLINE constexpr PtrType take_unique() {
+    state.assert_checked();
+    return cpp::move(ptr);
+  }
+};
+
+/// Allocates and constructs an object of type T from `args`. Returns a null
+/// CheckedPtr if the allocation fails.
+template <typename T, typename... Args,
+          cpp::enable_if_t<!cpp::is_array_v<T>, int> = 0>
+LIBC_INLINE CheckedPtr<T> make_unique(Args &&...args) {
+  // There is no need to inspect `ac`: the AllocChecker operator new is
+  // noexcept, so on failure no object is constructed and the new-expression
+  // evaluates to nullptr, which the caller is then forced to check for.
+  AllocChecker ac;
+  return CheckedPtr<T>(new (ac) T(cpp::forward<Args>(args)...));
+}
+
+/// Allocates an array of `n` value-initialized elements. T must be an array of
+/// unknown bound (e.g. `make_unique<int[]>(n)`). Returns a null CheckedPtr if
+/// the allocation fails. Note some allocators return a nullptr for zero-sized
+/// allocations (n=0), which makes it impossible to identify an allocation
+/// failure in this case.
+template <
+    typename T,
+    cpp::enable_if_t<cpp::is_same_v<T, cpp::remove_extent_t<T>[]>, int> = 0>
+LIBC_INLINE CheckedPtr<T> make_unique(size_t n) {
+  using Elem = cpp::remove_extent_t<T>;
+  // See the non-array overload for why `ac` is not inspected.
+  AllocChecker ac;
+  return CheckedPtr<T>(new (ac) Elem[n]());
+}
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_CHECKED_PTR_H
diff --git a/libc/test/src/__support/CMakeLists.txt b/libc/test/src/__support/CMakeLists.txt
index 765eec6b42178..947504ea43987 100644
--- a/libc/test/src/__support/CMakeLists.txt
+++ b/libc/test/src/__support/CMakeLists.txt
@@ -122,6 +122,20 @@ add_libc_test(
     libc.src.__support.blockstore
 )
 
+add_libc_test(
+  checked_ptr_test
+  SUITE
+    libc-support-tests
+  SRCS
+    checked_ptr_test.cpp
+  DEPENDS
+    libc.src.__support.alloc_checker
+    libc.src.__support.checked_ptr
+    libc.src.__support.CPP.type_traits
+    libc.src.__support.CPP.unique_ptr
+    libc.src.__support.CPP.utility
+)
+
 add_libc_test(
   endian_internal_test
   SUITE
diff --git a/libc/test/src/__support/checked_ptr_test.cpp b/libc/test/src/__support/checked_ptr_test.cpp
new file mode 100644
index 0000000000000..80bb31adbfcd3
--- /dev/null
+++ b/libc/test/src/__support/checked_ptr_test.cpp
@@ -0,0 +1,297 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Unittests for CheckedPtr and make_unique.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/__support/CPP/type_traits.h"
+#include "src/__support/CPP/unique_ptr.h"
+#include "src/__support/CPP/utility.h"
+#include "src/__support/alloc-checker.h"
+#include "src/__support/checked_ptr.h"
+#include "test/UnitTest/Test.h"
+
+using LIBC_NAMESPACE::AllocChecker;
+using LIBC_NAMESPACE::CheckedPtr;
+using LIBC_NAMESPACE::make_unique;
+using LIBC_NAMESPACE::cpp::is_constructible_v;
+using LIBC_NAMESPACE::cpp::move;
+using LIBC_NAMESPACE::cpp::unique_ptr;
+
+// Verify compile-time properties:
+// - Non-copyable and non-default-constructible.
+// - Explicit conversion to bool is only allowed on non-const lvalues/rvalues.
+static_assert(!is_constructible_v<CheckedPtr<int>>);
+static_assert(!is_constructible_v<CheckedPtr<int>, const CheckedPtr<int> &>);
+static_assert(is_constructible_v<bool, CheckedPtr<int> &>);
+static_assert(!is_constructible_v<bool, const CheckedPtr<int> &>);
+static_assert(is_constructible_v<bool, CheckedPtr<int[]> &>);
+static_assert(!is_constructible_v<bool, const CheckedPtr<int[]> &>);
+
+struct DestructTracker {
+  int *destruct_count = nullptr;
+  DestructTracker() = default;
+  DestructTracker(int *count) : destruct_count(count) {}
+  ~DestructTracker() {
+    if (destruct_count)
+      (*destruct_count)++;
+  }
+};
+
+struct Foo {
+  int val;
+  Foo(int v) : val(v) {}
+};
+
+struct Base {
+  int val;
+  virtual ~Base() = default;
+  Base(int v) : val(v) {}
+};
+
+struct Derived : public Base {
+  Derived(int v) : Base(v) {}
+};
+
+struct CustomDeleter {
+  int *count;
+  void operator()(int *p) const {
+    (*count)++;
+    delete p;
+  }
+};
+
+struct CustomArrayDeleter {
+  int *count;
+  void operator()(int *p) const {
+    if (count)
+      (*count)++;
+    delete[] p;
+  }
+};
+
+TEST(LlvmLibcCheckedPtrTest, BasicMakeUniqueAndAccess) {
+  int destruct_count = 0;
+  {
+    auto ptr = make_unique<DestructTracker>(&destruct_count);
+    ASSERT_TRUE(static_cast<bool>(ptr));
+
+    // Once checked, access via const reference is valid and safe.
+    const auto &cref = ptr;
+    ASSERT_EQ(*cref->destruct_count, 0);
+    ASSERT_EQ(*(*cref).destruct_count, 0);
+  }
+  ASSERT_EQ(destruct_count, 1);
+}
+
+TEST(LlvmLibcCheckedPtrTest, NullptrCheck) {
+  CheckedPtr<int> ptr(nullptr);
+  ASSERT_FALSE(static_cast<bool>(ptr));
+}
+
+TEST(LlvmLibcCheckedPtrTest, MoveRequiresRecheck) {
+  int destruct_count = 0;
+  {
+    auto ptr1 = make_unique<DestructTracker>(&destruct_count);
+    ASSERT_TRUE(static_cast<bool>(ptr1));
+
+    // Even though ptr1 was checked, moving into ptr2 requires checking ptr2.
+    CheckedPtr<DestructTracker> ptr2(move(ptr1));
+    ASSERT_TRUE(static_cast<bool>(ptr2));
+    ASSERT_EQ(*ptr2->destruct_count, 0);
+  }
+  ASSERT_EQ(destruct_count, 1);
+}
+
+TEST(LlvmLibcCheckedPtrTest, MoveUncheckedThenCheck) {
+  int destruct_count = 0;
+  {
+    auto ptr1 = make_unique<DestructTracker>(&destruct_count);
+    // Move before checking: ptr1 is disarmed; ptr2 must be checked.
+    CheckedPtr<DestructTracker> ptr2(move(ptr1));
+    ASSERT_TRUE(static_cast<bool>(ptr2));
+    ASSERT_EQ(*ptr2->destruct_count, 0);
+  }
+  ASSERT_EQ(destruct_count, 1);
+}
+
+TEST(LlvmLibcCheckedPtrTest, MoveAssignment) {
+  int destruct_count1 = 0;
+  int destruct_count2 = 0;
+  {
+    auto ptr1 = make_unique<DestructTracker>(&destruct_count1);
+    auto ptr2 = make_unique<DestructTracker>(&destruct_count2);
+    ASSERT_TRUE(static_cast<bool>(ptr1));
+    ASSERT_TRUE(static_cast<bool>(ptr2));
+
+    ptr2 = move(ptr1);
+    ASSERT_EQ(destruct_count1, 0);
+    ASSERT_EQ(destruct_count2, 1);
+    ASSERT_TRUE(static_cast<bool>(ptr2));
+  }
+  ASSERT_EQ(destruct_count1, 1);
+}
+
+TEST(LlvmLibcCheckedPtrTest, Conversions) {
+  auto derived = make_unique<Derived>(100);
+  CheckedPtr<Base> base(move(derived));
+  ASSERT_TRUE(static_cast<bool>(base));
+  ASSERT_EQ(base->val, 100);
+
+  auto mut_int = make_unique<int>(42);
+  CheckedPtr<const int> const_int(move(mut_int));
+  ASSERT_TRUE(static_cast<bool>(const_int));
+  ASSERT_EQ(*const_int, 42);
+}
+
+TEST(LlvmLibcCheckedPtrTest, Array) {
+  int destruct_count = 0;
+  {
+    auto ptr = make_unique<DestructTracker[]>(3);
+    ASSERT_TRUE(static_cast<bool>(ptr));
+    ptr[0].destruct_count = &destruct_count;
+    ptr[1].destruct_count = &destruct_count;
+    ptr[2].destruct_count = &destruct_count;
+    ASSERT_EQ(destruct_count, 0);
+  }
+  ASSERT_EQ(destruct_count, 3);
+}
+
+TEST(LlvmLibcCheckedPtrTest, CustomDeleters) {
+  int obj_deletes = 0;
+  {
+    AllocChecker ac;
+    CheckedPtr<int, CustomDeleter> ptr(new (ac) int(7),
+                                       CustomDeleter{&obj_deletes});
+    ASSERT_TRUE(static_cast<bool>(ptr));
+    ASSERT_EQ(*ptr, 7);
+    ASSERT_EQ(obj_deletes, 0);
+  }
+  ASSERT_EQ(obj_deletes, 1);
+
+  int arr_deletes = 0;
+  {
+    AllocChecker ac;
+    CheckedPtr<int[], CustomArrayDeleter> arr(new (ac) int[2],
+                                              CustomArrayDeleter{&arr_deletes});
+    ASSERT_TRUE(static_cast<bool>(arr));
+    arr[0] = 10;
+    arr[1] = 20;
+    ASSERT_EQ(arr[0] + arr[1], 30);
+    ASSERT_EQ(arr_deletes, 0);
+  }
+  ASSERT_EQ(arr_deletes, 1);
+}
+
+TEST(LlvmLibcCheckedPtrTest, SelfSwap) {
+  auto ptr = make_unique<int>(1);
+  ASSERT_TRUE(static_cast<bool>(ptr));
+  // Self-move-assignment (which happens as a part of self-swap) must be a
+  // no-op.
+  LIBC_NAMESPACE::cpp::swap(ptr, ptr);
+  ASSERT_TRUE(static_cast<bool>(ptr));
+  ASSERT_EQ(*ptr, 1);
+}
+
+struct VoidDeleter {
+  void operator()(void *) const {}
+};
+
+TEST(LlvmLibcCheckedPtrTest, Void) {
+  CheckedPtr<void, VoidDeleter> ptr(nullptr);
+  ASSERT_FALSE(static_cast<bool>(ptr));
+}
+
+TEST(LlvmLibcCheckedPtrTest, TakeUnique) {
+  int destruct_count = 0;
+  unique_ptr<DestructTracker> owner;
+  {
+    auto ptr = make_unique<DestructTracker>(&destruct_count);
+    ASSERT_TRUE(static_cast<bool>(ptr));
+    owner = ptr.take_unique();
+    // `ptr` is now null and still checked, so destroying it must neither delete
+    // the object nor assert.
+  }
+  ASSERT_EQ(destruct_count, 0);
+  ASSERT_TRUE(static_cast<bool>(owner));
+  owner.reset();
+  ASSERT_EQ(destruct_count, 1);
+}
+
+#if !defined(NDEBUG) && LIBC_TEST_SUBPROCESS_TESTS
+
+#if defined(LIBC_FULL_BUILD) && !defined(LIBC_COPT_USE_C_ASSERT)
+#define EXPECT_ASSERT_FAILS(stmt) EXPECT_EXITS(stmt, 255)
+#else
+#define EXPECT_ASSERT_FAILS(stmt) EXPECT_DEATH(stmt, WITH_SIGNAL(-1))
+#endif
+
+TEST(LlvmLibcCheckedPtrDeathTest, UncheckedDestructionDies) {
+  EXPECT_ASSERT_FAILS([] {
+    auto ptr = make_unique<int>(42);
+    (void)ptr;
+  });
+}
+
+TEST(LlvmLibcCheckedPtrDeathTest, MoveFromCheckedRequiresRecheckOrDies) {
+  EXPECT_ASSERT_FAILS([] {
+    auto ptr1 = make_unique<int>(42);
+    (void)static_cast<bool>(ptr1);
+    CheckedPtr<int> ptr2(move(ptr1));
+    (void)*ptr2;
+  });
+}
+
+// In the tests below, the pointer is checked after the operation under test, so
+// that its destruction cannot be the thing that triggers the assertion.
+
+TEST(LlvmLibcCheckedPtrDeathTest, UncheckedDereferenceDies) {
+  EXPECT_ASSERT_FAILS([] {
+    auto ptr = make_unique<int>(42);
+    (void)*ptr;
+    (void)static_cast<bool>(ptr);
+  });
+}
+
+TEST(LlvmLibcCheckedPtrDeathTest, UncheckedArrowDies) {
+  EXPECT_ASSERT_FAILS([] {
+    auto ptr = make_unique<Foo>(42);
+    (void)ptr->val;
+    (void)static_cast<bool>(ptr);
+  });
+}
+
+TEST(LlvmLibcCheckedPtrDeathTest, UncheckedArraySubscriptDies) {
+  EXPECT_ASSERT_FAILS([] {
+    auto ptr = make_unique<int[]>(3);
+    (void)ptr[0];
+    (void)static_cast<bool>(ptr);
+  });
+}
+
+TEST(LlvmLibcCheckedPtrDeathTest, OverwritingUncheckedViaMoveAssignmentDies) {
+  EXPECT_ASSERT_FAILS([] {
+    auto ptr1 = make_unique<int>(1);
+    auto ptr2 = make_unique<int>(2);
+    (void)static_cast<bool>(ptr2);
+    ptr1 = move(ptr2);
+    (void)static_cast<bool>(ptr1);
+  });
+}
+
+TEST(LlvmLibcCheckedPtrDeathTest, UncheckedTakeUniqueDies) {
+  EXPECT_ASSERT_FAILS([] {
+    auto ptr = make_unique<int>(42);
+    (void)ptr.take_unique();
+    (void)static_cast<bool>(ptr);
+  });
+}
+
+#endif // !defined(NDEBUG) && LIBC_TEST_SUBPROCESS_TESTS

``````````

</details>


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


More information about the libc-commits mailing list