C++带有延迟初始化的const getter方法

37

如何实现一个懒加载成员变量的getter方法并保持const正确性?也就是说,我希望我的getter方法是const的,因为在第一次使用之后,它就是一个普通的getter方法。只有在对象第一次初始化时,const不适用。我想做的是:

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

我能吃掉我的蛋糕而又留着它吗?

9个回答

27

这很好,也是通常的做法。

您需要将expensive_object_声明为mutable

mutable QObject *expensive_object_; 

mutable 的基本含义是:“我知道自己在一个 const 对象中,但修改它不会破坏 const 属性。”


1
@Jon-Eric:如果它不是真正的const,那么它应该是可变的。只在一个地方使用const_cast表达得不够清楚。 - David Thornley
5
我们正在谈论一个被保护/私有的变量的实现细节。如果其他方法也可以改变它,那又有什么关系呢?其他方法也可以使用const cast来改变它。不使用mutable没有额外的封装。然而,将其设置为mutable可以告诉类定义的读者有价值的信息。 - frankc
4
@user275455说,保证常量安全性对于类的维护者和类的客户都有帮助。这很重要,因为你不想在一个const方法中意外地改变成员变量。如果没有mutable关键字,编译器会检测到这个问题。你说客户不应该担心protected/private细节(没错),但是然后你又反过来说mutable私有变量的声明对客户有价值(不对)。 - Jon-Eric
2
你可以同样让另一个维护者在声明中看到const,认为其永远不会变化,并依赖于此而导致错误。我并不真的对const cast解决方案有问题,但我认为反对可变解决方案是错误的;每种方法都有优点。在我看来,可变解决方案更有优势,但只是稍微好一些。 - frankc
2
@user275455:非常清晰的解释,谢谢。我会撤回我的踩,将它们留到未来给那些严格错误的答案,并让其他人的赞决定“最佳”。 - Jon-Eric
显示剩余2条评论

26

如果您经常这样做,我建议将James Curran的答案封装到自己的类中:


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

我也喜欢你的回答。实现指针接口可能比引用接口更一致。 - Ken Bloom
这似乎是最通用的解决方案,与C++规范“友好相处”。它在表面下很丑陋,但公共API保持干净。 - Dave Mateer
1
我很惊讶我找不到一个适用于此的Boost库。 - Ken Bloom
1
@Ken Bloom:在我看来,悬挂对象方法的常量性对于MyClass而言是一种实现细节。(好吧,它比这更多,因为MyClass安排成员属于该类型——但我偏离了主题。)如果MyClass关心成员的常量性,则在expensive_object的声明中添加const关键字似乎是正确的方法。 - Dan Breslau
非常有趣的解决方案。我有两个建议。首先,对于T值,请使用boost::optional;其次,我认为您的示例代码是错误的。应该是expensive_object_.get()而不是suspended.get()。 - Martin
显示剩余4条评论

7
使 expensive_object_ 可变。

6

在那个特定的地方使用const_cast来避开常量。

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

在我看来,这比将expensive_object_变为可变对象更好,因为这样你就不会失去其他所有方法中的const安全性。


同意。尽管我会定期在代码库中使用grep搜索const_cast和C样式转换,以确保没有const违规,但在这种情况下,我认为这是可以被辩解的。(虽然我不确定我同意你对可变答案的负一评级...它们都是处理问题的好方法,只是具有不同的后果:令人担忧的代码感觉语法与更多不必要的可变性之间的差异...) - leander
2
我不是那个点踩者,但我认为如果MyClass实例被创建为“const”,这可能会导致未定义的行为。 - Fred Larson
7
除非对象实际上不是const,否则弃用const并修改对象是未定义的行为。从此处使用无法确定对象是否为const。 - Martin York
2
@Jon-Eric。那并没有解决问题。我想问题可能在于,当您将对象声明为const(没有可变成员时),编译器可能会决定将对象放在某个只读内存页面上。请参见http://www.parashift.com/c++-faq-lite/const-correctness.html。 - Ken Bloom
3
@Martin:这会教训我在发表言论之前先去阅读标准文件了 =) 5.2.11.7:“[注意:根据对象的类型,通过const_cast去除const资格限定符后,通过指针、左值或者数据成员指针进行写操作可能导致未定义行为(7.1.5.1)。]”7.1.5.1.4:“除了任何声明了mutable(7.1.1)的类成员可以被修改以外,在其生命周期(3.8)内试图修改const对象都会导致未定义行为。”这可能是为了允许将对象放置在写保护内存中之类的事情吧? - leander
显示剩余2条评论

3

您考虑过使用包装类吗? 您可能可以使用类似智能指针的东西,只提供const版本的 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_;
};

现在是时候去编译这个程序并且看看它是否能够被破解了... =)


他可能会想要查看我们两个版本的代码,以防默认构造函数不符合他的意愿,或者在CreateExpensiveObject返回null是一个有效的返回值的情况下。 - Ken Bloom

0

我在这个主题上稍微玩了一下,并提出了一个备选方案,以防您使用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在执行过程中会重新分配自己。在重新分配后访问任何非静态成员(在使用lambda时捕获)将导致未定义的行为,因此需要额外注意。此外,这种方法有点像黑客技术,因为GetExpensiveObject()是const,但它仍然在第一次调用时修改了成员属性。

在生产代码中,我可能更喜欢像James Curran所描述的那样将成员变量设置为mutable。这样,您类的公共API清楚地说明该成员不被视为对象状态的一部分,因此不会影响constness。

经过更深入的思考,我发现可以使用带有std::launch::deferred参数的std::async与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
我创建了一个名为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>();
};

关于MIT许可证:请参阅https://meta.stackexchange.com/questions/12527/do-i-have-to-worry-about-copyright-issues-for-code-posted-on-stack-overflow - Matthias
@Matthias:谢谢你告诉我!当我在StackOverflow上开始时,他们的默认许可证肯定更加严格。那时,将我的代码放在MIT许可证下是有意义的。我很高兴现在是CC,所以我已经删除了那句话。 - Daniel Wolf

0
提出了一个更加高级的解决方案这里,但它不能处理没有默认构造函数的类型...

-2

你的getter并不是真正的const,因为它确实改变了对象的内容。我认为你想得太多了。


2
这正是 mutable 被发明出来的原因 -- 使您能够声明某些东西为 const,即使您必须修改其内存,即使您知道其接口始终看起来相同。 - Ken Bloom

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接