Я написал очень простую обертку вокруг 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*()
и друзья. Однако это не суть обзора, и я уверен, что это легко исправить.— Тоби Спейт