65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ 中的 mutable 关键字和数据缓存

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (3投票s)

2012 年 10 月 29 日

CPOL

3分钟阅读

viewsIcon

19078

一个表示有效/无效数据的类以及 mutable 关键字的使用

引言

本文将介绍 C++ 关键字 mutable,并深入探讨如何使用 mutable 进行数据缓存。 为了说明这一点,我将举一个来自 Python 的例子。 如果我有一个类的属性计算成本很高,我想缓存它,并且只在必要时重新计算。  

#==============================================================================
class DataClass(object):
    
    def __init__(self):
        self.__data = None
    @property
    def data(self):
        if (self.__data is None):
            # work out self.__data
        return self.__data

在前面的例子中,我使用了 Python 的动态类型来将数据和一个布尔值本质上结合在一起。 因此,如果它是 None,它将被计算,否则将返回缓存的值。 

已验证的类 

现在我的目标是在 C++ 中复制它。 

C++ 是一种静态类型 语言,因此我们不能使用完全相同的方法。 所以我所做的是创建一个类来封装这个想法。 由于数据的类型与该类无关,所以我将其设为一个模板类。 这是我称为 Validated 的类的公共 API。 

//=============================================================================
template <typename T>
class Validated {
public:
  Validated();
  // invalid constructor
  Validated(const T& data);
  // valid constructor
  bool valid() const;
  // is this valid or not?
  void invalidate();
  // makes the class invalid again, so call in a function which will change the
  // cached value.
  void set_data(const T& data);
  // set the data, makes the instance valid.
  operator T();
  operator T() const;
  // implicit conversions to the template type.

这决定了类的行为,因此如果它具有数据值,则它是有效的,如果未设置数据值,则它是无效的。 它还(异常地)包含一个到数据类型的隐式类型转换器。 这是为了它可以自然地被视为它正在验证的数据。 这通常使用显式函数完成,例如标准字符串上的 c_str() 函数(另请参见 More Effective C++ 中的第 5 项),但我认为 Validated 类只是数据的包装器,因此它应该本地处理其操作。 

这背后的代码非常简单,可以在这里看到。 

这种方法也可以通过从 Validated 基类私有继承(实现继承)来实现。 我认为这是一个非常通用的概念,因此应该是一个模板,这也意味着它可以与现有类和内置类一起使用。 

使用 Validated  

 现在开始使用这个类和关键字 mutable。 我的例子将是一个三角形并计算它的面积。 因此,这是我的 Triangle 类的 API,以及一个 Point 结构来保存坐标。 

//=============================================================================
struct Point {
  Point(double new_x, double new_y) {
    x = new_x;
    y = new_y;
  }
  double x;
  double y;
};
//=============================================================================
class Triangle {
public:
  Triangle(const Point& point_1, const Point& point_2, const Point& point_3);
  // Constructor, points should be entered anti-clockwise.
  void set_first_point(const Point& point);
  // Sets the first point
  double area() const;
  // Returns the area

 

这是一个非常简单的类,但它的实现非常有趣。 以下是 Triangle 的私有成员。

private:
  Point m_point_1;
  Point m_point_2;
  Point m_point_3;
  
  mutable Validated<double> m_area;
  // this is mutable as it is used as a cache in area() which is a logically
  // const method

 

这就是这篇文章的重点所在,mutable Validated double。 这是 area() 函数的一个可能的实现。

//=============================================================================
double Triangle::area() const
//
//D Returns the area
//
{
  // work out the area using 1/2 ab sin(C)
    
  // a is the length between point 1 and point 2
  double a = sqrt(
    (m_point_2.x - m_point_1.x) * (m_point_2.x - m_point_1.x) +
    (m_point_2.y - m_point_1.y) * (m_point_2.y - m_point_1.y)
  );
  // b is the length between point 2 and point 3
  double b = sqrt(
    (m_point_3.x - m_point_2.x) * (m_point_3.x - m_point_2.x) +
    (m_point_3.y - m_point_2.y) * (m_point_3.y - m_point_2.y)
  );
  // c is the length between point 3 and point 1
  double c = sqrt(
    (m_point_1.x - m_point_3.x) * (m_point_1.x - m_point_3.x) +
    (m_point_1.y - m_point_3.y) * (m_point_1.y - m_point_3.y)
  );
  // use the cosine rule cos(C) = (a^2 + b^2 - c^2) / 2ab
  double cos_C = (a*a + b*b - c*c) / (2 * a * b);
  return 0.5 * a * b * sin(acos(cos_C);
} 

 

这不是一个计算成本很高的函数,但假设我们经常调用它,因此我们决定将结果缓存在成员变量 m_area 中。 我们有两个选择,因为我们将设置一个成员变量,我们可以更改该方法使其不是 const。 这工作正常,但计算面积在逻辑上不会以任何方式改变三角形,因此我们称它为逻辑 const。 您正在缓存面积的事实是一个实现细节,不应反映在类的公共 API 中。 所以这就是为什么使用关键字 mutable。 这意味着您可以在 const 方法中更改该变量。 这就是我将 m_area 变量声明为 mutable 的原因。 将其与 Validated 类结合起来,该函数变为如下所示。

//=============================================================================
double Triangle::area() const
//
//D Returns the area, first time it works it out it caches the result.
//
{
  if (!m_area.valid()) {
    // work out the area using 1/2 ab sin(C)
    
    // a is the length between point 1 and point 2
    double a = sqrt(
      (m_point_2.x - m_point_1.x) * (m_point_2.x - m_point_1.x) +
      (m_point_2.y - m_point_1.y) * (m_point_2.y - m_point_1.y)
    );
    // b is the length between point 2 and point 3
    double b = sqrt(
      (m_point_3.x - m_point_2.x) * (m_point_3.x - m_point_2.x) +
      (m_point_3.y - m_point_2.y) * (m_point_3.y - m_point_2.y)
    );
    // c is the length between point 3 and point 1
    double c = sqrt(
      (m_point_1.x - m_point_3.x) * (m_point_1.x - m_point_3.x) +
      (m_point_1.y - m_point_3.y) * (m_point_1.y - m_point_3.y)
    );
    // use the cosine rule cos(C) = (a^2 + b^2 - c^2) / 2ab
    double cos_C = (a*a + b*b - c*c) / (2 * a * b);
    m_area.set_data(0.5 * a * b * sin(acos(cos_C)));
  }
  return m_area;
} 

 

因此,Validated 类包装器用于缓存值,并且该成员是 mutable 的,以便该方法可以是 const 的。

BitBucket 存储库 中,有一个可执行文件显示正在使用缓存的值。 它还显示了在 Triangle 类的非 const 方法中使用的 Validated::invalidate() 方法。 

使用代码

附加的 .zip 是可以使用 make 编译的项目文件夹。 该代码也可以从 https://bitbucket.org/davidcorne/mutable 下载。  

历史 

- 2012年10月26日 编写。

- 2012年10月26日 附加 .zip 项目文件。 

© . All rights reserved.