struct TestSafeObject
TestSafeObject() = default;
~TestSafeObject()
// [1]
safeRef.reset();
SafeRef<TestSafeObject> safeRef { this };
using TestScopedAccess = SafeRef<TestSafeObject>::ScopedAccess;
// Elsewhere
std::thread thread;
TestSafeObject testSafeObject;
auto thread = std::move (std::thread ([testSafeRef = testSafeObject.safeRef]
if (auto access = TestSafeObject::TestScopedAccess (testSafeRef)) // [3]
// Do some stuff [4]
} // [2]
thread.join();
In the above, it’s possible that [2]
will be reached, enter testSafeObject
's destructor but not reset the safeRef, then [3]
happens on the background thread giving a valid access
object.
Then [4]
is being called whilst testSafeObject is sitting in its destructor waiting on the lock.
As I said, this is extremely unlikely to happen and depending on your vtable, compiler, scheduler and which way the wind is blowing may or may not actually cause problems…
However, I think if [4]
calls virtual methods of TestSafeObject
, you can end up with problems.
(Disclaimer, above code typed directly in and not tested)
It should be SafeRef<TestSafeObject>::Ptr
.
This way, after the referenced object is destroyed, the actual SafeRef<TestSafeObject>
is still alive and well (but it knows that the referenced object itsn’t available because its owner_
member is nullptr
). The SafeRef
object will be destroyed when both the reference-able object and all of its referencers are destroyed.
SafeRef<TestSafeObject>::Ptr safeRef { new SafeRef<TestSafeObject> (this) };
using TestScopedAccess = SafeRef<TestSafeObject>::ScopedAccess;
// Elsewhere
std::thread thread;
TestSafeObject testSafeObject;
auto thread = std::move (std::thread ([testSafeRef = testSafeObject.safeRef]
auto access = TestSafeObject::TestScopedAccess (testSafeRef);
if (access) // [3]
// Do some stuff [4]
access->blahblahblah();
} // [2]
thread.join();
I think that how you intend it to be used?
But the problem remains, you can still end up in the situation where TestSafeObject
is being destructed but the safeRef
member is still valid so grants an owner_
that is in the middle of its destructor.
It’s a small race, and possibly benign (there’s debate over whether any race can be benign though) but the only way to avoid any race is to have external pointer with strong and weak counts. That way, it’s impossible to start destructing the object before the ref count gets to 0.
As soon as you move the weak-pointer management inside the class, there’s no way to tell it to invalidate the pointer before you’ve entered the destructor.
Maybe take a look as std::enable_shared_from_this
https://en.cppreference.com/w/cpp/memory/enable_shared_from_this
if you want to be able to get a std::shared_ptr
out of an existing object?
dave96:
But the problem remains, you can still end up in the situation where TestSafeObject
is being destructed but the safeRef
member is still valid so grants an owner_
that is in the middle of its destructor.
As the reset()
call happens as the very first thing in the destructor, owner_
would only be at the beginning of its destructor rather than in the middle, without any destruction of it or its members yet to take place.
dave96:
But to be honest, the safest bet is to use std::shared_ptr
and std::weak_ptr
. They use atomic ref counts to ensure you can only dereference through a valid std::shared_ptr
, even if obtained via a std::weak_ptr
.
The only downside to this is that the memory storing the object will only be freed after all the weak_ptr
s have been destroyed. This means you may need to poll them occasionally to reset them.
Your suggestion for using std::shared_ptr
is good. But if I understand correctly, I cannot do that for my use-case externally to the object, because I’m implementing an existing API where I need to create a new object that is a sub-class of some given class… Perhaps I could used std::shared_ptr
rather than juce::ReferenceCountedObject
for my access-management, but it still has to be contained in my object iiuc…