Lazy-constructed shared_ptr
Asked Answered
N

3

8

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

  1. instantiation of the actual Things must be delayed as long as possible; Things will only get created when first dereferencing the SharedThing
  2. SharedThing must be safe to use, so rather no required factory methods
  3. SharedThing must have a shared_ptr (like) interface
  4. 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
  5. working with SharedThings must be as easy as possible (preferably 100% transparent, like working with actual Things)
  6. 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
  1. 0% score; need a Thing instance from the start
  2. 0% score; you can say SharedThing pThing; but that's being a bit overly worried about things
  3. 100% score here ;)
  4. n.a. due to point 1
  5. 100% score
  6. 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.

  1. maybe achievable by overriding the right members (depending on stl's implementation), but this fast becomes a mess I think
  2. 100% score
  3. 100% score, although mimicking all the constructors and operators is quite some work
  4. don't know if this is do-able...
  5. 100% score
  6. 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.

  1. 100% score
  2. 100% score
  3. do-able, but must create all interface members separately
  4. 0% score; when attaching to a nullptr'ed SharedThing (e.g. operator =), it needs to create the Thing first to be able to share
  5. 100% score again
  6. 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

  1. 100% score
  2. 100% score
  3. do-able, but must create all interface members separately
  4. 100% score
  5. 100% score again
  6. 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*

  1. 100% score
  2. same drawback as option 1 above
  3. 100% score here with no effort
  4. 100% score
  5. 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();
  6. 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! :)

Nedranedrah answered 17/9, 2015 at 21:29 Comment(14)
I'm confused, why are you creating these objects if they are not going to be used?Merrillmerrily
Why not use a shared_ptr to the wrapper class? operator -> keeps going recursively so it should work out.Rigel
Why don't you just derive from shared_ptr and shadow operator-> as well as get then?Lacunar
You don't need to mimic shared_ptrs 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.Associationism
@clcto: because they get passed around a lot, but for some their fate is uncertain until very late in the game, at which point the whole chain leading to them gets destroyed as well.Nedranedrah
@MarkRansom: sure, but I'm a bit of a perfectionist :) Anyway, it seems the most viable option so far indeedNedranedrah
@Claudiu: the moment I just now went to bed I thought of it as well, edited my post then saw I already had comments after the page refresh :) Added as option #3. But also see my objection there.Nedranedrah
@Damon: I'd rather not derive from a class with a non-virtual interface - shearing, destruction throught base classes and all. Although I agree that (most/all) instances of this class are going to live on the stack, so there shouldn't be much problems there...Nedranedrah
I'd like to clarify something. Do those Things have mutable state? And what are you going to do when SharedThing that has no object is copied?Loux
@ГригорийШуренков: they indeed have mutable state, and a copy of a nullptr-filled shared_ptr should link to the 'same' nullptr as the original - that is: when later one of them does instantiate the Thing, the other should point to it as well. The proxy class is able to do this.Nedranedrah
What's with that fear about deriving from a class with a non-virtual interface? You will never destroy a lazy_ptr via a pointer to shared_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
I'd suggest that you edit the question, clarifying what behavior you want for copying unconstructed objects. Currently it looks that, you want shared_ptr<optional<Thing>> or shared_ptr<shared_ptr<Thing>> with some object constructed eagerly.Loux
@ГригорийШуренков: good observation - indeed my suggestion for a custom SharedThing class is flawed wrt sharing to-be-created Things; fixed the post to reflect that issue. Seems like an intermediate LazyCreatedThing proxy class is the best option after all, despite of having to ensure every instantitation spot creates their shared_ptr with a new'ed LazyCreatedThing (make_shared mitigates the cost here somewhat).Nedranedrah
Been rumaging with this idea a bit further, but there's a complication with the proxy class (and @Claudiu): chaining the ->'s doesn't happen when the first link is a shared_ptr; shared_ptr returns a regular pointer to the proxy object, so any 'magic' chaining of it's operator -> to get at the actual Thing won't happen. Result: very verbose syntax to get at the Things. Will update the question to reflect this.Nedranedrah
C
3

I would implement LazyThing wrapper. I guess it is much easier to adapt Thing interface, rather than std::shared_ptr one.

