EDIT: total re-edit because the original was becoming an unstructured mess :) Thanks for everyone's input so far; I hope I worked it into the text below.
Question
I'm in search for a lazily-created shareable pointer. I have a hypothetical big class Thing. Things are big and thus costly to make, but while they are used everywhere in the code (shared, passed around liberally, modified, stored for later use, etc.), they are often not actually used in the end, so delaying their actual creation until they are actually accessed is preferable. Thing thus needs to be lazily-created, plus needs to be shareable. Lets call this encapsulating pointer wrapper SharedThing.
class SharedThing {
...
Thing* m_pThing;
Thing* operator ->() {
// ensure m_pThing is created
...
// then
return m_pThing
);
}
...
SharedThing pThing;
...
// Myriads of obscure paths taking the pThing to all dark corners
// of the program, some paths not even touching it
...
if (condition) {
pThing->doIt(); // last usage here
}
Requirements
- instantiation of the actual Things must be delayed as long as possible; Things will only get created when first dereferencing the SharedThing
- SharedThing must be safe to use, so rather no required factory methods
- SharedThing must have a shared_ptr (like) interface
- sharing with a not-yet-created SharedThing must actually share the to-be-created Thing, but instantiation of the Thing must again be delayed until needed
- working with SharedThings must be as easy as possible (preferably 100% transparent, like working with actual Things)
- it must be somewhat performant
So far we've come up with four options:
Option 1
typedef std::shared_ptr<Thing> SharedThing;
SharedThing newThing() {
return make_shared<Thing>();
}
...
// SharedThing pThing; // pThing points to nullptr, though...
SharedThing pThing(new Thing()); // much better
SharedThing pThing = newThing(); // alternative
- 0% score; need a Thing instance from the start
- 0% score; you can say SharedThing pThing; but that's being a bit overly worried about things
- 100% score here ;)
- n.a. due to point 1
- 100% score
- 0% score, since creating all the Things everywhere (even when not used) is a drain on performance and it's exactly why I asked this question :)
The lack of score on points 1 and 6 is a killer here; no more option 1.
Option 2
class SharedThing: public shared_ptr<Thing> {};
and override specific members to ensure that when the shared_ptr is dereferenced, it creates the Thing just in time.
- maybe achievable by overriding the right members (depending on stl's implementation), but this fast becomes a mess I think
- 100% score
- 100% score, although mimicking all the constructors and operators is quite some work
- don't know if this is do-able...
- 100% score
- 100% score, if internally things are done smartly
This option is better than 1 and might be OK, but seems a mess and/or hackerish...
Option 3.1
class SharedThing {
std::shared_ptr<Thing> m_pThing;
void EnsureThingPresent() {
if (m_pThing == nullptr) m_pThing = std::make_shared<Thing>();
}
public:
SharedThing(): m_pThing(nullptr) {};
Thing* operator ->() {
EnsureThingCreated();
return m_pThing.get();
}
}
and add extra wrapper methods alike for operator * and const versions.
- 100% score
- 100% score
- do-able, but must create all interface members separately
- 0% score; when attaching to a nullptr'ed SharedThing (e.g. operator =), it needs to create the Thing first to be able to share
- 100% score again
- 50% score; 2 indirections
This one fails miserably on 4, so this one's off as well.
Option 3.2
class SharedThing {
typedef unique_ptr<Thing> UniqueThing;
shared_ptr<UniqueThing> m_pThing;
}
and add all other methods as in 3.1
- 100% score
- 100% score
- do-able, but must create all interface members separately
- 100% score
- 100% score again
- 25% score? we have 3 indirections here...
This seems OK apart from the suggested performance (need to test, though).
Option 4
class LazyCreatedThing {
Thing* m_pThing;
}
typedef shared_ptr<LazyCreatedThing> SharedThing;
SharedThing makeThing() {
return make_shared<LazyCreatedThing>();
}
and add all sorts of operator -> overloads to make LazyCreatedThing look like a Thing*
- 100% score
- same drawback as option 1 above
- 100% score here with no effort
- 100% score
- 0% score; dereferencing a SharedThing yields a LazyCreatedThing, so even though it might have it's operator -> for accessing the Thing, it will never get chained, resulting in (*pThing)->doIt();
- 25-50% score? we have 3 indirections here, or 2 if we can make use of std::make_shared
Failing miserably on 5 here makes this a no-no.
Conclusion
The best option so far thus seems 3.2; let's see what else we can come up with! :)
operator ->
keeps going recursively so it should work out. – Rigelshared_ptr
and shadowoperator->
as well asget
then? – Lacunarshared_ptr
s entire interface, just the parts you're actually using. If you need a capability that isn't there, add it. The interface isn't really that large anyway. – Associationismlazy_ptr
via a pointer toshared_ptr
, why would you? And how, anyway? Smart pointers are generally not dynamically allocated (it's not forbidden to do that, but it just doesn't make a lot of sense). So, it would require invoking undefined behavior to call the wrong destructor via a pointer of the wrong type anyway. Which would, besides, do nothing but leak the managed object, and that's neglegible compared to the crash that you'll get from deleting an object with automatic storage. – Lacunar