Search code examples
c++c++17shared-ptrlazy-initialization

Lazy shared pointer - assignment operator


I have created simple lazy shared pointer class. However, currently I can have only single instance of it and my design does not support copy assignment.

/// <summary>
/// Simple lazy shared pointer
/// Pointer is initialized when first needed
/// 
/// Create new instance with CreateLazy static method
/// 
/// Copy is disabled, pointer can only be moved
/// If we would copy it not initialized
/// then two instances can be created
/// - from original and from copy
/// </summary>
template <class T>
class LazySharedPtr{
public:
        
    static LazySharedPtr<T> Create(){
        std::function<std::shared_ptr<T>()> customInit = [](){
            return std::make_shared<T>();
        };

        return LazySharedPtr(customInit);
    };

    template <typename ... Args>
    static LazySharedPtr<T> Create(Args ... args){
        return LazySharedPtr(std::forward<Args>(args) ...);
    };
    
    
    LazySharedPtr() :
        init(nullptr),
        ptr(nullptr){
    };

    LazySharedPtr(std::function<std::shared_ptr<T>()> customInit) :
        init(customInit),
        ptr(nullptr){
    };
    
    template <typename Y>
    LazySharedPtr(LazySharedPtr<Y> && other) :
        init(other.init),
        ptr(other.ptr){
        other.init = nullptr;
        other.ptr = nullptr;
    };

    LazySharedPtr(const LazySharedPtr& other) = delete;


    virtual ~LazySharedPtr() = default;

    T* operator->(){
        return InitAndGet().get();
    }

    const T* operator->() const{
        return InitAndGet().get();
    }

    T* operator*(){
        return InitAndGet().get();
    }

    const T* operator*() const{
        return InitAndGet().get();
    }

    explicit operator bool() const noexcept{
        return (ptr != nullptr);
    }

    explicit operator std::shared_ptr<T>() const{
        return InitAndGet();
    }

    template <typename U>
    friend class LazySharedPtr;

protected:    
    std::function<std::shared_ptr<T>()> init;

    mutable std::shared_ptr<T> ptr;

    template <typename ... Args>
    LazySharedPtr(Args ... args) :
        init([args = std::make_tuple(std::forward<Args>(args) ...)]() mutable {
        return std::apply(std::make_shared<T, Args...>, std::move(args));
    }),
        ptr(nullptr){
    };
    
    std::shared_ptr<T>& InitAndGet() const {
        if (!ptr) { ptr = init(); }
        return ptr;
    }
};

Do you have any idea, how to improve this to support copy assignment?

Current design does not support this:

class MyObject { };

LazySharedPtr<MyObject> t1 = LazySharedPtr<MyObject>::Create();
LazySharedPtr<MyObject> t2 = t1;

because after initialization of t2, t1 wont be inited.

I have thought to have internal shared_ptr as pointer to pointer and pass it around. However, with raw pointer I have to manage reference count and doing std::shared_ptr<std::shared_ptr<T>> seems weird. Or does not?

Do you have any other idea?


Solution

  • Here's a sketch - not tested, with missing pieces that should be easy to fill in. I hope the general idea is clear.

    template <class T>
    class LazySharedPtr {
      struct ControlBlock {
        std::shared_ptr<T> ptr;
        std::function<std::shared_ptr<T>()> factory;
        std::shared_ptr<T> InitAndGet() {
          // Add thread safety here.
          if (!ptr) {
            ptr = factory();
            factory = nullptr;
          }
          return ptr;
        }
      };
    
      std::function<std::shared_ptr<T>()> init;
      // This member is not strictly necessary, it's just a cache.
      // An alternative would be to call `init` every time.
      std::shared_ptr<T> ptr;
    
    public:
      // For exposition, assume all `T`s are constructible from `int`
      LazySharedPtr(int x) {
        auto control = std::make_shared<ControlBlock>();
        control->factory = [x]() { return std::make_shared<T>(x); };
        init = [control]() {return control->InitAndGet(); }
      }
    
      template <typename U>
      LazySharedPtr(const LazySharedPtr<U>& other)
          : ptr(other.ptr) {
        if (!ptr) {
          auto other_init = other.init;
          init = [other_init]() { return std::shared_ptr<T>(other_init()); };
        }  
      }
    
      std::shared_ptr<T> InitAndGet() {
        if (!ptr) {
          ptr = init();
          init = nullptr;
        }
        return ptr;
      }
    };
    

    Basically, type erasure all the way down.