class Thing
{
public:
    void Do()
    {
        std::cout << "THING" << std::endl;
    }
};

class LazyThing
{
public:
    void Do()
    {
        getThing().Do();
    }

private:
    Thing& getThing()
    {
        if (!thing_)
            thing_ = std::make_unique<Thing>();

        return *thing_;
    }

    std::unique_ptr<Thing> thing_;
};

Now, you can use it with any smart pointer, or even create it on the stack:

LazyThing lazy;
auto sharedLazy = std::make_shared<LazyThing>();
auto uniqueLazy = std::make_unique<LazyThing>();

Or as in your example:

typedef std::shared_ptr<LazyThing> SharedLazyThing;

SharedLazyThing newThing() {
  return std::make_shared<LazyThing>();
}
...
auto pThing = newThing();

UPDATE

If you want to guarantee shared semantic and do not bother with calling newThing() or any other factory method, just give up on shared_ptr interface. It is not needed there.

Implement SharedLazyThing as a value type with shared semantic. The tricky stuff is that you need to add yet another level of indirection to provide lazy construction of shared Thing object.

class SharedLazyThing
{
   using UniqueThing = std::unique_ptr<Thing>;

public:
   void Do()
   {
      getThing().Do();
   }

private:
   Thing& getThing()
   {
      if (!*thing_)
         *thing_ = std::make_unique<Thing>();

      return **thing_;
   }

   std::shared_ptr<UniqueThing> thing_ = std::make_shared<UniqueThing>();
};

Now, you can simply use SharedLazyThing everywhere.

SharedLazyThing thing1;
SharedLazyThing thing2(thing1);
SharedLazyThing thing3 = thing1;
Calisaya answered 18/9, 2015 at 7:35 Comment(9)
I'm leaning more and more to this solution as well, but I guess I want to have my cake and eat it too :) So an intermediate proxy, but without the obligatory use of the above newThing() all over the place. Oh well; let's wait some more and see if someone comes up with a smart out-of-the-box solution?Nedranedrah
then again, you now have to enforce auto pThing = newThing() all over the place, instead of just using SharedThing pThing being ready to go, which the wrapper around shared_ptr enables you to do... Oh, the choices!Nedranedrah
The best thing about the proxy: if You contain your pointer in the proxy you can change it to unique ptr or shared pointer once and it changes consistently in your code base.Larrigan
@Stas: this is what I currently have (except I have operator *, -> etc., and not wrappers for each method on Thing) :) It gave me the 20% performance boost over an always-filled regular shared_ptr<Thing>, but I guess I really wanted a more shared_ptr look-alike solution. Can't have it both ways though, it seems.Nedranedrah
@CarlColijn Yes. Anyway, my point is that it is still easier to adaptThing interface, rather than shared_ptr. And, I'm not sure why you need shared_ptr like interface. If the object is always not null, *, -> and other operators are not needed.Calisaya
@Calisaya Exactly why I don't want to adapt/inherit from shared_ptr. Though as Григорий Шуренков pointed out in his comment, you must also be able to attach to yet-to-be-created Things, which complicates things enough that a lightweight proxy class shines more brightly again. And I want to keep the Things nullptr's as long as possible due to performance gains. Your 2nd option allows for that, but they in turn cannot be shared themselves (stored in containers for later use e.g.), so your SharedLazyThing needs a shared_ptr<SharedLazyThing>, adding another level of indirection :)Nedranedrah
@CarlColijn Hmm... considering my 2nd option, why can't you use std::vector<SharedLazyThing> ?Calisaya
@CarlColijn " you must also be able to attach to yet-to-be-created Things" Again, 2nd option works there. You just link them by using copy-constructor or assignment operator. If you define SharedLazyThing thing1; auto thing11(thing1);, both thing1 and thing11 will share the same not yet created Thing object.Calisaya
@Stas: You're totally right; I read over the fact that it's a double pointer set-up (shared_ptr linking to unique_ptr linking to Thing). Regarding storing them in e.g. a vector: that's then indeed quite possible.Nedranedrah
O
1

You can actually implement the "lazy" aspect of your "thing" in a separate, reusable class, and use it for shared or non-shared objects.

