Я написал очень простую обертку вокруг 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 ответ
Действительно полезная идея — стоит ее создать.
Отсутствует #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совсем.— инди
Так же
std::forwardв конструкторе действительно вводит в заблуждение.[args = std::tuple{std::move(args)...}]является много яснее о том, что происходит. (И короче!)— инди
Да я бросился
operator*()и друзья. Однако это не суть обзора, и я уверен, что это легко исправить.— Тоби Спейт