Skip to content

feat: EBO‑optimized UniquePtr<T, Deleter> #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from

Conversation

brodheadw
Copy link

Closes #1: implement std‑like UniquePtr with EBO, converting moves, nullptr‑assign, etc.

brodheadw added 12 commits May 8, 2025 15:45
…n requirement to Development Setup section of CONTRIBUTING.md
…d version requirement to Development Setup section of CONTRIBUTING.md"

This reverts commit 2c4c34f.
…n requirement to Development Setup section of CONTRIBUTING.md
- Create radiant/UniquePtr.h with the class template declaration,
  default and pointer‐taking constructors, destructor and move‐ops.
- Add placeholder comments for the missing member functions.
Change UniquePtr to inherit from an empty DefaultDelete deleter so sizeof(UniquePtr<T>) == sizeof(T*)

Fix ctor to initialize base deleter, not mistakenly pass pointer

Remove stored function-pointer field, rely on empty-base optimization

Update UniquePtrDefault alias to use the new DefaultDelete

Add test/test_UniquePtr.cpp covering basic lifetime, release/reset, move, swap and custom deleter size guarantee

test: revise tests for EBO-optimized DefaultDelete (drop old alias, move D and its count to namespace scope, simplify static_assert, update CustomDeleter test)
…sive tests

Introduce DefaultDelete<T> as an empty deleter type to enable empty-base-optimization

Refactor UniquePtr<T, Deleter> to inherit from Deleter, removing extra storage and ensuring sizeof(UniquePtr) == sizeof(T*)

Add move-ctor, move-assign, release, reset, swap, and nullptr conversion APIs

Provide ADL-friendly swap overload

Update alias UniquePtrDefault<T> to use default deleter

Add test/test_UniquePtr.cpp with GoogleTest suite covering basic lifetime, sizeof check, release/reset, move semantics, swap semantics, and custom deleter
…d of std traits

add converting constructor to DefaultDelete to allow DefaultDelete<Base> from DefaultDelete<Derived>

remove <type_traits> dependency and replace std::is_convertible_v with rad::IsConv

keep existing move and converting-move overloads unchanged; tests continue to pass
Copy link

github-actions bot commented May 14, 2025

CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅

@brodheadw
Copy link
Author

I have read the CLA Document and I hereby sign the CLA

@brodheadw brodheadw closed this May 14, 2025
Copy link
Contributor

@ben-craig-cs ben-craig-cs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for showing interest in this project! We're definitely in need of a UniquePtr class.

Have you considered also adding UniquePtr<T[], D>? https://eel.is/c++draft/unique.ptr.runtime


/// @brief Default deleter using Radiant's StdAllocator (EBO if empty).
template <typename T>
struct DefaultDelete : private StdAllocator {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to couple DefaultDelete to an allocator? Do we need to make DefaultDelete a template?

I will suggest that we instead have a non-templated DefaultDelete, where the op() is similar to the following:

    template <typename T>
    constexpr void operator()(const T *t) const
    {
        // require complete types
        static_assert(sizeof(T)!=0);
        delete t;
    }

https://godbolt.org/z/fPv9b8oPh

I don't think we need any of the allocator related methods, at least not for the default deleter.

/// @brief A unique ownership smart pointer with customizable deleter.
/// If the deleter is empty (via EBO), sizeof(UniquePtr)==sizeof(T*).
template <typename T, typename Deleter = DefaultDelete<T>>
class UniquePtr : private Deleter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal here is correct: have a UniquePtr with sizeof(T*). We shouldn't get there by having UniquePtr inherit from Deleter though. If Deleter happened to have virtual methods, then that would make UniquePtr polymorphic as well.

Instead, put the Deleter in an EmptyOptimizedPair. That still does EBO, but it's on an internal type that won't leak subtle Deleter details into the UniquePtr.

If you're feeling really adventurous, you can also figure out the compatibility and support matrix, and then use [[no_unique_address]] and [[msvc::no_unique_address]] instead of EmptyOptimizedPair instead. I don't think we're ready for that yet though.

public:
RAD_NOT_COPYABLE(UniquePtr);

using pointer = T*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look at Vector.h for how we do these kinds of typedefs.
s/pointer/PointerType
s/deleter_type/DeleterType

constexpr UniquePtr() noexcept = default;

/// @brief Constructs owning p, with optional custom deleter d.
explicit constexpr UniquePtr(pointer p, Deleter d = Deleter()) noexcept
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to suggest mirroring the standard's unique_ptr overload set as best as possible, and then justify where you diverge.
https://eel.is/c++draft/unique.ptr

For this constructor, I think you should avoid default arguments. I think the default argument here forces the deleter to be movable, where if this were a single pointer ctor, then very careful UniquePtr usage could let a deleter get away with being immovable.

I'll also recommend that you add a type_identity_t analog, and use that in the UniquePtr constructors. That will suppress constructor template argument deduction (CTAD). We don't want CTAD here, because pointers to single elements and pointers to arrays look the same, and we don't want to make it unnecessarily easy to bind a scalar UniquePtr to an array allocation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I've now split the pointer + deleter ctor into two overloads so the single-arg ctor only ever default-constructs the deleter so an immovable custom deleter will still work. Two-arg ctor now only participates when you explicitly supply a deleter.

Also wrapped the pointer parameter in TypeIdentityT alias to suppress CTAD and avoid accidentally binding a new T[...] array to a scalar.

Also I am now making PRs to main on my own fork.

}

/// @brief Implicit bool conversion: true if non-null.
constexpr operator bool() const noexcept { return m_ptr != nullptr; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this explicit

constexpr operator bool() const noexcept { return m_ptr != nullptr; }

/// @brief Observers
constexpr pointer get() const noexcept { return m_ptr; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to Get(). Using STL naming conventions is ok when it enables substantial generic interoperability (notably, things like begin and end). I don't think get crosses that threshold, as the operator interface is the generic interface.

Similar comments throughout the file, like with release, reset, and swap


/// @brief ADL swap overload.
template <typename T, typename D>
void swap(UniquePtr<T,D>& a, UniquePtr<T,D>& b) noexcept {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Radiant's Swap is in Algorithm.h


/// @brief Allocator-aware make_unique: allocates via AllocTraits, constructs, and returns UniquePtr.
template <typename T, typename... Args>
UniquePtr<T> make_unique(Args&&... args) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written, this doesn't do much with regards to allocators, as it forces StdAllocator. I think there's potential here though.

I'll suggest adding an AllocatorDeleter<Alloc>, and then changing make_unique to something like the following:

template <typename T, typename Alloc, typename... Args>
UniquePtr<AllocatorDeleter<Alloc>> MakeUnique(Alloc alloc, Args&&... args) { /*...*/ }


A alloc;
T* raw = Traits::template Alloc<T>(alloc, 1);
Traits::Construct(alloc, raw, rad::Forward<Args>(args)...);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to check raw for null before constructing. Return an empty UniquePtr if that happens.

@brodheadw brodheadw deleted the feat/unique_ptr branch May 14, 2025 14:08
@brodheadw brodheadw restored the feat/unique_ptr branch May 14, 2025 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

UniquePtr implementation
2 participants