Jugué un poco con este tema y se me ocurrió una solución alternativa en caso de que utilice C++ 11. Considere lo siguiente:
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's 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;
};
La idea principal es llevar a cabo una función que devuelve el objeto caro como miembro en lugar del objeto en sí. En el constructor de inicializar con una función que hace lo siguiente:
- Inicializa el objeto de cara
- se reemplaza con una función que captura el objeto que ya está iniciado y simplemente lo devuelve.
- Devuelve el objeto.
Lo que me gusta de esto es que el código de inicialización todavía está escrito en el constructor (donde naturalmente lo puse si no se necesitaba pereza) aunque solo se ejecutará cuando la primera consulta objeto sucede.
Una desventaja de este enfoque es que std :: function se reasigna dentro de su ejecución. El acceso a cualquier miembro no estático (capturas en caso de utilizar lambda) después de la reasignación daría como resultado un comportamiento indefinido, por lo que esto requiere una atención especial. También es una especie de hack, ya que GetExpensiveObject() es const pero aún modifica un atributo de miembro en la primera llamada.
En el código de producción, probablemente prefiera simplemente hacer el miembro mutable como James Curran descrito. De esta forma, la API pública de su clase establece claramente que el miembro no se considera parte del estado de los objetos, por lo tanto, no afecta la constness.
Después de pensar un poco más pensé que std :: async con std :: launch :: deferred también podría usarse en combinación con std :: shared_future para poder recuperar el resultado varias veces. Aquí está el código:
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;
};
Me gusta tu respuesta también. Implementar una interfaz de puntero puede ser más consistente que una interfaz de referencia. –
Esta parece ser la solución más universal que "funciona bien" con la especificación de C++. Es feo debajo de la superficie, pero la API pública se mantiene limpia. –
Me sorprende que no haya encontrado una biblioteca de Boost para esto. –