С++ const getter метод с ленивой инициализацией
Каков надлежащий способ реализации метода getter для лениво инициализированной переменной-члена и поддержания const-correctness? То есть, я хотел бы, чтобы мой метод getter был const, потому что после первого использования он является обычным методом getter. Только в первый раз (когда объект сначала инициализируется), const не применяется. Что я хотел бы сделать:
class MyClass {
MyClass() : expensive_object_(NULL) {}
QObject* GetExpensiveObject() const {
if (!expensive_object_) {
expensive_object = CreateExpensiveObject();
}
return expensive_object_;
}
private:
QObject *expensive_object_;
};
Могу ли я есть пирог и иметь его?
Ответы
Ответ 1
Я предлагаю инкапсулировать Джеймс Карран в свой класс, если вы это часто делаете:
template <typename T>
class suspension{
std::tr1::function<T()> initializer;
mutable T value;
mutable bool initialized;
public:
suspension(std::tr1::function<T()> init):
initializer(init),initialized(false){}
operator T const &() const{
return get();
}
T const & get() const{
if (!initialized){
value=initializer();
initialized=true;
}
return value;
}
};
Теперь используйте это в своем коде следующим образом:
class MyClass {
MyClass() : expensive_object_(CreateExpensiveObject) {}
QObject* GetExpensiveObject() const {
return expensive_object_.get();
}
private:
suspension<QObject *> expensive_object_;
};
Ответ 2
Это прекрасный и типичный способ сделать это.
Вам нужно объявить expensive_object_
как mutable
mutable QObject *expensive_object_;
mutable
в основном означает "Я знаю, что я в const-объекте, но изменение этого не нарушит константы".
Ответ 3
Сделайте expensive_object_
изменчивым.
Ответ 4
Используйте const_cast
для бокового шага const в этом конкретном месте.
QObject* GetExpensiveObject() const {
if (!expensive_object_) {
const_cast<QObject *>(expensive_object_) = CreateExpensiveObject();
}
return expensive_object_;
}
IMHO, это лучше, чем создание expensive_object_
mutable
, потому что вы не потеряете const-безопасность во всех своих других методах.
Ответ 5
Рассматривали ли вы класс оболочки? Вы могли бы уйти с чем-то вроде умного указателя, только с константными версиями operator*
и operator->
и, возможно, operator[]
... Вы можете получить от него scoped_ptr
-подобное поведение как бонус.
Позвольте этому выстрелу, я уверен, что люди могут указать несколько недостатков:
template <typename T>
class deferred_create_ptr : boost::noncopyable {
private:
mutable T * m_pThing;
inline void createThingIfNeeded() const { if ( !m_pThing ) m_pThing = new T; }
public:
inline deferred_create_ptr() : m_pThing( NULL ) {}
inline ~deferred_create_ptr() { delete m_pThing; }
inline T * get() const { createThingIfNeeded(); return m_pThing; }
inline T & operator*() const { return *get(); }
inline T * operator->() const { return get(); }
// is this a good idea? unintended conversions?
inline T * operator T *() const { return get(); }
};
Использование type_traits
может сделать это лучше...
Вам понадобятся разные версии для указателей на массивы, и вам может потребоваться немного поиграть с создателем-функтором или объектом factory или что-то еще, если вы хотите передать аргументы конструктору T
.
Но вы можете использовать его так:
class MyClass {
public:
// don't need a constructor anymore, it comes up NULL automatically
QObject * getExpensiveObject() const { return expensive_object_; }
protected:
deferred_create_ptr<QObject> expensive_object_;
};
Время уйти и скомпилировать это и посмотреть, могу ли я сломать его... =)
Ответ 6
Предлагая решение
Ответ 7
Я создал шаблон класса Lazy<T>
со следующими функциями:
- Знакомый интерфейс, похожий на стандартные интеллектуальные указатели
- Поддерживает типы без конструктора по умолчанию
- Поддерживает (перемещаемые) типы без конструктора копирования
- потокобезопасна
- Возможность копирования с использованием ссылочной семантики: все копии имеют одно и то же состояние; их значение создается только один раз.
Вот как вы его используете:
// Constructor takes function
Lazy<Expensive> lazy([] { return Expensive(42); });
// Multiple ways to access value
Expensive& a = *lazy;
Expensive& b = lazy.value();
auto c = lazy->member;
// Check if initialized
if (lazy) { /* ... */ }
Здесь реализация.
#pragma once
#include <memory>
#include <mutex>
// Class template for lazy initialization.
// Copies use reference semantics.
template<typename T>
class Lazy {
// Shared state between copies
struct State {
std::function<T()> createValue;
std::once_flag initialized;
std::unique_ptr<T> value;
};
public:
using value_type = T;
Lazy() = default;
explicit Lazy(std::function<T()> createValue) {
state->createValue = createValue;
}
explicit operator bool() const {
return static_cast<bool>(state->value);
}
T& value() {
init();
return *state->value;
}
const T& value() const {
init();
return *state->value;
}
T* operator->() {
return &value();
}
const T* operator->() const {
return &value();
}
T& operator*() {
return value();
}
const T& operator*() const {
return value();
}
private:
void init() const {
std::call_once(state->initialized, [&] { state->value = std::make_unique<T>(state->createValue()); });
}
std::shared_ptr<State> state = std::make_shared<State>();
};
Ответ 8
Я немного поиграл с этой темой и придумал альтернативное решение, если вы используете С++ 11. Рассмотрим следующее:
class MyClass
{
public:
MyClass() :
expensiveObjectLazyAccess()
{
// Set initial behavior to initialize the expensive object when called.
expensiveObjectLazyAccess = [this]()
{
// Consider wrapping result in a shared_ptr if this is the owner of the expensive object.
auto result = std::shared_ptr<ExpensiveType>(CreateExpensiveObject());
// Maintain a local copy of the captured variable.
auto self = this;
// overwrite itself to a function which just returns the already initialized expensive object
// Note that all the captures of the lambda will be invalidated after this point, accessing them
// would result in undefined behavior. If the captured variables are needed after this they can be
// copied to local variable beforehand (i.e. self).
expensiveObjectLazyAccess = [result]() { return result.get(); };
// Initialization is done, call self again. I'm calling self->GetExpensiveObject() just to
// illustrate that it safe to call method on local copy of this. Using this->GetExpensiveObject()
// would be undefined behavior since the reassignment above destroys the lambda captured
// variables. Alternatively I could just use:
// return result.get();
return self->GetExpensiveObject();
};
}
ExpensiveType* GetExpensiveObject() const
{
// Forward call to member function
return expensiveObjectLazyAccess();
}
private:
// hold a function returning the value instead of the value itself
std::function<ExpensiveType*()> expensiveObjectLazyAccess;
};
Основная идея состоит в том, чтобы удерживать функцию, возвращающую дорогостоящий объект как член вместо самого объекта. В конструкторе инициализируется с помощью функции, которая выполняет следующие действия:
- Инициализирует дорогой объект
- Заменяет себя функцией, которая захватывает уже инициализированный объект и просто возвращает его.
- Возвращает объект.
Что мне нравится в этом, так это то, что код инициализации все еще написан в конструкторе (где я, естественно, выразился бы, если бы ленивость не нужна), хотя он будет выполняться только при первом запросе дорогого объекта.
Недостатком этого подхода является то, что std:: function переназначает себя в его выполнении. Доступ к любым нестационарным членам (захват в случае использования лямбда) после переназначения приведет к поведению undefined, поэтому это требует дополнительного внимания. Также это своего рода хак, поскольку GetExpensiveObject() является const, но при первом вызове он изменяет атрибут member.
В производственном коде я бы предпочел просто сделать член измененным как Джеймс Карран. Таким образом, в публичном API вашего класса четко указано, что элемент не считается частью состояния объектов, поэтому он не влияет на константу.
Подумав немного, я понял, что std:: async с std:: launch:: deferred также может использоваться в сочетании с std:: shared_future, чтобы иметь возможность получить результат несколько раз. Вот код:
class MyClass
{
public:
MyClass() :
deferredObj()
{
deferredObj = std::async(std::launch::deferred, []()
{
return std::shared_ptr<ExpensiveType>(CreateExpensiveObject());
});
}
const ExpensiveType* GetExpensiveObject() const
{
return deferredObj.get().get();
}
private:
std::shared_future<std::shared_ptr<ExpensiveType>> deferredObj;
};
Ответ 9
Ваш получатель не является константой, поскольку он изменяет содержимое объекта. Я думаю, вы уже думали об этом.