Подтвердить что ты не робот

Как обеспечить, чтобы вызовы виртуальных методов распространялись вплоть до базового класса?

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

Пример сценария

class Container
{
public:
  virtual void PrepareForInsertion(ObjectToInsert* pObject)
  {
    // Nothing to do here
  }
};

class SpecializedContainer : public Container
{
protected:
  virtual void PrepareForInsertion(ObjectToInsert* pObject)
  {
    // Set some property of pObject and pass on.
    Container::PrepareForInsertion(pObject);
  }
};

class MoreSpecializedContainer : public SpecializedContainer
{
protected:
  virtual void PrepareForInsertion(ObjectToInsert* pObject)
  {
    // Oops, forgot to propagate!
  }
};

Мой вопрос: есть хороший способ/шаблон, чтобы гарантировать, что базовая реализация всегда вызывается в конце цепочки вызовов?

Я знаю два метода для этого.

Способ 1

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

class Container
{
public:
  void PrepareForInsertion(ObjectToInsert* pObject)
  {
    m_callChainCorrect = false;
    PrepareForInsertionImpl(pObject);
    assert(m_callChainCorrect);
  }

protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
  {
    m_callChainCorrect = true;
  }

private:
  bool m_callChainCorrect;
};

class SpecializedContainer : public Container
{
protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
  {
    // Do something and pass on
    Container::PrepareForInsertionImpl(pObject);
  }
};

Способ 2

Другой способ сделать это - заменить переменную-член непрозрачным параметром "cookie" и сделать то же самое:

class Container
{
public:
  void PrepareForInsertion(ObjectToInsert* pObject)
  {
    bool callChainCorrect = false;
    PrepareForInsertionImpl(pObject, &callChainCorrect);
    assert(callChainCorrect);
  }

protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject, void* pCookie)
  {
    *reinrepret_cast<bool*>(pCookie) = true;
  }
};

class SpecializedContainer : public Container
{
protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject, void* pCookie)
  {
    // Do something and pass on
    Container::PrepareForInsertionImpl(pObject, pCookie);
  }
};

Этот подход уступает первому, на мой взгляд, но он избегает использования выделенной переменной-члена.

Какие еще существуют возможности?

4b9b3361

Ответ 1

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

Реальный ответ - не делать этого во время выполнения. Это ошибка программиста, а не ошибка времени выполнения.

Сделайте это во время компиляции: используйте конструкцию языка, если язык ее поддерживает, или используйте шаблон, который его применяет (например, Template Method), или сделайте вашу компиляцию зависимой от прохождения тестов и настройте тесты для ее принудительного применения.

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

Ответ 2

То, что вы ищете, это просто шаблон не виртуального интерфейса.

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

"Не виртуальный интерфейс" Google. Подробнее.

Изменить: после поиска "Шаблон метода шаблона" я вижу, что это другое имя для не виртуального интерфейса. Я никогда не слышал, чтобы это упоминалось по имени раньше (я не совсем член карточки в фан-клубе GoF). Лично я предпочитаю имя Non-Virtual Interface, потому что само имя фактически описывает, что такое шаблон.

Изменить снова. Здесь используется способ NVI:

class Container
{
public:
  void PrepareForInsertion(ObjectToInsert* pObject)
  {
    PrepareForInsertionImpl(pObject);

    // If you put some base class implementation code here, then you get
    // the same effect you'd get if the derived class called the base class
    // implementation when it finished.
    //
    // You can also add implementation code in this function before the call
    // to PrepareForInsertionImpl, if you want.
  }

private:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject) = 0;
};

class SpecializedContainer : public Container
{
private:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
  {
    // Do something and return to the base class implementation.
  }
};

Ответ 3

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

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

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

class base {
protected:
    class remember_to_call_base {
        friend base;
        remember_to_call_base() {} 
    };

    virtual remember_to_call_base do_foo()  { 
        /* do common stuff */ 
        return remember_to_call_base(); 
    }

    remember_to_call_base base_impl_not_needed() { 
        // allow opting out from calling base::do_foo (optional)
        return remember_to_call_base();
    }

public:
    void foo() {
        do_foo();
    }
};

class derived : public base  {

    remember_to_call_base do_foo()  { 
        /* do specific stuff */
        return base::do_foo(); 
    }
};

Если вам нужна функция public (non virtual) для возврата значения внутреннего virtual, следует вернуть std::pair< return-type, remember_to_call_base>.


Примечания:

  • remember_to_call_base имеет явный конструктор, объявленный private, поэтому только его friend (в данном случае base) может создать новый экземпляр этого класса.
  • remember_to_call_base не имеет явно определенного конструктора копирования, поэтому компилятор создаст один с public доступностью, которая позволяет его вернуть по значению из реализации base.
  • remember_to_call_base объявлен в разделе protected base, если он был в разделе private derived, не сможет вообще ссылаться на него.

Ответ 4

Совершенно другой подход заключается в регистрации функторов. Производные классы регистрируют некоторую функцию (или функцию-член) с базовым классом, а в конструкторе производного класса. Когда действительная функция вызывается клиентом, это функция базового класса, которая затем выполняет итерацию через зарегистрированные функции. Это масштабируется до многих уровней наследования, каждый производный класс должен быть связан только с его собственной функцией.

Ответ 5

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

Ответ 6

Один из способов - не использовать виртуальные методы вообще, а вместо этого разрешать пользователю регистрировать обратные вызовы и вызывать их перед выполнением работы prepareForInsertion. Таким образом становится невозможным совершить эту ошибку, так как это базовый класс, который гарантирует, что и обратные вызовы, и нормальная обработка. Вы можете получить много обратных вызовов, если вы хотите, чтобы это поведение выполняло множество функций. Если вы действительно используете такой шаблон настолько, вы можете захотеть изучить такие инструменты, как AspectJ (или что-то вроде эквивалента С#), которые могут автоматизировать такие вещи.

Ответ 7

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

class Container
{
public:
  void PrepareForInsertion(ObjectToInsert* pObject)
  {
    PrepareForInsertionImpl(pObject);
    doBasePreparing(pObject);
  }

protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
  {
    // nothing to do
  }

private:
  void doBasePreparing(ObjectToInsert* pObject)
  {
    // put here your code from Container::PrepareForInsertionImpl
  }
};