Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lazy-constructed shared_ptr

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! :)

like image 654
Carl Colijn Avatar asked Sep 17 '15 21:09

Carl Colijn


People also ask

Why would you choose shared_ptr instead of Unique_ptr?

Use unique_ptr when you want to have single ownership(Exclusive) of the resource. Only one unique_ptr can point to one resource. Since there can be one unique_ptr for single resource its not possible to copy one unique_ptr to another. A shared_ptr is a container for raw pointers.

What is the difference between Make_shared and shared_ptr?

The difference is that std::make_shared performs one heap-allocation, whereas calling the std::shared_ptr constructor performs two.

Is shared_ptr slow?

When we create an object with new operator in shared_ptr there will be two dynamic memory allocations that happen, one for object from the new and the second is the manager object created by the shared_ptr constructor. Since memory allocations are slow, creating shared_ptr is slow when compared to raw pointer.

Do I need to delete a shared_ptr?

The purpose of shared_ptr is to manage an object that no one "person" has the right or responsibility to delete, because there could be others sharing ownership. So you shouldn't ever want to, either.


1 Answers

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;
like image 70
Stas Avatar answered Sep 22 '22 08:09

Stas