简体   繁体   中英

C++ const getter method with lazy initialization

What is the proper way to implement a getter method for a lazily-initialized member variable and maintain const-correctness? That is, I would like to have my getter method be const, because after the first time it is used, it's a normal getter method. It is only the first time (when the object is first initialized) that const does not apply. What I would like to do:

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

Can I eat my cake and have it too?

That's fine and is the typical way of doing it.

You will have to declare expensive_object_ as mutable

mutable QObject *expensive_object_; 

mutable basically means "I know I'm in a const object, but modifying this won't break const-ness."

I propose encapsulating James Curran 's answer into a class of its own if you do this frequently:

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

Now use this in your code as follows:

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

使expensive_object_变得可变。

Use a const_cast to side-step const in that one specific place.

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

IMHO, this is better than making expensive_object_ mutable because you don't lose the const-safety in all your other methods.

Have you considered a wrapper class? You might be able to get away with something like a smart pointer, with only const-returning versions of operator* and operator-> and maybe operator[] ... You can get scoped_ptr -like behavior out of it as a bonus.

Let's give this a shot, I'm sure people can point out a few flaws:

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

Use of type_traits might make this better...

You'd need different versions for array pointers, and you might have to play around a bit with a creator functor or factory object or something if you wanted to pass in arguments to T 's constructor.

But you could use it like this:

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

Time to go off and compile this and see if I can break it... =)

在这里提出一个更好的解决方案,但它不处理没有默认构造函数的类型...

I've crated a class template Lazy<T> with the following features:

  • Familiar interface similar to standard smart pointers
  • Supports types without default constructor
  • Supports (movable) types without copy constructor
  • Thread-safe
  • Copyable using reference semantics: All copies share the same state; their value is created only once.

Here's how you use it:

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

Here's the implementation.

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

I played a bit with this topic and came up with an alternative solution in case you use C++11. Consider the following:

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

Main idea is to hold a function returning the expensive object as member instead of the object itself. In the constructor initialize with a function that does the following:

  • Initializes the expensive object
  • Replaces itself with a function which captures the already initialized object and just returns it.
  • Returns the object.

What I like about this is that the initialization code is still written in the constructor (where I'd naturally put it if lazyness was not needed) even though it will only get executed when the first query for the expensive object happens.

A downside of this approach is that std::function reassigns itself within it's execution. Accessing any non static members (captures in case of using lambda's) after the reassignment would result in undefined behavior so this requires extra attention. Also this is kind of a hack since GetExpensiveObject() is const but it still modifies a member attribute on first call.

In production code I'd probably prefer to just make the member mutable as James Curran described. This way the public API of your class clearly states that the member is not considered part of the objects state, therefore it doesn't affect constness.

After a bit more thinking I figured that std::async with std::launch::deferred could also be used in combination with a std::shared_future in order to be able to retrieve the result multiple times. Here is the 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;
};

Your getter isn't really const since it does change the content of the object. I think you're over thinking it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM