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

Переопределение общедоступных виртуальных функций с частными функциями в С++

Есть ли причина, по которой разрешения на переопределенную виртуальную функцию С++ отличаются от базового класса? Есть ли опасность при этом?

Например:

class base {
    public:
        virtual int foo(double) = 0;
}

class child : public base {
    private:
        virtual int foo(double);
}

С++ faq говорит, что это плохая идея, но не говорит, почему.

Я видел эту идиому в некотором коде, и я считаю, что автор пытался сделать окончательный класс на основе предположения о невозможности переопределить частную функцию-член. Однако В этой статье показан пример переопределения частных функций. Конечно, другая часть С++ faq рекомендует не делать этого.

Мои конкретные вопросы:

  • Есть ли какая-либо техническая проблема с использованием другого разрешения для виртуальных методов в производных классах по сравнению с базовым классом?

  • Есть ли законная причина для этого?

4b9b3361

Ответ 1

Проблема заключается в том, что методы класса Base - это способ декларирования интерфейса. Это, по существу, говорит: "Это то, что вы можете сделать с объектами этого класса".

Когда в классе Derived вы делаете то, что Base объявляло публичным, вы забираете что-то. Теперь, несмотря на то, что объект Derived "is-a" базовый объект, то, что вы должны сделать с объектом базового класса, вы не можете сделать с объектом класса Derived, нарушая Лисков Замена Prinicple

Будет ли это причиной "технической" проблемы в вашей программе? Возможно, нет. Но это, вероятно, означает, что объект ваших классов не будет вести себя так, как ваши пользователи ожидают, что они будут себя вести.

Если вы находите себя в ситуации, когда это то, что вы хотите (за исключением случая устаревшего метода, упомянутого в другом ответе), скорее всего, у вас есть модель наследования, где наследование не действительно моделирует "is-a," (например, пример Скотта Майерса на квадрат, наследующий от Rectangle, но вы не можете изменить ширину квадрата, не зависящую от его высоты, как вы можете для прямоугольника), и вам может потребоваться пересмотреть отношения между классами.

Ответ 2

Вы получаете удивительный результат: если у вас есть ребенок, вы не можете вызвать foo, но вы можете отбросить его на базу, а затем вызвать foo.

child *c = new child();
c->foo; // compile error (can't access private member)
static_cast<base *>(c)->foo(); // this is fine, but still calls the implementation in child

Я полагаю, вы могли бы придумать пример, где вы не хотите, чтобы функция была открыта, за исключением случаев, когда вы рассматриваете ее как экземпляр базового класса. Но сам факт, что эта ситуация всплывает, подсказывает, что плохой дизайн OO находится где-то вдоль линии, которая, вероятно, должна быть реорганизована.

Ответ 3

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

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

Ответ 4

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

Ответ 5

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

Ответ 6

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

Ответ 7

Хорошим вариантом использования для частного наследования является интерфейс события Listener/Observer.

Пример кода для частного объекта:

class AnimatableListener {
  public:
    virtual void Animate(float delta_time);
};

class BouncyBall : public AnimatableListener {
  public:
    void TossUp() {}
  private:
    void Animate(float delta_time) override { }
};

Некоторые пользователи объекта хотят родительскую функциональность, а некоторые хотят дочерние функции:

class AnimationList {
   public:
     void AnimateAll() {
       for (auto & animatable : animatables) {
         // Uses the parent functionality.
         animatable->Animate();
       }
     }
   private:
     vector<AnimatableListener*> animatables;
};

class Seal {
  public:
    void Dance() {
      // Uses unique functionality.
      ball->TossUp();
    }
  private:
    BouncyBall* ball;
};

Таким образом, AnimationList может содержать ссылку на родителей и использовать родительские функции. Пока Seal содержит ссылку на дочерний элемент и использует уникальную дочернюю функциональность и игнорирует родительский. В этом примере Seal не следует вызывать Animate. Теперь, как упоминалось выше, Animate может быть вызвано путем отбрасывания на базовый объект, но это сложнее и вообще не должно выполняться.