diff --git a/API/fleece/RefCounted.hh b/API/fleece/RefCounted.hh index ec5b6537..7f88bc51 100644 --- a/API/fleece/RefCounted.hh +++ b/API/fleece/RefCounted.hh @@ -24,7 +24,7 @@ namespace fleece { enum Nullability {NonNull, MaybeNull}; - /** Simple thread-safe ref-counting implementation. + /** Thread-safe ref-counting implementation. `RefCounted` objects should be managed by \ref Retained smart-pointers: `Retained foo = new Foo(...)` or `auto foo = make_retained(...)`. \note The ref-count starts at 0, so you must call retain() on an instance, or assign it @@ -32,8 +32,15 @@ namespace fleece { class RefCounted { public: RefCounted() =default; - - int refCount() const FLPURE {return _refCount;} + + /// The number of (strong) references to this object. + int refCount() const FLPURE; + + /// Returns the strong and the weak reference count. + std::pair refCounts() const FLPURE; + + /// The global number of objects with weak references but no strong references. + static size_t zombieCount(); protected: RefCounted(const RefCounted &) { } // must not copy the refCount! @@ -44,28 +51,25 @@ namespace fleece { private: template friend class Retained; + template friend class WeakRetained; template friend T* FL_NULLABLE retain(T* FL_NULLABLE) noexcept; friend void release(const RefCounted* FL_NULLABLE) noexcept; friend void assignRef(RefCounted* FL_NULLABLE &dst, RefCounted* FL_NULLABLE src) noexcept; #if DEBUG - void _retain() const noexcept {_careful_retain();} - void _release() const noexcept {_careful_release();} + void _retain() const noexcept; + static constexpr uint64_t kInitialRefCount = 0x66666666; #else - ALWAYS_INLINE void _retain() const noexcept { ++_refCount; } - void _release() const noexcept; + ALWAYS_INLINE void _retain() const noexcept { ++_bothRefCounts; } + static constexpr uint64_t kInitialRefCount = 1; #endif + void _release() const noexcept; - static constexpr int32_t kCarefulInitialRefCount = -6666666; - void _careful_retain() const noexcept; - void _careful_release() const noexcept; + void _weakRetain() const noexcept; + void _weakRelease() const noexcept; + bool _weakToStrong() const noexcept; - mutable std::atomic _refCount -#if DEBUG - {kCarefulInitialRefCount}; -#else - {0}; -#endif + mutable std::atomic _bothRefCounts {kInitialRefCount}; }; template concept RefCountedType = std::derived_from; @@ -92,6 +96,15 @@ namespace fleece { assignRef((RefCounted* FL_NULLABLE&)holder, newValue); } + // Type `nullable_if::ptr` is a pointer to T with the appropriate nullability. +#if __has_feature(nullability) + template struct nullable_if; + template struct nullable_if {using ptr = X* _Nullable;}; + template struct nullable_if {using ptr = X* _Nonnull;}; +#else + template struct nullable_if {using ptr = X*;}; +#endif + [[noreturn]] void _failNullRef(); /** A smart pointer that retains the RefCounted instance it holds, similar to `std::shared_ptr`. @@ -109,14 +122,6 @@ namespace fleece { template class Retained { public: - #if __has_feature(nullability) - template struct nullable_if; - template struct nullable_if {using ptr = X* _Nullable;}; - template struct nullable_if {using ptr = X* _Nonnull;}; - #else - template struct nullable_if {using ptr = X*;}; - #endif - using T_ptr = typename nullable_if::ptr; // This is `T*` with appropriate nullability Retained() noexcept requires (N==MaybeNull) :_ref(nullptr) { } @@ -312,6 +317,49 @@ namespace fleece { return std::move(retained).detach(); } + + /** A ref-counted smart pointer to an arbitrary type `T` which does not have to derive from + * \ref RefCounted. However, it has to be instantiated with an instance of a subclass which + * _does_ derive from RefCounted. + * + * This is useful when you have something like a pure-virtual delegate interface and want to + * manage delegates with ref-counting. Making the interface derive from RefCounted would be + * awkward for classes that want to implement it, if they already derive from RefCounted + * through another base class. Instead, the delegate can be passed as a RetainedBySubclass. + * + * PS: If you need a _weak_ reference, use \ref WeakRetainedBySubclass. */ + template + class RetainedBySubclass { + public: + RetainedBySubclass() = default; + + template Sub> + explicit RetainedBySubclass(Sub* FL_NULLABLE sub) noexcept + requires (std::derived_from) + :_ptr{sub}, _ref{sub} { } + + template Sub, Nullability N> + explicit RetainedBySubclass(Retained sub) noexcept + requires (std::derived_from) + :_ptr{sub}, _ref{std::move(sub)} { } + + explicit operator bool() const noexcept FLPURE {return _ptr != nullptr;} + + T* FL_NULLABLE get() const noexcept FLPURE {return _ptr;} + T* FL_NULLABLE operator*() const noexcept FLPURE {return _ptr;} + T* FL_NULLABLE operator->() const noexcept FLPURE {return _ptr;} + + void clear() {_ref = nullptr; _ptr = nullptr;} + + private: + template friend class WeakRetainedBySubclass; + RetainedBySubclass(T* FL_NULLABLE ptr, Retained sub) + :_ptr(ptr), _ref(std::move(sub)) { } + + T* FL_NULLABLE _ptr = nullptr; + Retained _ref; + }; + } FL_ASSUME_NONNULL_END diff --git a/API/fleece/WeakRef.hh b/API/fleece/WeakRef.hh new file mode 100644 index 00000000..be467270 --- /dev/null +++ b/API/fleece/WeakRef.hh @@ -0,0 +1,263 @@ +// +// WeakRef.hh +// +// Copyright 2026-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. +// + +#pragma once +#include "RefCounted.hh" + +FL_ASSUME_NONNULL_BEGIN + +namespace fleece { + + [[noreturn]] void _failZombie(void*); + + + /** A smart pointer very much like \ref Retained except that it holds a _weak_ reference. + * The existence of the weak reference does not keep the referred-to object alive on its own; + * once no strong references exist, the object is freed and any weak references cleared. + * + * `WeakRetained` is useful for breaking reference cycles that could otherwise cause leaks. + * If one object in the cycle uses a WeakRef instead of a Ref, both objects will be freed + * properly once there are no external references to them. + * + * You can't dereference a WeakRef to a plain pointer, because that pointer could be + * invalidated at any moment by another thread releasing the last strong reference. + * Instead, dereferencing returns a Retained<> instance with a safe strong reference. + * + * It's important to remember that a WeakRef can be cleared unexpectedly. Unless you know + * for a fact that the pointee is still alive, it's recommended to call \ref tryGet and + * test the return value before using it, or else call \ref use. + */ + template + class WeakRetained { + public: + using T_ptr = typename nullable_if::ptr; // This is `T*` with appropriate nullability + + WeakRetained() noexcept requires (N==MaybeNull) :_ref(nullptr) { } + WeakRetained(std::nullptr_t) noexcept requires (N==MaybeNull) :WeakRetained() { } // optimization + + #if __has_feature(nullability) + WeakRetained(T* FL_NONNULL t) noexcept :_ref(_weakRetain(t)) { } + WeakRetained(T* FL_NULLABLE t) noexcept requires(N==MaybeNull) :_ref(_weakRetain(t)) { } + #else + WeakRetained(T* t) noexcept :_ref(_weakRetain(t)) { } + #endif + + WeakRetained(const WeakRetained &r) noexcept :_ref(_weakRetain(r._ref)) { } + WeakRetained(WeakRetained &&r) noexcept :_ref(std::move(r).detach()) { } + + template requires (std::derived_from && N >= UN) + WeakRetained(const WeakRetained &r) noexcept :_ref(_weakRetain(r._ref)) { } + template requires (std::derived_from && N >= UN) + WeakRetained(WeakRetained &&r) noexcept :_ref(std::move(r).detach()) { } + + ~WeakRetained() noexcept {if (_ref) _ref->_weakRelease();} + + WeakRetained& operator=(T_ptr t) & noexcept { + _weakRetain(t); + std::swap(_ref, t); + if (t) t->_weakRelease(); + return *this; + } + + WeakRetained& operator=(std::nullptr_t) & noexcept requires(N==MaybeNull) { // optimized assignment + auto oldRef = _ref; + _ref = nullptr; + if (oldRef) oldRef->_weakRelease(); + return *this; + } + + WeakRetained& operator=(const WeakRetained &r) & noexcept {*this = r._ref; return *this;} + + template requires(std::derived_from && N >= UN) + WeakRetained& operator=(const WeakRetained &r) & noexcept {*this = r._ref; return *this;} + + WeakRetained& operator= (WeakRetained &&r) & noexcept { + std::swap(_ref, r._ref); // old _ref will be released by r's destructor + return *this; + } + + template requires(std::derived_from && N >= UN) + WeakRetained& operator= (WeakRetained &&r) & noexcept { + if ((void*)&r != this) { + auto oldRef = _ref; + _ref = std::move(r).detach(); + _weakRelease(oldRef); + } + return *this; + } + + /// Converts any WeakRetained to non-nullable form (WeakRef), or throws if its value is nullptr. + WeakRetained asWeakRef() const & noexcept(!N) {return WeakRetained(_ref);} + + WeakRetained asWeakRef() && noexcept(!N) { + WeakRetained result(_ref, false); + _ref = nullptr; + return result; + } + + /// Returns true if I hold a non-null pointer. + /// Does _not_ check if the pointed-to object still exists. + /// @warning Do not use this to preflight \ref asRetained. + explicit operator bool () const noexcept FLPURE {return N==NonNull || (_ref != nullptr);} + + /// True if the object no longer exists. + bool invalidated() const noexcept {return !_ref || _ref->refCount() == 0;} + + /// If this holds a non-null pointer, and the object pointed to still exists, + /// returns a `Retained` instance holding a new strong reference to it. + /// Otherwise returns an empty (nullptr) `Retained`. + /// @warning You **must** check the `Retained` for null before dereferencing it! + Retained tryGet() const noexcept { + if (_ref->_weakToStrong()) + return Retained::adopt(_ref); + else + return nullptr; + } + + /// If this holds a non-null pointer, and the object pointed to still exists, + /// returns a `Retained` instance holding a new strong reference to it; otherwise throws. + /// @throws std::illegal_state if the object no longer exists. + Retained get() const { + if (!_ref || _ref->_weakToStrong()) + return Retained::adopt(_ref); + else [[unlikely]] + _failZombie(_ref); + } + + /// Similar to \ref get, will throw if the pointed-to object has been deleted. + /// @throws std::illegal_state if the object no longer exists. + Retained operator->() const {return get();} + + /// An alternative to \ref tryGet. If the object pointed to still exists, + /// calls the function `fn` with a pointer to it, then returns true. + /// Otherwise just returns false. + template FN> + [[nodiscard]] bool use(FN&& fn) const requires(std::is_void_v>) { + if (auto ref = tryGet()) { + fn(ref.get()); + return true; + } else { + return false; + } + } + + /// An alternative to \ref tryGet. If the object pointed to still exists, + /// calls the function `fn` with a pointer to it, returning whatever it returned. + /// Otherwise calls `elsefn` with no arguments, returning whatever it returned. + template FN, std::invocable<> ELSEFN> + auto use(FN&& fn, ELSEFN&& elsefn) const { + if (auto ref = tryGet()) { + return fn(ref.get()); + } else { + return elsefn(); + } + } + + private: + template friend class WeakRetained; + + WeakRetained(T_ptr t, bool) noexcept(N==MaybeNull) // private no-retain ctor + :_ref(t) { + if constexpr (N == NonNull) { + if (t == nullptr) [[unlikely]] + _failNullRef(); + } + } + + static T_ptr _weakRetain(T_ptr t) noexcept { + if constexpr (N == NonNull) { + t->_weakRetain(); // this is faster, and it detects illegal null (by signal) + } else { + if (t) t->_weakRetain(); + } + return t; + } + + static void _weakRelease(T_ptr t) noexcept { + if constexpr (N == NonNull) { + t->_weakRelease(); // this is faster, and it detects illegal null (by signal) + } else { + if (t) t->_weakRelease(); + } + } + + [[nodiscard]] T_ptr detach() && noexcept {return std::exchange(_ref, nullptr);} + + // _ref has to be declared nullable, even when N==NonNull, because a move assignment + // sets the moved-from _ref to nullptr. The WeakRetained may not used any more in this state, + // but it will be destructed, which is why the destructor also checks for nullptr. + T* FL_NULLABLE _ref; + }; + + template WeakRetained(T* FL_NULLABLE) -> WeakRetained; // deduction guide + + /// WeakRef is an alias for a non-nullable WeakRetained. + template using WeakRef = WeakRetained; + + /// NullableWeakRef is an alias for a (default) nullable WeakRetained. + template using NullableWeakRef = WeakRetained; + + + /** The weak-reference equivalent of \ref RetainedBySubclass. */ + template + class WeakRetainedBySubclass { + public: + WeakRetainedBySubclass() = default; + + template Sub> + explicit WeakRetainedBySubclass(Sub* FL_NULLABLE sub) noexcept + requires (std::derived_from) + :_ptr{sub}, _ref{sub} { } + + template Sub, Nullability N> + explicit WeakRetainedBySubclass(Retained sub) noexcept + requires (std::derived_from) + :WeakRetainedBySubclass{sub.get()} { } + + explicit operator bool() const noexcept FLPURE {return _ptr != nullptr;} + + bool invalidated() const noexcept {return _ref.invalidated();} + + /// If this holds a non-null pointer, and the object pointed to still exists, + /// returns a `RetainedBySubclass` instance holding a new strong reference to it. + /// Otherwise returns an empty (nullptr) result. + /// @warning You **must** check the result for null before dereferencing it! + RetainedBySubclass tryGet() const noexcept { + if (auto strongRef = _ref.tryGet()) + return {_ptr, std::move(strongRef)}; + else + return {}; + } + + /// An alternative to \ref tryGet. If the object pointed to still exists, + /// calls the function `fn` with a pointer to it, then returns true. + /// Otherwise just returns false. + template FN> + bool use(FN&& fn) const requires(std::is_void_v>) { + if (auto ref = tryGet()) { + fn(ref.get()); + return true; + } else { + return false; + } + } + + void clear() noexcept {_ref = nullptr; _ptr = nullptr;} + + private: + T* FL_NULLABLE _ptr = nullptr; + WeakRetained _ref; + }; + +} + +FL_ASSUME_NONNULL_END diff --git a/Fleece/Support/RefCounted.cc b/Fleece/Support/RefCounted.cc index 2e05f7cc..52c30c4d 100644 --- a/Fleece/Support/RefCounted.cc +++ b/Fleece/Support/RefCounted.cc @@ -11,6 +11,7 @@ // #include "fleece/RefCounted.hh" +#include "fleece/WeakRef.hh" #include "Backtrace.hh" #include #include @@ -22,68 +23,37 @@ namespace fleece { -#if !DEBUG - __hot void RefCounted::_release() const noexcept { - if (--_refCount <= 0) - delete this; - } -#endif + static std::atomic sZombieCount = 0; + static void fail(const RefCounted *obj, const char *what, uint64_t refCount, bool andThrow =true); - __hot void release(const RefCounted *r) noexcept { - if (r) r->_release(); + + static uint32_t refCountOf(uint64_t rc) { + return uint32_t(rc); + } + static uint32_t weakCountOf(uint64_t rc) { + return uint32_t(rc >> 32); } + #if DEBUG + static bool invalidCount(uint32_t count) { + return count >= 0x80000000; + } + #endif - __hot void assignRef(RefCounted* &holder, RefCounted *newValue) noexcept { - RefCounted *oldValue = holder; - if (_usuallyTrue(newValue != oldValue)) { - if (newValue) newValue->_retain(); - holder = newValue; - if (oldValue) oldValue->_release(); - } + int RefCounted::refCount() const { + return std::max(refCountOf(_bothRefCounts), 1u) - 1; // Don't expose the internal 1 extra ref } - __cold static void fail(const RefCounted *obj, const char *what, int refCount, - bool andThrow =true) - { - char *message = nullptr; - int n = asprintf(&message, - "RefCounted object <%s @ %p> %s while it had an invalid refCount of %d (0x%x)", - Unmangle(typeid(*obj)).c_str(), obj, what, refCount, unsigned(refCount)); - if (n < 0) - throw std::runtime_error("RefCounted object has an invalid refCount"); -#ifdef WarnError - WarnError("%s", message); -#else - fprintf(stderr, "WARNING: %s\n", message); -#endif - if (andThrow) { - std::string str(message); - free(message); - throw std::runtime_error(str); - } - free(message); + std::pair RefCounted::refCounts() const { + uint64_t both = _bothRefCounts; + return {std::max(refCountOf(both), 1u) - 1, weakCountOf(both)}; } - RefCounted::~RefCounted() noexcept { - // Store a garbage value to detect use-after-free - int32_t oldRef = _refCount.exchange(-9999999); - if (_usuallyFalse(oldRef != 0)) { -#if DEBUG - if (oldRef != kCarefulInitialRefCount) -#endif - { - // Detect if the destructor is not called from _release, i.e. the object still has - // references. This is probably a direct call to delete, which is illegal. - // Or possibly some other thread had a pointer to this object without a proper - // reference, and then called retain() on it after this thread's release() set the - // ref-count to 0. - fail(this, "destructed", oldRef, false); - } - } + size_t RefCounted::zombieCount() { + return sZombieCount; } @@ -94,34 +64,114 @@ namespace fleece { // the object. - void RefCounted::_careful_retain() const noexcept { - auto oldRef = _refCount++; + #if DEBUG + void RefCounted::_retain() const noexcept { + auto oldRef = _bothRefCounts++; // Special case: the initial retain of a new object that takes it to refCount 1 - if (oldRef == kCarefulInitialRefCount) { - _refCount = 1; + if (oldRef == kInitialRefCount) { + _bothRefCounts = 2; return; } // Otherwise, if the refCount was 0 we have a bug where another thread is destructing // the object, so this thread shouldn't have a reference at all. // Or if the refcount was negative or ridiculously big, this is probably a garbage object. - if (oldRef <= 0 || oldRef >= 10000000) + if (refCountOf(oldRef) <= 1 || refCountOf(oldRef) >= 0x7FFFFFFF) fail(this, "retained", oldRef); } +#endif + + + __hot void RefCounted::_release() const noexcept { + auto newRef = --_bothRefCounts; + if (refCountOf(newRef) == 1) { + if (weakCountOf(newRef) == 0) { + delete this; + } else { + this->~RefCounted(); // Weak references remain, so don't delete yet + ++sZombieCount; + if (--_bothRefCounts == 0) { + operator delete(const_cast(this)); + --sZombieCount; + } + } + } +#if DEBUG + else if (refCountOf(newRef) == 0 || invalidCount(refCountOf(newRef))) + fail(this, "released", newRef + 1); +#endif + } - void RefCounted::_careful_release() const noexcept { - auto oldRef = _refCount--; + void RefCounted::_weakRetain() const noexcept { + auto newRef = _bothRefCounts += (1ull << 32); +#if DEBUG + if (weakCountOf(newRef) < 1 || invalidCount(weakCountOf(newRef))) + fail(this, "weakRetained", newRef - (1ull << 32)); +#endif + } - // If the refCount was 0 we have a bug where another thread is destructing - // the object, so this thread shouldn't have a reference at all. - // Or if the refcount was negative or ridiculously big, this is probably a garbage object. - if (oldRef <= 0 || oldRef >= 10000000) - fail(this, "released", oldRef); - // If the refCount just went to 0, delete the object: - if (oldRef == 1) delete this; + void RefCounted::_weakRelease() const noexcept { + auto newRef = _bothRefCounts -= (1ull << 32); + if (weakCountOf(newRef) == 0) { + if (refCountOf(newRef) == 0) { + operator delete(const_cast(this)); + --sZombieCount; + } + } +#if DEBUG + else if (invalidCount(weakCountOf(newRef))) + fail(this, "weakReleased", newRef + (1ull << 32)); +#endif + } + + + bool RefCounted::_weakToStrong() const noexcept { + auto refs = _bothRefCounts.load(); + do { + #if DEBUG + if (weakCountOf(refs) == 0 || invalidCount(weakCountOf(refs)) || invalidCount(refCountOf(refs))) + fail(this, "weakToStrong", refs); + #endif + if (refCountOf(refs) <= 1) + return false; // No strong refs; fail + } while (!_bothRefCounts.compare_exchange_strong(refs, refs + 1)); + // Successfully added a strong ref; successs + return true; + } + + + RefCounted::~RefCounted() noexcept { + if (auto refs = _bothRefCounts.load(); refCountOf(refs) != 1) { +#if DEBUG + if (refCountOf(refs) != kInitialRefCount) +#endif + { + // Detect if the destructor is not called from _release, i.e. the object still has + // references. This is probably a direct call to delete, which is illegal. + // Or possibly some other thread had a pointer to this object without a proper + // reference, and then called retain() on it after this thread's release() set the + // ref-count to 0. + fail(this, "destructed", refs, false); + } + } + } + + + __hot void release(const RefCounted *r) noexcept { + if (r) r->_release(); + } + + + __hot void assignRef(RefCounted* &holder, RefCounted *newValue) noexcept { + RefCounted *oldValue = holder; + if (_usuallyTrue(newValue != oldValue)) { + if (newValue) newValue->_retain(); + holder = newValue; + if (oldValue) oldValue->_release(); + } } @@ -130,6 +180,30 @@ namespace fleece { throw std::invalid_argument("storing nullptr in a non-nullable Retained"); } + __cold void _failZombie(void* zombie) { + throw std::invalid_argument("storing nullptr in a non-nullable Retained"); + } + + + __cold static void fail(const RefCounted *obj, const char *what, uint64_t refCount, bool andThrow) { + char *message = nullptr; + int n = asprintf(&message, + "RefCounted object <%s @ %p> %s while it had an invalid refCount of 0x%llx", + Unmangle(typeid(*obj)).c_str(), obj, what, (unsigned long long)refCount); + if (n < 0) + throw std::runtime_error("RefCounted object has an invalid refCount"); + #ifdef WarnError + WarnError("%s", message); + #else + fprintf(stderr, "WARNING: %s\n", message); + #endif + if (andThrow) { + std::string str(message); + free(message); + throw std::runtime_error(str); + } + free(message); + } } diff --git a/Tests/PerfTests.cc b/Tests/PerfTests.cc index 25acdc82..4568415c 100644 --- a/Tests/PerfTests.cc +++ b/Tests/PerfTests.cc @@ -59,8 +59,8 @@ TEST_CASE("GetUVarint performance", "[.Perf]") { } bench.stop(); CHECK(result != 1); // bogus - fprintf(stderr, "n = %16llx; %2zd bytes; time = %.3f ns\n", - (long long)n, nBytes, + fprintf(stderr, "n = %16llx; %2zu bytes; time = %.3f ns\n", + (unsigned long long)n, nBytes, bench.elapsed() / kNRounds * 1.0e9); d *= 1.5; } diff --git a/Tests/SupportTests.cc b/Tests/SupportTests.cc index 01e1e477..2a849bbc 100644 --- a/Tests/SupportTests.cc +++ b/Tests/SupportTests.cc @@ -12,6 +12,7 @@ #include "fleece/FLBase.h" #include "fleece/InstanceCounted.hh" +#include "fleece/WeakRef.hh" #include "FleeceTests.hh" #include "FleeceImpl.hh" #include "Backtrace.hh" @@ -234,7 +235,7 @@ TEST_CASE("ConcurrentMap concurrency", "[ConcurrentMap]") { constexpr size_t bufSize = 10; for (int i = 0; i < kSize; i++) { char keybuf[bufSize]; - snprintf(keybuf, bufSize, "%x", i); + snprintf(keybuf, bufSize, "%x", unsigned(i)); keys.push_back(keybuf); } @@ -400,3 +401,113 @@ TEST_CASE("InstanceCounted") { } CHECK(InstanceCounted::liveInstanceCount() == n); } + + +TEST_CASE("WeakRef", "[RefCounted]") { + auto n = InstanceCounted::liveInstanceCount(); + { + Retained i1 = make_retained(); + WeakRetained weak1{i1}; + { + Retained r1 = weak1.tryGet(); + CHECK(r1 == i1); + } + + bool called = false; + bool result = weak1.use([&](ICTest* test) {CHECK(test == i1); called = true;}); + CHECK(result); + CHECK(called); + + unsigned v = weak1.use( + [&](ICTest* test) {CHECK(test == i1); return 0x1337;}, + [&]() {return 0xdead;}); + CHECK(v == 0x1337); + + CHECK(RefCounted::zombieCount() == 0); + + // Release the strong ref: + i1 = nullptr; + + CHECK(RefCounted::zombieCount() == 1); + + { + Retained r1 = weak1.tryGet(); + CHECK(r1 == nullptr); + } + + called = false; + result = weak1.use([&](ICTest* test) {CHECK(test == i1); called = true;}); + CHECK_FALSE(result); + CHECK_FALSE(called); + + v = weak1.use( + [&](ICTest* test) {CHECK(test == i1); return 0x1337;}, + [&]() {return 0xdead;}); + CHECK(v == 0xdead); + + } + CHECK(InstanceCounted::liveInstanceCount() == n); + CHECK(RefCounted::zombieCount() == 0); +} + + +class CycleTest : public ICTest { +public: + Retained strongRef; + WeakRetained weakRef; +}; + + +TEST_CASE("WeakRef Cycle", "[RefCounted]") { + auto n = InstanceCounted::liveInstanceCount(); + + Retained master = make_retained(); + Retained servant = make_retained(); + master->strongRef = servant; + servant->weakRef = master; + + master = nullptr; + CHECK(RefCounted::zombieCount() == 1); + CHECK(servant->weakRef.invalidated()); + servant = nullptr; + + CHECK(InstanceCounted::liveInstanceCount() == n); + CHECK(RefCounted::zombieCount() == 0); +} + + +struct TestDelegate { + virtual int grooviness() = 0; + virtual ~TestDelegate() = default; +}; + +static int callDelegate(RetainedBySubclass const& delegate) { + return delegate->grooviness(); +} + +struct TestDelegateImpl final : RefCounted, TestDelegate, InstanceCountedIn { + int grooviness() override {return 9000;} +}; + + +TEST_CASE("RetainedSubclass", "[RefCounted]") { + auto n = InstanceCounted::liveInstanceCount(); + { + auto delegate = make_retained(); + CHECK(callDelegate(RetainedBySubclass(delegate)) == 9000); + } + CHECK(InstanceCounted::liveInstanceCount() == n); +} + + +TEST_CASE("WeakRetainedSubclass", "[RefCounted]") { + auto n = InstanceCounted::liveInstanceCount(); + { + Retained delegate = make_retained(); + auto weakRef = WeakRetainedBySubclass(delegate); + CHECK(callDelegate(weakRef.tryGet()) == 9000); + delegate = nullptr; + CHECK(!weakRef.tryGet()); + } + CHECK(InstanceCounted::liveInstanceCount() == n); +}