C ++ метод получения констант с отложенной инициализацией - PullRequest
30 голосов
/ 30 июля 2010

Как правильно реализовать метод получения для переменной с ленивой инициализацией и поддерживать константность?То есть я бы хотел бы , чтобы мой метод получения был постоянным, потому что после первого использования это обычный метод получения.Это только первый раз (когда объект впервые инициализируется), что const не применяется.Что я хотел бы сделать:

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

Могу ли я съесть свой торт и иметь его тоже?

Ответы [ 9 ]

22 голосов
/ 30 июля 2010

Это нормально, и это типичный способ сделать это.

Вы должны будете объявить expensive_object_ как mutable

mutable QObject *expensive_object_; 

mutable в основном означает «Я знаю, что я нахожусь в const-объекте, но изменение этого не нарушит константность».

19 голосов
/ 31 июля 2010

Я предлагаю инкапсулировать Джеймс Курран в свой собственный класс, если вы делаете это часто:

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_;
};
6 голосов
/ 30 июля 2010

Сделать expensive_object_ изменяемым.

4 голосов
/ 30 июля 2010

Используйте const_cast для бокового шага const в этом конкретном месте.

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

ИМХО, это лучше, чем сделать expensive_object_ mutable, потому что вы не потеряете const-безопасность во всех других ваших методах.

3 голосов
/ 31 июля 2010

Рассматривали ли вы класс оболочки? Возможно, вам удастся сойти с рук что-то вроде интеллектуального указателя, только с константными версиями 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 может сделать это лучше ...

Вам понадобятся разные версии для указателей массива, и вам, возможно, придется поэкспериментировать с функтором-создателем, фабричным объектом или чем-то еще, если вы хотите передать аргументы конструктору 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_;
};

Время уйти и скомпилировать это и посмотреть, смогу ли я это сломать ... =)

0 голосов
/ 06 июля 2017

Я немного поиграл с этой темой и придумал альтернативное решение, если вы используете C ++ 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'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;
};

Основная идея состоит в том, чтобы содержать функцию, возвращающую дорогой объект в качестве члена, а не сам объект. В конструкторе инициализируйте с помощью функции, которая делает следующее:

  • Инициализирует дорогой объект
  • Заменяет себя функцией, которая захватывает уже инициализированный объект и просто возвращает его.
  • Возвращает объект.

Что мне нравится в этом, так это то, что код инициализации все еще пишется в конструкторе (куда я естественно поместил бы его, если бы лень не требовалась), даже если он будет выполняться только тогда, когда произойдет первый запрос для дорогого объекта.

Недостатком этого подхода является то, что std :: function переназначает себя в своем исполнении. Доступ к любым нестатическим элементам (захватывает в случае использования лямбды) после переназначения приведет к неопределенному поведению, поэтому это требует дополнительного внимания. Кроме того, это своего рода хак, так как GetExoyObject () является const, но он все равно изменяет атрибут члена при первом вызове.

В производственном коде я, вероятно, предпочел бы просто сделать член изменяемым, как описано Джеймс Керран . Таким образом, общедоступный 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;
};
0 голосов
/ 19 июля 2016

Я создал шаблон класса 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>();
};
0 голосов
/ 13 июля 2013

Предложение еще более изящного решения здесь , но оно не обрабатывает типы без конструктора по умолчанию ...

0 голосов
/ 31 июля 2010

Ваш геттер на самом деле не является константой, так как он меняет содержимое объекта. Я думаю, что ты слишком много думаешь об этом.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...