Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 95 additions & 19 deletions modules/strong_ptr.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ struct is_array_like<std::array<T, N>> : std::true_type

// Helper variable template
template<typename T>
inline constexpr bool is_array_like_v = is_array_like<T>::value;
constexpr bool is_array_like_v = is_array_like<T>::value;

// Concept for array-like types
template<typename T>
Expand Down Expand Up @@ -307,6 +307,16 @@ struct nullptr_access : public exception
}
};

/**
* @brief API tag used to create a strong_ptr which points to static memory
*
* As the name implies this is unsafe and is up to the developer to ensure that
* the object passed to strong_ptr actually has static storage duration.
*
*/
struct unsafe_assume_static_tag
{};

/**
* @brief A non-nullable strong reference counted pointer
*
Expand All @@ -325,16 +335,16 @@ struct nullptr_access : public exception
*
* ```C++
* // Create a strong_ptr to an object
* auto ptr = mem::make_strong_ptr<my_i2c_driver>(allocator, arg1, arg2);
* auto i2c = mem::make_strong_ptr<my_i2c_driver>(allocator, arg1, arg2);
*
* // Use the object using dereference (*) operator
* (*ptr).configure({ .clock_rate = 250_kHz });
* (*i2c).configure({ .clock_rate = 250_kHz });
*
* // OR use the object using arrow (->) operator
* ptr->configure({ .clock_rate = 250_kHz });
* i2c->configure({ .clock_rate = 250_kHz });
*
* // Share ownership with another driver or object
* auto my_imu = mem::make_strong_ptr<my_driver>(allocator, ptr, 0x13);
* auto sensor = mem::make_strong_ptr<my_sensor>(allocator, i2c, 0x13);
* ```
*
* @tparam T The type of the managed object
Expand All @@ -351,6 +361,33 @@ public:
/// Delete nullptr constructor - strong_ptr must always be valid
strong_ptr(std::nullptr_t) = delete;

/**
* @brief Create a strong_ptr that points to points to an object with static
* storage duration.
*
* This API MUST only be used with objects with static storage duration.
* Without that precondition met, it is UB to create such a strong_ptr.
*
* There is no way in C++23 and below to determine if an lvalue passed has
* static storage duration. With C++26 and `std::has_static_storage_duration`
* we can determine this at compile time and provide a compile time error if
* the passed lvalue is not an object with static storage duration. This
* constructor will be deprecated once the library migrates to C++26.
*
* Since the original object was statically allocated, there is no need for a
* ref counted control block and thus no allocation occurs. `use_count()` will
* return 0 meaning that the object is statically allocated.
*
* @param p_object - a statically allocated object to
* @return strong_ptr<T> - A strong_ptr pointing to lvalue which should have
* static storage duration.
*/
strong_ptr(unsafe_assume_static_tag, T& p_object)
: m_ctrl(nullptr)
, m_ptr(&p_object)
{
}

/**
* @brief Copy constructor
*
Expand All @@ -362,7 +399,7 @@ public:
: m_ctrl(p_other.m_ctrl)
, m_ptr(p_other.m_ptr)
{
m_ctrl->add_ref();
add_ref();
}

/**
Expand All @@ -380,7 +417,7 @@ public:
: m_ctrl(p_other.m_ctrl)
, m_ptr(p_other.m_ptr)
{
m_ctrl->add_ref();
add_ref();
}

/**
Expand All @@ -403,7 +440,7 @@ public:
: m_ctrl(p_other.m_ctrl)
, m_ptr(p_other.m_ptr)
{
m_ctrl->add_ref();
add_ref();
}

/**
Expand All @@ -429,7 +466,7 @@ public:
release();
m_ctrl = p_other.m_ctrl;
m_ptr = p_other.m_ptr;
m_ctrl->add_ref();
add_ref();
}
return *this;
}
Expand Down Expand Up @@ -499,7 +536,7 @@ public:
: m_ctrl(p_other.m_ctrl)
, m_ptr(&((*p_other).*p_member_ptr))
{
m_ctrl->add_ref();
add_ref();
}

/**
Expand Down Expand Up @@ -545,7 +582,7 @@ public:
throw_if_out_of_bounds(N, p_index);
m_ctrl = p_other.m_ctrl;
m_ptr = &((*p_other).*p_array_ptr)[p_index];
m_ctrl->add_ref();
add_ref();
}

// NOLINTBEGIN(modernize-avoid-c-arrays)
Expand Down Expand Up @@ -590,7 +627,7 @@ public:
throw_if_out_of_bounds(N, p_index);
m_ctrl = p_other.m_ctrl;
m_ptr = &((*p_other).*p_array_ptr)[p_index];
m_ctrl->add_ref();
add_ref();
}
// NOLINTEND(modernize-avoid-c-arrays)

Expand Down Expand Up @@ -619,7 +656,7 @@ public:
release();
m_ctrl = p_other.m_ctrl;
m_ptr = p_other.m_ptr;
m_ctrl->add_ref();
add_ref();
}
return *this;
}
Expand All @@ -641,7 +678,7 @@ public:
release();
m_ctrl = p_other.m_ctrl;
m_ptr = p_other.m_ptr;
m_ctrl->add_ref();
add_ref();
return *this;
}

Expand Down Expand Up @@ -698,6 +735,18 @@ public:
return m_ctrl ? m_ctrl->strong_count.load(std::memory_order_relaxed) : 0;
}

/**
* @brief Returns if the object this is pointing to is statically allocated or
* not.
*
* @return true - object is assumed to have static storage duration.
* @return false - object has dynamic storage duration.
*/
constexpr bool is_dynamic()
{
return m_ctrl != nullptr;
}