#include <memory>
#include <iostream>
#include <optional>

template <typename T>
class Lazy
{
public:
    T const& operator*() const
    {
        return provide();
    }

    T& operator*()
    {
        return provide();
    }

    T const* operator->() const
    {
        return &provide();
    }

    T* operator->()
    {
        return &provide();
    }

private:
    T& provide() const
    {
        // warning: no thread safety
        if (not object) {
            object = T{};
        }
        return *object;
    }

    mutable std::optional<T> object;
};

struct Thing
{
    Thing()
    {
        std::cout << "(constructing new thing) ";
    }

    int value = 5;
};

int main()
{
    Lazy<Thing> lazyThing;
    std::cout << "lazy thing constructed\n";
    std::cout << "accessing lazy thing: " << lazyThing->value << "\n";
    std::cout << "accessing lazy thing again: " << lazyThing->value << "\n";

    auto sharedLazyThing = std::make_shared<Lazy<Thing>>();
    std::cout << "shared lazy thing constructed\n";
    std::cout << "accessing shared lazy thing: " << (**sharedLazyThing).value << "\n";
    std::cout << "accessing shared lazy thing again: " << (**sharedLazyThing).value << "\n";

    return 0;
}

outputs

lazy thing constructed
accessing lazy thing: (constructing new thing) 5
accessing lazy thing again: 5
shared lazy thing constructed
accessing shared lazy thing: (constructing new thing) 5
accessing shared lazy thing again: 5

However this only works for "things" with default constructor. I guess often you want to control with what arguments a "thing" will be created. Maybe a factory std::function or something similar can be used to extend the Lazy class.

Please note that the object member is mutable, because even when used as const object, we must be able to create the member variably on-the-fly.

Osi answered 23/8, 2024 at 10:59 Comment(2)
This is indeed also a nice (belated) solution! It's basically option 4, but with LazyCreatedThing worked out to behave like a proper lazy proxy for it's Thing. It thus also somewhat satisfies point 2, but you're still forced to call make_shared when instantiating, and point 5 still remains as you showed: shared_ptr's operator -> doesn't chain with the underlying class's operator ->... Which is really a shame tbh. I must say I don't even know anymore why I needed this construct in the first place with these constraints, but I probably was a bit too over-perfectionistic there :)Nedranedrah
Instead of a std::shared_ptr<Lazy<Thing>> you could also use a Lazy<std::shared_ptr<Thing>>. It would not work out-of-the-box, as instead of the default constructor, std::make_shared needs to be called. But you could specialize Lazy for shared pointers, and I think with such a specialization it would be possible to access the underlying class directly with the * and -> operators.Osi
P
0

Maybe I misunderstand the question but couldn't it be as simple as this?

class Factory
{
private:

  std::shared_ptr<My1stType> my1st_ {};
  std::shared_ptr<My2ndType> my2nd_ {};
  std::shared_ptr<My3rdType> my3rd_ {};
  // …

public:

  std::shared_ptr<My1stType>
  get1st()
  {
    if (!this->my1st_)
      this->my1st_ = std::make_shared<My1stType>(/* … */);
    return this->my1st_;
  }

  // …
};

As shown above, this is not thread-safe, however, in case this matters to you.

Photobathic answered 17/9, 2015 at 22:23 Comment(4)
Yes, I think you misunderstand the question.Associationism
Nice, but I'd like to say SharedThing pThing; *** lots of code, function calls with refs to pThing, etc., possibly doing nothing to it *** if (condition) { pThing->x() *** here we first need the thing created *** } *** end of life of pThing; if condition didn't hold, we'd be better off if pThing never got created ***Nedranedrah
My suggestion being of course that you replace every pThing->doit() with factory.getThing()->doit() and instead of passing around the smart pointer, you pass around a reference to the factory.Photobathic
@5gon12eder: Unfortunately I have many such objects around simultaneously, the number also being unknown at forehand. The suggested usage pattern thus isn't really a fit for my situation. Plus that you then have a much more verbose call syntax; something I want to avoid by conforming to the regular shared_ptr interface.Nedranedrah

© 2022 - 2025 — McMap. All rights reserved.