Функции членов, которые иногда являются константами

У меня есть класс, похожий на следующий:

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? Есть ли лучший подход?

Ответы

Ответ 1

Я предполагаю, что ваша реализация выглядит примерно так:

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

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

    return isValid;
}

У вас действительно есть две разные функции, запихиваемые в одну. Одним из ключевых индикаторов такого рода запутывания является логический аргумент функции..., который пахнет, потому что вызывающий не может сразу определить, следует ли поставить true или false без ссылки на код/​​документы.

Разделите метод:

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

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

И тогда ваши общедоступные методы могут сделать вызовы более подходящими.

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

Ответ 2

Вот подход, который может быть полезен в некоторых случаях. Это может быть излишним для вашей конкретной ситуации.

Функция 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);
        //   .
        //   .
        //   .
    }
};

Использование шаблона обеспечивает максимальную эффективность. Альтернативно, использование базового класса с виртуальными функциями для обработчика может обеспечить меньший размер исполняемого файла.

Если способы, в которых объект может быть недействительным, проще, то с возвратом функции CheckValidity структура, содержащая соответствующую информацию, может быть более простой.

Ответ 3

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

Ниже приведена реализация для игрушечного класса. Он имеет единственный элемент c-массива v с 10 int и для наших целей он действителен только тогда, когда каждый из них равен нулю.

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 вне класса. Чтобы соответствующие функции имели доступ к v и методу fix(), мы сделаем их друзьями. Теперь наш код выглядит следующим образом:

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-объекта он зафиксирует позицию. Для const 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, как и ожидалось. Теперь любое обслуживание этого кода не будет иметь дело с дублированием кода, чего вы, вероятно, намеревались избежать. Я надеюсь, что это было полезно.

Конечно, если вы намерены скрыть данные реализации от конечного пользователя, вы должны перенести их в соответствующее пространство имен деталей. Я оставлю это до вас:)