private:
template<class U>
friend class enable_strong_from_this;
Expand All @@ -706,6 +755,9 @@ private:
friend strong_ptr<U> make_strong_ptr(std::pmr::polymorphic_allocator<>,
Args&&...);

template<class U, typename... Args, auto>
friend strong_ptr<T> make_static_strong_ptr(Args&&... p_args);

template<typename U>
friend class strong_ptr;

Expand All @@ -715,20 +767,27 @@ private:
template<typename U>
friend class optional_ptr;

inline void throw_if_out_of_bounds(std::size_t p_size, std::size_t p_index)
void throw_if_out_of_bounds(std::size_t p_size, std::size_t p_index)
{
if (p_index >= p_size) {
throw mem::out_of_range({ .m_index = p_index, .m_capacity = p_size });
}
}

void add_ref()
{
if (is_dynamic()) {
m_ctrl->add_ref();
}
}

// Internal constructor with control block and pointer - used by make() and
// aliasing
strong_ptr(ref_info* p_ctrl, T* p_ptr) noexcept
: m_ctrl(p_ctrl)
, m_ptr(p_ptr)
{
m_ctrl->add_ref();
add_ref();
}

struct bypass_ref_count
Expand All @@ -744,7 +803,7 @@ private:

void release()
{
if (m_ctrl) {
if (is_dynamic()) {
m_ctrl->release();
}
}
Expand Down Expand Up @@ -1480,6 +1539,23 @@ public:
return m_value;
}

/**
* @brief Get the current reference count
*
* This is primarily for testing purposes. To determine if an optional_ptr
* points to a statically allocated object, this API must return 0 and
* `is_engaged()` must return true.
*
* @return The number of strong references to the managed object. Returns 0 if
* this pointer is nullptr.
*/
[[nodiscard]] auto use_count() const noexcept
{
return is_engaged()
? m_value.m_ctrl->strong_count.load(std::memory_order_relaxed)
: 0;
}

