Ленивый общий указатель — оператор присваивания

#c #c 17 #shared-ptr #lazy-initialization

#c #c 17 #общий-ptr #отложенная инициализация

Вопрос:

Я создал простой класс ленивых общих указателей. Однако в настоящее время у меня может быть только один его экземпляр, и мой дизайн не поддерживает назначение копирования.

 /// <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> amp;amp; other) :
        init(other.init),
        ptr(other.ptr){
        other.init = nullptr;
        other.ptr = nullptr;
    };

    LazySharedPtr(const LazySharedPtramp; 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>amp; InitAndGet() const {
        if (!ptr) { ptr = init(); }
        return ptr;
    }
};
 

Есть ли у вас какие-либо идеи, как улучшить это, чтобы поддерживать назначение копирования?

Текущий дизайн не поддерживает это:

 class MyObject { };

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

потому что после инициализации t2 , t1 не будет инициализирован.

Я думал иметь internal shared_ptr в качестве указателя на указатель и передавать его. Однако с необработанным указателем я должен управлять количеством ссылок, и это std::shared_ptr<std::shared_ptr<T>> кажется странным. Или нет?

У вас есть какие-либо другие идеи?

Комментарии:

1. Первая перегрузка Create() принимает параметры шаблона, но никогда их не использует. На самом деле он не может быть вызван — если вы явно укажете аргументы шаблона, это будет неоднозначно при второй перегрузке; и если вы этого не сделаете, он не будет использоваться для сбоя при выводе параметров шаблона.

2. @IgorTandetnik это была опечатка

3. Вы принимаете свои переменные аргументы по значению, а не по универсальной ссылке, поэтому все ваши std::forward вызовы бессмысленны — аргументы все равно копируются.

4. Вместо shared_ptr<T> , хранить shared_ptr<ControlBlock> , с struct ControlBlock {std::function<std::shared_ptr<T>()> init; std::shared_ptr<T> ptr;}; помощью . Тогда копирование было бы тривиальным. Примечание: сделать эту вещь потокобезопасной может быть сложно; std::call_once может помочь.

5. @IgorTandetnik При этом у меня возникают проблемы с преобразованием дочернего элемента в родительский. ControlBlock<Child> для ControlBlock<Parent> того, чтобы назначить ctor

Ответ №1:

Вот набросок — не тестировался, с недостающими частями, которые должно быть легко заполнить. Я надеюсь, что общая идея ясна.

 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>amp; 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;
  }
};
 

По сути, вводите стирание полностью.