Méthode c++ const getter avec initialisation paresseuse

Quelle est la bonne façon d'implémenter une méthode getter pour une variable membre paresseusement initialisée et de maintenir l'exactitude de la const? Autrement dit, je voudrais Que ma méthode getter soit const, car après la première fois qu'elle est utilisée, c'est une méthode getter normale. C'est seulement la première fois (lorsque l'objet est d'abord initialisé) qui const ne s'applique pas. Ce que je voudrais faire:

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

Puis-je manger mon gâteau et l'avoir aussi?

27
demandé sur Dave Mateer 2010-07-30 23:30:41

9 réponses

Je propose d'encapsuler la réponse de James Curran dans une classe à part si vous le faites fréquemment:

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

Maintenant, utilisez ceci dans votre code comme suit:

class MyClass {
  MyClass() : expensive_object_(CreateExpensiveObject) {}
  QObject* GetExpensiveObject() const {
    return expensive_object_.get();
  }
private:
  suspension<QObject *> expensive_object_;
};
17
répondu Ken Bloom 2017-05-23 10:30:04

C'est bien et c'est la façon typique de le faire.

, Vous aurez à déclarer expensive_object_ comme mutable

mutable QObject *expensive_object_; 

mutable fondamentalement signifie "Je sais que je suis dans un objet const, mais modifier cela ne cassera pas la const-ness."

22
répondu James Curran 2010-07-30 20:26:24

Rend expensive_object_ mutable.

6
répondu bshields 2010-07-30 19:34:24

Utilisez un const_cast pour faire un pas de côté dans cet endroit spécifique.

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

IMHO, c'est mieux que de faire expensive_object_ mutable parce que vous ne perdez pas la const-sécurité dans toutes vos autres méthodes.

4
répondu Jon-Eric 2010-07-30 19:52:57

Avez-vous considéré une classe wrapper? Vous pourriez être en mesure de vous en sortir avec quelque chose comme un pointeur intelligent, avec seulement des versions const-retournantes de operator* et operator-> et peut-être operator[]... Vous pouvez obtenir un comportement de type scoped_ptr en prime.

Donnons un coup de feu, je suis sûr que les gens peuvent signaler quelques défauts:

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

L'utilisation de {[6] } pourrait rendre cela meilleur...

Vous auriez besoin de différentes versions pour les pointeurs de tableau, et vous pourriez avoir à jouer un peu avec un foncteur creator ou un objet d'usine ou quelque chose si vous voulez passer des arguments au constructeur de T.

, Mais vous pouvez l'utiliser comme ceci:

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

Il est temps de partir et de compiler ceci et de voir si je peux le casser... =)

3
répondu leander 2010-07-30 20:06:23

Proposant une solution encore plus sophistiquée ici , mais elle ne gère pas les types sans constructeur par défaut...

0
répondu James Hugard 2017-05-23 10:30:04

J'ai créé un modèle de classe Lazy<T> avec les caractéristiques suivantes:

  • interface familière similaire aux pointeurs intelligents standard
  • prend en charge les types sans constructeur par défaut
  • prend en charge les types (mobiles) sans constructeur de copie
  • Thread-safe
  • copiable en utilisant la sémantique de référence: toutes les copies partagent le même état; leur valeur n'est créée qu'une seule fois.

Voici comment vous l'utilisez:

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

Voici la mise en œuvre.

#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
répondu Daniel Wolf 2017-07-01 18:41:06

J'ai joué un peu avec ce sujet et j'ai trouvé une solution alternative au cas où vous utiliseriez C++11. Considérez ce qui suit:

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

L'idée principale est de contenir une fonction renvoyant l'objet coûteux en tant que membre au lieu de l'objet lui-même. Dans le constructeur initialiser avec une fonction qui fait ce qui suit:

  • initialise l'objet coûteux
  • se remplace par une fonction qui capture l'objet déjà initialisé et le renvoie simplement.
  • renvoie le objet.

Ce que j'aime à ce sujet, c'est que le code d'initialisation est toujours écrit dans le constructeur (où je le mettrais naturellement si lazyness n'était pas nécessaire) même s'il ne sera exécuté que lorsque la première requête pour l'objet coûteux se produira.

Un inconvénient de cette approche est que std::function se réaffecte dans son exécution. L'accès à tous les membres non statiques (captures en cas d'utilisation de lambda) après la réaffectation entraînerait un comportement indéfini cela exige une attention supplémentaire. En outre, c'est une sorte de hack puisque GetExpensiveObject() est const mais il modifie toujours un attribut de membre lors du premier appel.

Dans le code de production, je préférerais probablement simplement rendre le membre mutable commeJames Curran décrit. De cette façon, L'API publique de votre classe indique clairement que le membre n'est pas considéré comme faisant partie de l'état des objets, donc cela n'affecte pas la constance.

Après un peu plus de réflexion, j'ai pensé que std:: async avec std:: launch:: deferred peut également être utilisé en combinaison avec un std::shared_future afin de pouvoir récupérer le résultat plusieurs fois. Voici le code:

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;
};
0
répondu otama 2017-07-06 14:26:36

Votre getter n'est pas vraiment const car il change le contenu de l'objet. Je pense que vous êtes sur la pensée.

-1
répondu Jay 2010-07-30 20:10:11