/**
* @brief Swap the contents of this optional_ptr with another
*
Expand Down Expand Up @@ -1537,7 +1613,7 @@ private:
* @return An optional_ptr that is either empty or contains a strong_ptr
*/
template<typename T>
[[nodiscard]] inline optional_ptr<T> weak_ptr<T>::lock() const noexcept
[[nodiscard]] optional_ptr<T> weak_ptr<T>::lock() const noexcept
{
if (expired()) {
return nullptr;
Expand Down Expand Up @@ -1823,7 +1899,7 @@ private:
* @throws std::bad_alloc if memory allocation fails
*/
template<class T, typename... Args>
[[nodiscard]] inline strong_ptr<T> make_strong_ptr(
[[nodiscard]] strong_ptr<T> make_strong_ptr(
std::pmr::polymorphic_allocator<> p_alloc,
Args&&... p_args)
{
Expand Down
2 changes: 1 addition & 1 deletion test_package/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ int main()
std::println("\n{{");
mem::optional_ptr<int> ptr2 = ptr;
std::println(" optional_ptr<int> created!");
std::println(" [ptr2](use_count = {}) = {}", ptr.use_count(), *ptr);
std::println(" [ptr2](use_count = {}) = {}", ptr2.use_count(), *ptr2);
std::println(" [ptr](use_count = {}) = {}", ptr.use_count(), *ptr);
std::println(" optional_ptr<int> destroyed!");
std::println("}}\n");
Expand Down
88 changes: 84 additions & 4 deletions tests/strong_ptr.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ boost::ut::suite<"strong_ptr_test"> strong_ptr_test = []() {
auto ptr = mem::make_strong_ptr<test_class>(test_allocator, 42);

expect(that % 42 == ptr->value());
expect(that % 1 == test_class::s_instance_count)
expect(that % 1 == test_class::instance_count)
<< "Should have created one instance";
expect(that % 1 == ptr.use_count())
<< "Should have exactly one reference\n";
Expand Down Expand Up @@ -197,16 +197,16 @@ boost::ut::suite<"strong_ptr_test"> strong_ptr_test = []() {
};

"destruction"_test = [&] {
expect(that % 0 == test_class::s_instance_count)
expect(that % 0 == test_class::instance_count)
<< "Should start with no instances\n";

{
auto ptr = make_strong_ptr<test_class>(test_allocator, 42);
expect(that % 1 == test_class::s_instance_count)
expect(that % 1 == test_class::instance_count)
<< "Should have one instance\n";
}

expect(that % 0 == test_class::s_instance_count)
expect(that % 0 == test_class::instance_count)
<< "Instance should be destroyed\n";
};
};
Expand Down Expand Up @@ -246,5 +246,85 @@ boost::ut::suite<"strong_ptr_only_test"> strong_ptr_only_test = []() {
expect(that % 2 == obj.use_count()) << "Should share ownership";
expect(that % 42 == copy_ptr->value());
};

"statically_allocate_strong_ptr"_test = [&] {
// Create a static object
static int static_obj = 42;

// Create strong_ptr to static object using unsafe_assume_static_tag
strong_ptr<int> ptr(mem::unsafe_assume_static_tag{}, static_obj);

// Verify the pointer works correctly
expect(that % 42 == *ptr);

// use_count should be 0 for statically allocated objects
expect(that % 0 == ptr.use_count())
<< "Static strong_ptr should have use_count of 0";

// Modify through the pointer
*ptr = 100;
expect(that % 100 == *ptr);
expect(that % 100 == static_obj)
<< "Static int should be modified through strong_ptr";

// Test copying a static strong_ptr
auto ptr_copy = ptr;
expect(that % 0 == ptr_copy.use_count())
<< "Copy of static strong_ptr should also have use_count of 0";
expect(that % 100 == *ptr_copy);

// Test copying a static strong_ptr
mem::optional_ptr<int> opt_ptr = ptr;
*opt_ptr = 17;
expect(that % 0 == ptr_copy.use_count())
<< "Copy of static optional_ptr should also have use_count of 0";
expect(that % 17 == *ptr_copy);
expect(that % 17 == static_obj)
<< "Static int should be modified through optional_ptr";
};

"static_strong_ptr_no_destructor_call"_test = [&] {
// Use a separate class to avoid affecting test_class::s_instance_count
static bool destructor_called = false;
struct static_tracked_class
{
int value;
explicit static_tracked_class(int v)
: value(v)
{
}
~static_tracked_class()
{
destructor_called = true;
}
};

static static_tracked_class static_obj(999);
destructor_called = false; // Reset flag

{
// Create strong_ptr to static object using unsafe_assume_static_tag
strong_ptr<static_tracked_class> ptr(mem::unsafe_assume_static_tag{},
static_obj);

expect(that % 999 == ptr->value);
expect(that % 0 == ptr.use_count())
<< "Static strong_ptr should have use_count of 0";

// Copy the pointer to ensure copies also don't call destructor
auto ptr_copy = ptr;
expect(that % 0 == ptr_copy.use_count());
}

// After scope exit, destructor should NOT have been called
expect(not destructor_called)
<< "Static object's destructor should not be called when strong_ptr "
"goes out of scope";

// Verify object is still valid
expect(that % 999 == static_obj.value)
<< "Static object should still be accessible after strong_ptr "
"destruction";
};
};
} // namespace mem
Loading