Оболочка общего указателя C ++ для ленивой инициализации

Я написал очень простую обертку вокруг std::shared_ptr который поддерживает отложенную инициализацию. Вы видите какие-нибудь проблемы?

#include <functional>
#include <tuple>
#include <utility>

template <class T, typename ... Args>
class LazySharedPtr {
public:
    LazySharedPtr(Args ... args) :
        ptr(nullptr) {
        this->init = [args = std::make_tuple(std::forward<Args>(args) ...)]() mutable {
            return std::apply([](auto&& ... args) {
                return std::make_shared<T>(std::forward<Args>(args) ...);
                }, std::move(args));
        };
    }

    virtual ~LazySharedPtr() = default;

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

    void Init() {
        this->InitAndGet();
    }

    std::shared_ptr<T> Get() {
        return (ptr) ? ptr : InitAndGet();
    }

    const std::shared_ptr<T> Get() const {
        return (ptr) ? ptr : InitAndGet();      
    }

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

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

    explicit operator bool() const noexcept {
        return this->IsInited();
    }

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

    std::shared_ptr<T> InitAndGet() const {
        ptr = this->init();
        return ptr;
    }
};

Заметка: Предупреждение отчета Visual Studio для this->Get().get():

Предупреждение C26815 Указатель болтается, поскольку он указывает на временный экземпляр, который был уничтожен.

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

1 ответ
1

Действительно полезная идея — стоит ее создать.

Отсутствует #include <memory>. С этим исправлением я получаю почти чистую компиляцию:

255367.cpp:9:5: warning: ‘LazySharedPtr<int, int>::init’ should be initialized in the member initialization list [-Weffc++]

Я согласен, мы должны использовать список инициализации для init:

LazySharedPtr(Args ... args)
    : init{[args = std::make_tuple(std::forward<Args>(args) ...)]() mutable
           {
               return std::apply([](auto&& ... args) {
                                     return std::make_shared<T>(std::forward<Args>(args) ...);
                                 }, std::move(args));
           }},
      ptr{}
{}

Я не понимаю, почему у нас есть внутренняя лямбда. Нет веской причины не пройти std::make_shared() прямо к std::apply(), как это:

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{}
{}

Я думаю, что этим классом было бы проще пользоваться. Типы аргументов не обязательно должны быть частью самого класса, так как они стираются std::function. Так что сделайте шаблон класса только T как аргумент. В противном случае нам понадобятся отдельные пути кода для наших интеллектуальных указателей, если они созданы по-другому. Намного лучше разделить тип, который существенный к LazySharedPtr из тех, которые случайный:

template <class T>
class LazySharedPtr {
public:
    template <typename ... Args>
    LazySharedPtr(Args ... args);
}

Отсутствуют некоторые функции, которых я ожидал бы от простой замены для std::shared_ptr:

  • operator*
  • get()
  • reset()

Я бы также ожидал LazySharedPtr быть назначенным std::shared_ptr (вызывая в процессе функцию создателя). Это может снизить потребность в этих функциях (особенно reset(), что здесь может не иметь смысла).

С другой стороны, я не думаю, что LazySharedPtr предназначен как базовый класс для наследования, поэтому виртуальный деструктор не требуется. И нам не нужно IsInited(), учитывая, что у нас уже есть operator bool.

Подумайте о том, что значит копировать объект с ленивым указателем. В его нынешнем виде копирование материализованного экземпляра дает еще один общий указатель на тот же T объект, но копирование нематериализованного экземпляра приведет к другому T объекты в оригинале и копии. Интересно, может ли это затруднить правильное использование; мы могли бы захотеть сделать это типом только для перемещения и потребовать приведение к std::shared_ptr чтобы скопировать (таким образом материализуя объект).

Для эффективности InitAndGet() не должен копировать общий указатель, а должен возвращать ссылку. Я не люблю именование — в соглашении C ++ используется snake_case для имен функций.

Много лишнего this-> загромождение кода.


Модифицированный код

#include <cstddef>
#include <functional>
#include <memory>
#include <tuple>
#include <utility>

template <class T>
class LazySharedPtr {
public:
    template <typename... Args>
    LazySharedPtr(Args... args)
        : ptr{},
          init{[args = std::make_tuple(std::move<Args>(args)...)]() mutable
               { return std::apply(std::make_shared<T, Args...>, std::move(args)); }}
    {}

    // compiler-defaulted copy/move construct and assign, and destructor

    auto operator->() const
    { return object().get(); }

    auto operator*() const
    { return *object(); }

    auto operator[](std::ptrdiff_t idx)
    { return object()[idx]; }

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

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

private:
    mutable std::shared_ptr<T> ptr;
    std::function<std::shared_ptr<T>()> init;

    auto& object() const
    {
        if (!ptr) { ptr = init(); }
        return ptr;
    }
};


int main()
{
    LazySharedPtr<int> a{0};
    auto b = std::shared_ptr<int>{a};
    return *b;
}

  • Разве мы не должны возвращать ссылку из operator* вместо указателя? Также std::shared_ptr::get() это const функция, которая возвращает не-const указатель, поэтому я думаю, что мы можем обойтись только одной функцией для операторов разыменования и стрелок: T& operator*() const и T* operator->() const. (Ну я думаю ptr изменчив, поэтому мы все равно могли бы это сделать …)

    — user673679


  • Да, есть немного const путаница здесь. Для (умного) указателя, который const, operator-> должен вернуться T* const… Не T const*… И const в T* const ложно на возвращаемый тип, поэтому в основном operator->() const должен вернуться T*. Но я думаю, что это все спорный вопрос, потому что ленивый тип не должен поддерживать const функции-члены вообще (даже с operator bool… какова должна быть семантика этого? IsInited было более логичным именем). Если вы хотите const указатель, приведите его к std::shared_ptr; но LazySharedPtr не должен поддерживать const совсем.

    — инди

  • 1

    Так же std::forward в конструкторе действительно вводит в заблуждение. [args = std::tuple{std::move(args)...}] является много яснее о том, что происходит. (И короче!)

    — инди

  • Да я бросился operator*() и друзья. Однако это не суть обзора, и я уверен, что это легко исправить.

    — Тоби Спейт

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *