2010-07-30 13 views
26

¿Cuál es la forma correcta de implementar un método getter para una variable de miembro inicializada de forma diferida y mantener la corrección de const? Es decir, me gustaría como que mi método getter sea const, porque después de la primera vez que se usa, es un método getter normal. Es solo la primera vez (cuando el objeto se inicializa por primera vez) que const no se aplica. Lo que me gustaría hacer:C++ método get getter con inicialización diferida

class MyClass { 
    MyClass() : expensive_object_(NULL) {} 
    QObject* GetExpensiveObject() const { 
    if (!expensive_object_) { 
     expensive_object = CreateExpensiveObject(); 
    } 
    return expensive_object_; 
    } 
private: 
    QObject *expensive_object_; 
}; 

¿Puedo comer mi pastel y tenerlo también?

Respuesta

17

Propongo encapsular respuesta James Curran 's en una clase de su propio si lo hace con frecuencia:

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

Ahora utilizar esto en su código como sigue:

class MyClass { 
    MyClass() : expensive_object_(CreateExpensiveObject) {} 
    QObject* GetExpensiveObject() const { 
    return expensive_object_.get(); 
    } 
private: 
    suspension<QObject *> expensive_object_; 
}; 
+0

Me gusta tu respuesta también. Implementar una interfaz de puntero puede ser más consistente que una interfaz de referencia. –

+0

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. –

+1

Me sorprende que no haya encontrado una biblioteca de Boost para esto. –

21

Eso está bien y es la manera típica de hacerlo.

Tendrá que declarar como expensive_object_mutable

mutable QObject *expensive_object_; 

mutable básicamente significa "Sé que estoy en un objeto constante, pero esta modificación no va a romper const-ness".

+1

@ Jon-Eric: Si no es realmente const, debería ser mutable. Tener un const_cast en un punto no lo expresa bien. –

+1

@David: Mi opinión es que realmente es const y que un lugar es el ÚNICO lugar en el que será modificado (inicializado). Si muchos métodos/lugares necesitan modificarlo (como un caché), mutable tendría más sentido. –

+5

Estamos hablando de los detalles de implementación de una variable presumiblemente protegida/privada.¿Qué importa si otros métodos también pueden cambiarlo? Otros métodos también podrían sacar un molde constante y cambiarlo también. No hay encapsulación adicional al no usar mutable. Sin embargo, hacer que sea mutable le dice al lector sobre la definición de clase algo valioso que debe saber. – frankc

6

Marca expensive_object_ mutable.

4

Usa un const_cast para formar const de un lado en ese lugar específico.

QObject* GetExpensiveObject() const { 
    if (!expensive_object_) { 
    const_cast<QObject *>(expensive_object_) = CreateExpensiveObject(); 
    } 
    return expensive_object_; 
} 

en mi humilde opinión, esto es mejor que hacer expensive_object_mutable debido a que no pierda la const-seguridad en todos sus otros métodos.

+1

¿Por qué el voto a favor? –

+0

de acuerdo. Mientras yo doy grep la base del código para const_cast y modelos de estilo C periódicamente para asegurarme de que no haya violaciones const, este es un caso en el que creo que sería justificable. (Aunque no estoy seguro de que estoy de acuerdo con su -1 en la respuesta mutable ... Ambas son buenas formas de manejar el problema, solo que con diferentes consecuencias: alarmante sintaxis de código contra más mutabilidad de la necesaria. ..) – leander

+2

No soy el que menosprecia, pero creo que esto podría llevar a un comportamiento indefinido si la instancia de MyClass se crea como 'const'. –

3

¿Usted ha considerado una clase de envoltura? Es posible que pueda salirse con la suya con algo así como un puntero inteligente, con solo versiones regresivas de operator* y operator-> y tal vez operator[] ... Puede obtener el comportamiento similar al scoped_ptr como bonificación.

Vamos a darle a este un tiro, estoy seguro que la gente puede señalar algunos defectos:

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(); } 
}; 

El uso de type_traits podría hacer esto mejor ...

Se necesitaría diferentes versiones para array punteros, y es posible que tenga que jugar un poco con un functor creador u objeto de fábrica o algo así si desea pasar argumentos al constructor T.

Pero se puede utilizar de esta manera:

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_; 
}; 

tiempo para salir y compilar esto y ver si puedo romperlo ... =)

+0

Probablemente quiera ver nuestras dos versiones, por si acaso el constructor predeterminado no es lo que tenía en mente, o en caso de que el nulo fuera un valor de retorno válido de CreateExpensiveObject. –

-1

su captador no es realmente const desde cambia el contenido del objeto. Creo que ya te estás dando cuenta.

+1

Esto es precisamente para lo que se inventó "mutable", por lo que podría declarar algo 'const' aunque tenga que modificar su memoria, aunque sepa que su interfaz siempre tendrá el mismo aspecto. –

0

Proponiendo una solución aún más lujosa here, pero no maneja los tipos sin un constructor predeterminado ...

0

He Crated una plantilla de clase Lazy<T> con las siguientes características:

  • Interfaz familiar similar a punteros inteligentes estándar
  • soporta tipos sin constructor predeterminado
  • soportes (móviles) tipos sin constructor de copia
  • Thread-safe
  • Copiable usando semántica de referencia: todas las copias comparten el mismo estado; su valor se crea solo una vez

Así es como usted lo utiliza:

// 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) { /* ... */ } 

Aquí está la aplicación.

#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>(); 
}; 
+0

Con respecto a la licencia de MIT: Consulte https://meta.stackexchange.com/questions/12527/do-i-have-to-worry-about-copyright-issues-for-code-posted-on-stack-overflow – Matthias

+0

@Matthias: ¡gracias por avisarme! Cuando comencé en StackOverflow, estoy bastante seguro de que tenían una licencia predeterminada más restrictiva. En aquel entonces, poner mi código bajo la Licencia MIT tenía sentido. Me alegra que ahora sea CC, así que eliminé esa frase. –

0

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