| // Copyright 2025 The Pigweed Authors | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not | |
| // use this file except in compliance with the License. You may obtain a copy of | |
| // the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
| // License for the specific language governing permissions and limitations under | |
| // the License. | |
| #include "pw_allocator/weak_ptr.h" | |
| // TODO(b/402489948): Remove when portable atomics are provided by `pw_atomic`. | |
| #if PW_ALLOCATOR_HAS_ATOMICS | |
| #include <cstddef> | |
| #include "pw_allocator/allocator.h" | |
| #include "pw_allocator/internal/counter.h" | |
| #include "pw_allocator/shared_ptr.h" | |
| #include "pw_allocator/testing.h" | |
| #include "pw_unit_test/framework.h" | |
| namespace { | |
| using pw::allocator::test::Counter; | |
| class WeakPtrTest : public pw::allocator::test::TestWithCounters { | |
| protected: | |
| pw::allocator::test::AllocatorForTest<256> allocator_; | |
| }; | |
| TEST_F(WeakPtrTest, DefaultInitializationIsExpired) { | |
| pw::WeakPtr<int> weak; | |
| EXPECT_EQ(weak.use_count(), 0); | |
| EXPECT_TRUE(weak.expired()); | |
| EXPECT_EQ(weak.Lock(), nullptr); | |
| } | |
| TEST_F(WeakPtrTest, CanConstructMultipleFromSingleSharedPtr) { | |
| auto shared = allocator_.MakeShared<int>(42); | |
| pw::WeakPtr<int> weak1(shared); | |
| EXPECT_EQ(weak1.use_count(), 1); | |
| EXPECT_FALSE(weak1.expired()); | |
| pw::WeakPtr<int> weak2(shared); | |
| EXPECT_EQ(weak1.use_count(), 1); | |
| EXPECT_EQ(weak2.use_count(), 1); | |
| EXPECT_FALSE(weak1.expired()); | |
| EXPECT_FALSE(weak2.expired()); | |
| } | |
| TEST_F(WeakPtrTest, CanLockWhenActive) { | |
| auto shared1 = allocator_.MakeShared<int>(42); | |
| pw::WeakPtr<int> weak(shared1); | |
| EXPECT_EQ(weak.use_count(), 1); | |
| EXPECT_FALSE(weak.expired()); | |
| auto shared2 = weak.Lock(); | |
| ASSERT_NE(shared2.get(), nullptr); | |
| EXPECT_EQ(*shared2, 42); | |
| } | |
| TEST_F(WeakPtrTest, CannotLockWhenExpired) { | |
| auto shared = allocator_.MakeShared<int>(42); | |
| pw::WeakPtr<int> weak(shared); | |
| shared.reset(); | |
| EXPECT_EQ(weak.use_count(), 0); | |
| EXPECT_TRUE(weak.expired()); | |
| auto shared2 = weak.Lock(); | |
| EXPECT_EQ(shared2.get(), nullptr); | |
| } | |
| TEST_F(WeakPtrTest, CanCopyConstructWhenActive) { | |
| auto shared1 = allocator_.MakeShared<int>(42); | |
| pw::WeakPtr<int> weak1(shared1); | |
| pw::WeakPtr<int> weak2(weak1); | |
| EXPECT_EQ(weak1.use_count(), 1); | |
| EXPECT_EQ(weak2.use_count(), 1); | |
| EXPECT_FALSE(weak1.expired()); | |
| EXPECT_FALSE(weak2.expired()); | |
| auto shared2 = weak2.Lock(); | |
| ASSERT_NE(shared2.get(), nullptr); | |
| EXPECT_EQ(*shared2, 42); | |
| } | |
| TEST_F(WeakPtrTest, CanCopyConstructWhenExpired) { | |
| auto shared1 = allocator_.MakeShared<int>(42); | |
| pw::WeakPtr<int> weak1(shared1); | |
| // resetting the shared pointer should delete the object, but not the control | |
| // block. | |
| size_t allocated = allocator_.allocate_size(); | |
| shared1.reset(); | |
| size_t deallocated = allocator_.deallocate_size(); | |
| EXPECT_GT(allocated, deallocated); | |
| pw::WeakPtr<int> weak2(weak1); | |
| EXPECT_EQ(weak1.use_count(), 0); | |
| EXPECT_EQ(weak2.use_count(), 0); | |
| EXPECT_TRUE(weak1.expired()); | |
| EXPECT_TRUE(weak2.expired()); | |
| // Allocator should be untouched by copy-construction. | |
| EXPECT_EQ(allocator_.allocate_size(), allocated); | |
| EXPECT_EQ(allocator_.deallocate_size(), deallocated); | |
| } | |
| TEST_F(WeakPtrTest, CanCopyAssignWhenActive) { | |
| auto shared1 = allocator_.MakeShared<int>(42); | |
| pw::WeakPtr<int> weak; | |
| { | |
| pw::WeakPtr<int> tmp(shared1); | |
| weak = tmp; | |
| } | |
| EXPECT_EQ(weak.use_count(), 1); | |
| EXPECT_FALSE(weak.expired()); | |
| auto shared2 = weak.Lock(); | |
| ASSERT_NE(shared2.get(), nullptr); | |
| EXPECT_EQ(*shared2, 42); | |
| } | |
| TEST_F(WeakPtrTest, CanCopyAssignWhenExpired) { | |
| pw::WeakPtr<int> weak; | |
| // The shared pointer should delete the object when it goes out of scope, but | |
| // not the control block. | |
| size_t allocated = 0; | |
| { | |
| auto shared = allocator_.MakeShared<int>(42); | |
| allocated = allocator_.allocate_size(); | |
| pw::WeakPtr<int> tmp(shared); | |
| weak = tmp; | |
| } | |
| size_t deallocated = allocator_.deallocate_size(); | |
| EXPECT_GT(allocated, deallocated); | |
| EXPECT_EQ(weak.use_count(), 0); | |
| EXPECT_TRUE(weak.expired()); | |
| // Allocator should be untouched by copy-assignment. | |
| EXPECT_EQ(allocator_.allocate_size(), allocated); | |
| EXPECT_EQ(allocator_.deallocate_size(), deallocated); | |
| } | |
| TEST_F(WeakPtrTest, CanMoveConstructWhenActive) { | |
| auto shared1 = allocator_.MakeShared<int>(42); | |
| pw::WeakPtr<int> weak1(shared1); | |
| pw::WeakPtr<int> weak2(std::move(weak1)); | |
| EXPECT_EQ(weak2.use_count(), 1); | |
| EXPECT_FALSE(weak2.expired()); | |
| auto shared2 = weak2.Lock(); | |
| ASSERT_NE(shared2.get(), nullptr); | |
| EXPECT_EQ(*shared2, 42); | |
| } | |
| TEST_F(WeakPtrTest, CanMoveConstructWhenExpired) { | |
| auto shared1 = allocator_.MakeShared<int>(42); | |
| pw::WeakPtr<int> weak1(shared1); | |
| // Restting the shared pointer should delete the object, but not the control | |
| // block. | |
| size_t allocated = allocator_.allocate_size(); | |
| shared1.reset(); | |
| size_t deallocated = allocator_.deallocate_size(); | |
| EXPECT_GT(allocated, deallocated); | |
| pw::WeakPtr<int> weak2(std::move(weak1)); | |
| EXPECT_EQ(weak2.use_count(), 0); | |
| EXPECT_TRUE(weak2.expired()); | |
| // Allocator should be untouched by move-construction. | |
| EXPECT_EQ(allocator_.allocate_size(), allocated); | |
| EXPECT_EQ(allocator_.deallocate_size(), deallocated); | |
| } | |
| TEST_F(WeakPtrTest, CanMoveAssignWhenActive) { | |
| auto shared1 = allocator_.MakeShared<int>(42); | |
| pw::WeakPtr<int> weak; | |
| { | |
| pw::WeakPtr<int> tmp(shared1); | |
| weak = std::move(tmp); | |
| } | |
| EXPECT_EQ(weak.use_count(), 1); | |
| EXPECT_FALSE(weak.expired()); | |
| auto shared2 = weak.Lock(); | |
| ASSERT_NE(shared2.get(), nullptr); | |
| EXPECT_EQ(*shared2, 42); | |
| } | |
| TEST_F(WeakPtrTest, CanMoveAssignWhenExpired) { | |
| pw::WeakPtr<int> weak; | |
| // The shared pointer should delete the object when it goes out of scope, but | |
| // not the control block. | |
| size_t allocated = 0; | |
| { | |
| auto shared = allocator_.MakeShared<int>(42); | |
| allocated = allocator_.allocate_size(); | |
| pw::WeakPtr<int> tmp(shared); | |
| weak = std::move(tmp); | |
| } | |
| size_t deallocated = allocator_.deallocate_size(); | |
| EXPECT_GT(allocated, deallocated); | |
| EXPECT_EQ(weak.use_count(), 0); | |
| EXPECT_TRUE(weak.expired()); | |
| // Allocator should be untouched by move-assignment. | |
| EXPECT_EQ(allocator_.allocate_size(), allocated); | |
| EXPECT_EQ(allocator_.deallocate_size(), deallocated); | |
| } | |
| TEST_F(WeakPtrTest, DestructorFreesControlBlockExactlyOnce) { | |
| pw::WeakPtr<int> weak1; | |
| // The shared pointer should delete the object when it goes out of scope, but | |
| // not the control block. | |
| size_t allocated = 0; | |
| { | |
| auto shared = allocator_.MakeShared<int>(42); | |
| allocated = allocator_.allocate_size(); | |
| pw::WeakPtr<int> tmp(shared); | |
| weak1 = std::move(tmp); | |
| } | |
| size_t deallocated = allocator_.deallocate_size(); | |
| EXPECT_GT(allocated, deallocated); | |
| { | |
| pw::WeakPtr<int> weak2 = weak1; | |
| weak1.reset(); | |
| // Allocator should be untouched; there is still one weak pointer remaining. | |
| EXPECT_EQ(allocator_.deallocate_size(), deallocated); | |
| } | |
| // Last weak pointer has fallen out of scope and the control block is free. | |
| EXPECT_NE(allocator_.deallocate_size(), deallocated); | |
| } | |
| TEST_F(WeakPtrTest, OwnerBeforeProvidesPartialOrder) { | |
| // Intentionally mix weak and shared types. | |
| pw::WeakPtr<int> weak1 = allocator_.MakeShared<int>(111); | |
| auto shared2 = allocator_.MakeShared<int>(222); | |
| pw::WeakPtr<int> weak2 = shared2; | |
| pw::WeakPtr<int> shared3 = weak2.Lock(); | |
| pw::WeakPtr<int> weak4 = allocator_.MakeShared<int>(444); | |
| // Remain agnostic to allocation order. | |
| bool ascending = weak1.owner_before(weak2); | |
| // Reflexive. | |
| EXPECT_FALSE(weak1.owner_before(weak1)); | |
| EXPECT_FALSE(weak2.owner_before(shared3)); | |
| EXPECT_FALSE(shared3.owner_before(weak2)); | |
| // Symmetric. | |
| EXPECT_NE(weak2.owner_before(weak1), ascending); | |
| EXPECT_NE(shared3.owner_before(weak1), ascending); | |
| // Transitive. | |
| EXPECT_EQ(weak1.owner_before(shared3), ascending); | |
| EXPECT_EQ(shared3.owner_before(weak4), ascending); | |
| EXPECT_EQ(weak1.owner_before(weak4), ascending); | |
| } | |
| TEST_F(WeakPtrTest, CanswapWhenNeitherAreExpired) { | |
| auto shared1 = allocator_.MakeShared<Counter>(111u); | |
| auto shared2 = allocator_.MakeShared<Counter>(222u); | |
| pw::WeakPtr<Counter> weak1(shared1); | |
| pw::WeakPtr<Counter> weak2(shared2); | |
| weak1.swap(weak2); | |
| EXPECT_EQ(weak1.Lock()->value(), 222u); | |
| EXPECT_EQ(weak2.Lock()->value(), 111u); | |
| } | |
| TEST_F(WeakPtrTest, CanswapWhenOneIsExpired) { | |
| auto shared1 = allocator_.MakeShared<Counter>(111u); | |
| auto shared2 = allocator_.MakeShared<Counter>(222u); | |
| pw::WeakPtr<Counter> weak1(shared1); | |
| pw::WeakPtr<Counter> weak2(shared2); | |
| shared2.reset(); | |
| // weak2 is expired. | |
| weak1.swap(weak2); | |
| EXPECT_EQ(weak2.Lock()->value(), 111u); | |
| EXPECT_TRUE(weak1.expired()); | |
| // weak1 is expired. | |
| weak1.swap(weak2); | |
| EXPECT_EQ(weak1.Lock()->value(), 111u); | |
| EXPECT_TRUE(weak2.expired()); | |
| } | |
| TEST_F(WeakPtrTest, CanswapWhenBothAreExpired) { | |
| auto shared1 = allocator_.MakeShared<Counter>(111u); | |
| auto shared2 = allocator_.MakeShared<Counter>(222u); | |
| pw::WeakPtr<Counter> weak1(shared1); | |
| pw::WeakPtr<Counter> weak2(shared2); | |
| shared1.reset(); | |
| shared2.reset(); | |
| weak1.swap(weak2); | |
| EXPECT_TRUE(weak1.expired()); | |
| EXPECT_TRUE(weak2.expired()); | |
| } | |
| TEST_F(WeakPtrTest, Conversions) { | |
| struct Foo { | |
| int foo() const { return 1; } | |
| }; | |
| struct Bar : public Foo { | |
| int bar() const { return 2; } | |
| }; | |
| struct Baz : public Bar { | |
| int baz() const { return 3; } | |
| }; | |
| pw::SharedPtr<Bar> shared = allocator_.MakeShared<Baz>(); | |
| pw::WeakPtr<Bar> bar(shared); | |
| // Upcast. | |
| pw::WeakPtr<Foo> foo = bar; | |
| EXPECT_EQ(static_cast<pw::WeakPtr<Bar>>(foo).Lock(), bar.Lock()); | |
| // Downcast. | |
| pw::SharedPtr<Baz> baz = static_cast<pw::WeakPtr<Baz>>(foo).Lock(); | |
| EXPECT_EQ(baz->foo(), 1); | |
| EXPECT_EQ(baz->bar(), 2); | |
| EXPECT_EQ(baz->baz(), 3); | |
| } | |
| } // namespace | |
| // TODO(b/402489948): Remove when portable atomics are provided by `pw_atomic`. | |
| #endif // PW_ALLOCATOR_HAS_ATOMICS |