std::vector<TObject*>-member: Object ownership double free

Dear ROOTers,

I have run into a problem / unclear understanding concering ROOT I/O which the reference documentation can sadly not solve.

Background (skip this part and jump to the problem for tl;dr if you are an expert):
Let me describe with an example:
[ul]
[li]In our experiment, we have a class containing “Hit” information. [/li]
[li]Derived from that “Hit” class, we have a “Cluster” class. [/li][/ul]
Both are TObject-derived, have a ClassDef / ClassImp, and are compiled with dictionary (rootcint / rootcling).
Both live in TClonesArray 's.

Naturally, the “Cluster” has a list of elements, which can be an arbitrary number of "Hit"s. To conserve space and allow to re-use this elements-array in e.g. a “Track” class (to store the Clusters, then), I used

as type for that elements-array.
As only the pointers to the Hits are in the elements-array, they are not owned by the Clusters - the Hits still live in their TClonesArray, and ROOT I/O keeps the pointers intact / deduplicates them on streaming, as far as I understood.

To not leak in the std::vector during TClonesArray::Clear, I implemented:

void Cluster::Clear(Option_t *opt) {
        elementsArray.clear();
        std::vector<const TObject*>().swap(elementsArray);
}

to free any heap allocated storage inside the std::vector. This is working perfectly fine, valgrind shows no leaks. I am well aware that the std::vector comes at some cost over a fixed-type fixed-size array especially when splitting into branches, but I consider this a negligible overhead over the gains in flexibility.

Problem:
Finally, the problem - it seems the std::vector<TObject*> believes it owns the TObject’s the TObject* inside point at during branch-reading.
This is what the documentation of TTree::GetEntry says for “TObject*” members, it seems this also holds for vectors of TObject*, which I found very unintuitive (since it is just the pointers which are stored inside the vector), but valgrind hinted me in that direction.

Due to this, when reading back the TTree (splitlevel 1) with the Hit and Cluster objects inside, I get a crash after a few entries - since the Hits are both owned by the splitted TClonesArray of Hits, AND owned by the std::vector<TObject*> in the Cluster objects, so they are deleted twice via the TGenCollectionProxy::Clear.

I tried to use

but it does not help (i.e. the “->” seems not to propagate to the TObject* members inside the vector).

How can I solve this, i.e. remove object ownership of the TObject* from the vector?

Is there a different shallow TObject* collection I can use instead of a std::vector which is suitable as a member of objects inside a TClonesArray, and does not own the objects inside?

I do not want to use:
[ul]
[li]TObjArray (I tried, but: https://sft.its.cern.ch/jira/browse/ROOT-6811 ). [/li]
[li]TRefArray (too expensive, we had this before, and already use many UUID in other places, which eats a lot of UUID handling time which is not needed here). [/li]
[li]TList (too expensive, I do not need a doubly linked list). [/li][/ul]

I am glad for any help.

Cheers,
Oliver

Dear ROOTers,

I forgot to add one important thing: For writing out the TClonesArrays, I use a TTree and add the TClonesArrays as Branches. Splitlevel is 1, but it seems other splitlevels also do not solve that issue.

As a workaround, I have now switched to TObjArray and changed all our uses of placement-new inside TClonesArray to ConstructedAt as recommended in https://sft.its.cern.ch/jira/browse/ROOT-6811 .
This solves the issue for std::vector<TObject*>, but it seems TObject* members are still “owned” by the objects in which they are declared as members (even though they are just pointers) and thus double-deleted upon readback.

I cannot move to
{code}
const TObject* someSourceTrack; //->
{code}
syntax as the source-track is living also in a TClonesArray (and also written out in a different branch), creating different streamers (with and without optimization).

Any ideas, apart from using a TRef here (which seems expensive when just the deduplicated pointer could be stored - we always read back all branches, so the auto-loading TRef can do is not needed)?

Cheers,
Oliver