Каким должен быть прототип моей виртуальной функции?

#c #oop #function-prototypes

#c #ооп #функция-прототипы

Вопрос:

Допустим, у меня есть абстрактный базовый класс Base с виртуальной функцией doSomething()

Существует два производных класса, один из которых не принимает никаких параметров, в doSomething() то время как другой принимает структуру и целое число в качестве параметра.

Функция в другом классе (SomeClass) вызывается doSomething() с использованием Base* переменной. Ему также необходимо передать параметры, о которых я упоминал DerivedTwo .

Как мне выбрать прототип, не используя if-else для проверки класса объекта во время выполнения?

Спасибо.

     class Base {
      public:
      void virtual doSomething(); 
    }

    class DerivedOne : Base {
      public:
      void doSomething(int a,struct b);
    }

    class DerivedTwo : Base {
      public:
      void doSomething();
    }
  

Комментарии:

1. Можете ли вы быть немного более конкретным, чем Base , DerivedOne и DerivedTwo ? Способ, которым я бы решил это, — просто заставить Base::doSomething() принять a struct и an int . Но если прототипы отличаются, я склонен думать, что есть фундаментальная проблема с дизайном. if else операторы / почти всегда являются неправильным ответом на ситуацию такого типа.

Ответ №1:

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

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

Существует много разных способов более правильного решения проблемы, но трудно что-то посоветовать на основе этого искусственно сконструированного примера.

Комментарии:

1. виртуальный метод следует использовать в случае перегрузки, а не в случае чрезмерного скрытия, верно?

Ответ №2:

Одним из способов сделать это было бы:

 class Base {
public:
    Base();
    virtual ~Base();
    virtual void doSomething();
};

class DerivedOne : public Base {
public:
    DerivedOne();
    void doSomethingElse(int a,struct b);
};

class DerivedTwo : public Base {
public:
    DerivedTwo();
    virtual void doSomething();
};
  

Затем вы могли бы использовать dynamic_cast для определения типа во время выполнения, поскольку у вас, похоже, где-то есть условное выражение типа SomeClass . Методы не равны и принципиально различны. Кроме того, DerivedOne::doSomething скрыл Base::doSomething бы .

Обновить

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

 class Base {
public:
    Base();
    virtual ~Base();
    virtual void doSomething();
};

class DerivedOne : public Base {
public:
    DerivedOne();
    // ...
    virtual void doSomething(); // << no parameters required.
                                // they have moved to member data:
private:
    int a;
    b another;
};

class DerivedTwo : public Base {
public:
    DerivedTwo();
    virtual void doSomething();
};
  

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

Комментарии:

1. Введите условный код в базовый класс, чтобы проверить, к какому производному классу относится экземпляр, — это довольно серьезный недостаток дизайна.

2. @DavidHeffernan Я согласен с вами, но здесь это не так; в OP указано условие типа, которое встречается в реализации SomeClass .

3. Это примерно то же самое, поскольку целью вызова функции является Base * . В любом случае, я просто хотел отметить, что, хотя это общепринятый ответ, исходный вопрос и этот прямой ответ имеют привкус плохого дизайна.

4. Дэвид здесь прав, независимо от того, находится ли он в Base коде вызывающего или в коде вызывающего, факт в том, что если DerivedOne должен быть известен тип для вызова другого метода / перегрузки, это явно нарушает принцип подстановки Лискова. Важно отметить, что интерфейс — это не только имя методов, но также сигнатура и семантика . Однажды мне привели пример: и камера, и пистолет стреляют, но остерегайтесь того, что у вас в руках, прежде чем делать автопортрет , и в этом конкретном случае интерфейсы более разнородны: shoot(Pictureamp;) , shoot(Bulletamp;) .

5. @DavidHeffernan Я тоже не фанат дизайна. Но эта ситуация возникла из-за того, что мы портируем наш код нескольким поставщикам. Каждый предоставляет свои собственные прототипы для своих API. Таким образом, полный дизайн не в руках моей команды

Ответ №3:

Если вам нужно передать эти переменные для обоих производных типов, просто объявите их одинаковыми везде, вот так:

 class Base {
  public:
  void virtual doSomething(int a,struct b); 
}

class DerivedOne : Base {
  public:
  void doSomething(int a,struct b);
}

class DerivedTwo : Base {
  public:
  void doSomething(int a,struct b);
}
  

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

Комментарии:

1. Спасибо. Является ли хорошей практикой не передавать никаких параметров в doSomething() и вместо этого предоставлять для них геттеры. Они могут быть использованы, если того требует производный класс.

2. @Pramod: я не могу комментировать, является ли отказ от передачи параметров хорошей или плохой практикой на основе этого поверхностного примера. Это зависит от вашей реальной проблемы.

Ответ №4:

Можем ли мы решить эту проблему следующим образом? :

 class Base {
  public:
  void virtual doSomething();
  void virtual doSomething(int a,struct b); 
}

class DerivedOne : Base {
  public:
  void doSomething(int a,struct b);
}

class DerivedTwo : Base {
  public:
  void doSomething();
}
  

Ответ №5:

в вашем примере вы можете использовать RTTI вместо if /else, поэтому вы создаете экземпляр DerivedOne и DerivedTwo, затем вы приводите базовый указатель на оба (DerivedOne и DerivedTwo), так что в результате один из них равен NULL. затем вы проверяете, не является ли желаемый производный объект NULL, и если это так, вы вызываете функцию:

 DerivedOne *pDrvOne=dynamic_cast<DerivedOne*>(pBase);
DerivedTwo *pDrvTwo=dynamic_cast<DerivedTwo*>(pBase);
then: if(pDrvOne)pDrvOne->deSo,ething(a,b); if(pDrvTwo) pDrvTwo->doSomething();
  

* Помните, что использование RTTI подорвет принципы полиморфизма.
в вашем примере вы не перегрузили doSomething() в подклассе DerivedOne: «перегрузить функцию — значит использовать ту же сигнатуру (имя функции и то же количество и тип параметров)». но вы просто перегружаете его в DerivedTwo. таким образом, рассмотрим этот пример:

 Base *pBs;
pBs=new DerivedOne; pBs->doSomething(a,b); the result a call to base doSomthing!!!
  

* Решение состоит в том, чтобы попытаться понять проблему, которую собирается решить ваш класс.

Ответ №6:

Аналогично примеру Тараса.. просто добавьте «значения по умолчанию» к прототипу Base и DerivedTwo в примере Тараса. Таким образом, остальной код, использующий базовый класс или класс DerivedTwo, не должен будет меняться.