I have my own multi-threaded service which handles some commands. The service consists of command parser, worker threads with queues and some caches. I don't want to keep an eye on each object's life-cycle, so I use shared_ptr's very extensive. Every component uses shared_ptr's in its own way:
And there is another underlying service (for example, command receiver and sender) that has the same structure, but uses his own cache, workers and shared_ptr's. It's independent from my service and is maintained by another developer.
It's a complete nightmare, when I try to track all shared_ptr dependencies to prevent cross-references.
Is there a way to specify some shared_ptr "interface" or "policy", so I will know which shared_ptr's I can pass safely to the underlying service without inspecting the code or interacting with the developer? Policy should involve the shared_ptr owning-cycle, for example, the worker holds the functor with binded shared_ptr since the dispatch() function call and only til some other function call, while the cache holds the shared_ptr since the cache's constructor call and til the cache's destructor call.
Especially, I'm curious about shutdown situation, when the application may freeze while waiting the threads to join.
There is no silver bullet... and shared_ptr
certainly is not one.
My first question would be: do you need all those shared pointers ?
The best way to avoid cyclic references is to define the lifetime policy of each object and make sure they are compatible. This can be easily documented:
unique_ptr
, I am now responsible for the objectshared_ptr
, I expect to be able to keep a handle to the object myself without adversely affecting youNow, there are rare situations where the use of shared_ptr
is indeed necessary. The indication of caches lead me to think that it might be your case, at least for some uses.
In this case, you can (at least informally) enforce a layering approach.
0
(the base) to infiniteA
might only hold a shared_ptr
to an object of type B
if, and only if, Layer(A) > Layer(B)
Note that we expressly forbid sibling relationships. With this scheme, no circle of references can ever be formed. Indeed, we obtain a DAG (Directed Acyclic Graph).
Now, when a type is created, it must be ascribed a layer number, and this must be documented (preferably in the code).
An object may change of layer, however:
Note: by convention, types of objects which cannot hold any reference are usually in the layer 0
.
Note 2: I first stumble upon this convention in an article by Herb Sutter, where he applied it to Mutexes and tried to prevent deadlock. This is an adaptation to the current issue.
This can be enforced a bit more automatically (by the compiler) as long as you are ready to work your existing code base.
We create a new SharedPtr
class aware of our layering scheme:
template <typename T>
constexpr unsigned getLayer(T const&) { return T::Layer; }
template <typename T, unsigned L>
class SharedPtrImpl {
public:
explicit SharedPtrImpl(T* t): _p(t)
{
static_assert(L > getLayer(std::declval<T>()), "Layering Violation");
}
T* get() const { return _p.get(); }
T& operator*() const { return *this->get(); }
T* operator->() const { return this->get(); }
private:
std::shared_ptr<T> _p;
};
Each type that may be held in such a SharedPtr
is given its layer statically, and we use a base class to help us out:
template <unsigned L>
struct LayerMember {
static unsigned const Layer = L;
template <typename T>
using SharedPtr<T> = SharedPtrImpl<T, L>;
};
And now, we can easily use it:
class Foo: public LayerMember<3> {
public:
private:
SharedPtr<Bar> _bar; // statically checked!
};
However this coding approach is a little more involved, I think that convention may well be sufficient ;)