8
votes

If I have a boost::shared_array<T> (or a boost::shared_ptr<T[]>), is there a way to obtain a boost::shared_ptr<T> which shares with the array?

So for example, I might want to write:

shared_array<int> array(new int[10]);
shared_ptr<int> element = &array[2];

I know that I can't use &array[2], because it just has type int *, and it would be dangerous for shared_ptr<int> to have an implicit constructor that will take that type. Ideally shared_array<int> would have an instance method on it, something like:

shared_ptr<int> element = array.shared_ptr_to(2);

Unfortunately I can't find anything like this. There is an aliasing constructor on shared_ptr<int> which will alias with another shared_ptr<T>, but it won't allow aliasing with shared_array<T>; so I can't write this either (it won't compile):

shared_ptr<int> element(array, &array[2]);
//Can't convert 'array' from shared_array<int> to shared_ptr<int>

Another option I played with was to use std::shared_ptr<T> (std instead of boost). The specialisation for T[] isn't standardised, so I thought about defining that myself. Unfortunately, I don't think that's actually possible in a way that doesn't break the internals of the aliasing constructor, as it tries to cast my std::shared_ptr<T[]> to its own implementation-specific supertype, which is no longer possible. (Mine is currently just inheriting from the boost one at the moment.) The nice thing about this idea would have been that I could implement my instance shared_ptr_to method.

Here's another idea I experimented with, but I don't think it's efficient enough to be acceptable as something we're potentially going to use throughout a large project.

template<typename T>
boost::shared_ptr<T> GetElementPtr(const boost::shared_array<T> &array, size_t index) {
    //This deleter works by holding on to the underlying array until the deleter itself is deleted.
    struct {
        boost::shared_array<T> array;
        void operator()(T *) {} //No action required here.
    } deleter = { array };
    return shared_ptr<T>(&array[index], deleter);
}

The next thing I'm going to try is upgrading to Boost 1.53.0 (we currently only have 1.50.0), using shared_ptr<T[]> instead of shared_array<T>, and also always using boost instead of std (even for non-arrays). I'm hoping this will then work, but I haven't had a chance to try it yet:

shared_ptr<int[]> array(new int[10]);
shared_ptr<int> element(array, &array[2]);

Of course I'd still prefer the instance method syntax, but I guess I'm out of luck with that one (short of modifying Boost):

shared_ptr<int> element = array.shared_ptr_to(2);

Anyone else have any ideas?

3
Your deleter idea is the way it should be done. There is nothing particularly inefficient in it (if in doubt, run an experiment with a profiler). - n. 1.8e9-where's-my-share m.
@n.m. I think it'll create a new control block (on the heap) for the new shared_ptr to refer to. Granted it's not that bad, but it's still better if it can share the same control block. :) - entheh
Do you need a shared_array at all? If you had a shared_ptr<T> (with appropriate deleter), you could use the aliasing constructors. - Luc Danton
I've since thought about that. The only thing missing then is operator[]. I'll post an answer saying what I did in the end. - entheh

3 Answers

1
votes

You are doing strange stuff. Why do you need shared_ptr to element? Do you want element of array be passed somewhere else and hold down your array from removal?

If yes, than std::vector<shared_ptr<T>> is more suited for that. That solution is safe, standard and has fine granularity on objects removal

0
votes

boost::shared_ptr does not seem to support this nativly. Maybe you can work around this with a custom deleter. But std::shared_ptr offers a special constructor to support what you want:

struct foo
{
    int a;
    double b;
};

int main()
{
    auto sp1 = std::make_shared<foo>();
    std::shared_ptr<int> sp2 (sp1,&sp1->a);
}

Here, sp1 and sp2 share ownership of the foo object but sp2 points to a member of it. If sp1 is destroyed, the foo object will still be alive and sp2 will still be valid.

0
votes

Here's what I did in the end.

I made my own implementation of shared_array<T>. It effectively extends shared_ptr<vector<T>>, except it actually extends my own wrapper for vector<T> so that the user can't get the vector out. This means I can guarantee it won't be resized. Then I implemented the instance methods I needed - including weak_ptr_to(size_t) and of course operator[].

My implementation uses std::make_shared to make the vector. So the vector allocates its internal array storage separately from the control block, but the vector itself becomes a member of the control block. It's therefore about equivalent to forgetting to use std::make_shared for a normal type - but because these are arrays, they're likely to be largeish and few, so it's less important.

I could also create an implementation that's based on shared_ptr<T> but with default_delete<T[]> or whatever is required, but it would have to allocate the array separately from the control block (so there's not much saving versus vector). I don't think there's a portable way to embed a dynamically sized array in the control block.

Or my implementation could be based on boost::shared_array<T>, and use the custom deleter when taking element pointers (as per the example in the question). That's probably worse in most cases, because instead of a one-time hit allocating the array, we get a hit every time we take an aliased pointer (which could happen a lot with very short-lived ones).

I think the only reasonable way to get it even more optimal would be to use the latest boost (if it works; I didn't get as far as trying it before I changed my mind, mainly because of the desire for my own instance members). And of course this means using the boost ones everywhere, even for single objects.

But, the main advantage with what I went with is Visual Studio's debugger is (I'm told) good at displaying the contents of std::shared_ptrs and std::vectors, and (we expect) less good at analysing the contents of boost things or custom things.

So I think what I've done is basically optimal. :)