有时是 const 的成员函数

Member functions that are sometimes const

我的 class 设计类似于以下内容:

class MyClass {
public:
    bool IsValid() const;
    void MakeValid();
private:
    bool CheckValidity(bool fix);
};

bool MyClass::IsValid() const {
    // Check validity, but don't fix any problems found.  Doesn't work.
    return CheckValidity(false);
}

void MyClass::MakeValid() {
    // Check validity and fix problems found.
    CheckValidity(true);
}

IsValid 应该是 const,因为它没有进行更改。 MakeValid 应该是非常量,因为它确实进行了更改。它们共享相同的实现,CheckValidity,但由于 CheckValidity 可能会或可能不会进行更改,因此无法标记为 const

处理此问题的最佳方法是什么?最简单的方法是只使用 const_cast,但是丢弃 const 感觉有点脏:

bool MyClass::IsValid() const {
    // Check validity, but don't fix any problems found.
    return const_cast<MyClass*>(this)->CheckValidity(false);
}

这是对 const_cast 的合法使用吗?有没有更好的方法?

我假设您的实现与此类似:

bool CheckValidity(bool fix)
{
    // Actually check validity.
    bool isValid = ...;

    if (!isValid && fix)
    {
        // Attempt to fix validity (and update isValid).
        isValid = ...;
    }

    return isValid;
}

您确实将两个不同的功能合而为一。这种纠缠的关键指标之一是函数的布尔参数......这有味道,因为调用者无法在不引用 code/docs.

的情况下立即辨别是输入 true 还是 false

拆分方法:

bool CheckValidity() const
{
    // Actually check validity.
    bool isValid = ...;
    return isValid;
}

void FixValidity()
{
    // Attempt to fix validity.
    // ...
}

然后您的 public 方法可以更恰当地进行调用。

bool IsValid() const
{
    // No problem: const method calling const method
    return CheckValidity();
}

void MakeValid()
{
    if (!CheckValidity())  // No problem: non-const calling const
    {
         FixValidity();    // No problem: non-const calling non-const
    }
}

这是一种在某些情况下可能有用的方法。对于您的特定情况,这可能有点矫枉过正。

您的 CheckValidity 函数可以传递一个处理程序对象。 CheckValidity 函数会发现无效的内容,并调用处理程序对象的适当方法。对于不同类型的有效性违规,您可以有许多不同的方法,并且可以向这些方法传递足够的信息,以便在必要时解决问题。要实现 IsValid,您只需要传递一个处理程序,该处理程序设置一个标志以指示存在问题。要实施 MakeValid,您可以传递一个实际解决问题的处理程序。 const 问题通过让修复处理程序保留对对象的非常量引用来解决。

这是一个例子:

class MyClass {
public:
    bool IsValid() const 
    { 
        bool flag = false;
        CheckValidity(FlagProblems{flag});
        return flag;
    }

    void MakeValid() 
    {
        CheckValidity(FixProblems{*this});
    }

private:
    struct FlagProblems {
        bool& flag;

        void handleType1(arg1,arg2)      const { flag = true; }
        void handleType2(arg1,arg2,arg3) const { flag = true; }
        .
        .
        .
    };

    struct FixProblems {
        MyClass& object;
        void handleType1(arg1,arg2)      const { ... }
        void handleType2(arg1,arg2,arg3) const { ... }
        .
        .
        .
    };

    template <typename Handler>
    bool CheckValidity(const Handler &handler) const
    {
        // for each possible problem:
        //   if it is a type-1 problem:
        //     handler.handleType1(arg1,arg2);
        //   if it is a type-2 problem:
        //     handler.handleType2(arg1,arg2,arg3);
        //   .
        //   .
        //   .
    }
};

使用模板可以实现最高效率。或者,为处理程序使用带有虚函数的基 class 可能会提供更小的可执行文件大小。

如果使对象无效的方法更简单,那么让 CheckValidity return 一个包含相关信息的结构可能会更直接。

您可以使用模板特化来分离仅对非常量对象有用的部分。

以下是玩具 class 的实现。它有一个包含 10 个整数的 c 数组成员 v,并且为了我们的目的,它仅在每个成员都为零时才有效。

class ten_zeroes {
  int v[10];
  void fix(int pos) {v[pos] = 0;}

  public:
  ten_zeroes() { // construct as invalid object
    for (int i=0;i<10;i++) {
      v[i] = i;
    }
  }
};

看到我已经创建了一个修复无效位置的函数成员,以及一个将其初始化为无效对象的漂亮构造函数(不要那样做 :D)

由于我们要使用模板,我们需要将 check/fix 循环的实现移到 class 之外。为了让相关函数能够访问vfix()方法,我们将它们加为好友。我们的代码现在看起来像:

class ten_zeroes {
  int v[10];
  void fix(int pos) {v[pos] = 0;}

  public:
  ten_zeroes() { // construct as invalid object
    for (int i=0;i<10;i++) {
      v[i] = i;
    }
  }

  template<typename T>
  friend void fix(T& obj, int pos);

  template<typename T>
  friend bool check(T& obj);
};

check()的实现很简单:

// Check and maybe fix object
template<typename T>
bool check(T& obj){
  bool result = true;
  for(int i=0;i<10;i++) {
    if (obj.v[i]) {
      result = false;
      fix(obj, i);
    }
  }
  return result;
}

现在是棘手的部分。我们希望 fix() 函数根据常量改变行为。为此,我们需要专门化模板。对于非常量对象,它将固定位置。对于 const 一个,它什么都不做:

// For a regular object, fix the position
template<typename T>
void fix(T& obj, int pos) { obj.fix(pos);}

// For a const object, do nothing
template<typename T>
void fix(const T& obj, int pos) {}

最后,我们编写 is_valid()make_valid() 方法,这里我们有完整的实现:

#include <iostream>

class ten_zeroes {
  int v[10];
  void fix(int pos) {v[pos] = 0;}

  public:
  ten_zeroes() { // construct as invalid object
    for (int i=0;i<10;i++) {
      v[i] = i;
    }
  }

  bool is_valid() const {return check(*this);} // since this is const, it will run check with a const ten_zeroes object
  void make_valid() { check(*this);} // since this is non-const , it run check with a non-const ten_zeroes object

  template<typename T>
  friend void fix(T& obj, int pos);

  template<typename T>
  friend bool check(T& obj);
};

// For a regular object, fix the position
template<typename T>
void fix(T& obj, int pos) { obj.fix(pos);}

// For a const object, do nothing
template<typename T>
void fix(const T& obj, int pos) {}

// Check and maybe fix object
template<typename T>
bool check(T& obj){
  bool result = true;
  for(int i=0;i<10;i++) {
    if (obj.v[i]) {
      result = false;
      fix(obj, i);
    }
  }
  return result;
}

int main(){
  ten_zeroes a;
  std::cout << a.is_valid() << a.is_valid(); // twice to make sure the first one didn't make any changes
  a.make_valid(); // fix the object
  std::cout << a.is_valid() << std::endl; // check again
}

希望您不要介意那里的 main() 功能。它将测试我们的小玩具,并按预期输出 001。现在,对此代码的任何维护都不必处理代码重复,而这可能是您想要避免的。希望对您有所帮助。

当然,如果您打算对最终用户隐藏这些实现细节,您应该将它们移动到适当的细节命名空间。我会把它留给你:)