Обеспечение правильных типов параметров в производной виртуальной функции - PullRequest
7 голосов
/ 24 августа 2011

Мне трудно описать эту проблему очень кратко, поэтому я прикрепил код для демонстрационной программы.

Общая идея заключается в том, что нам нужен набор производных классов, которые вынуждены реализовывать некоторую абстрактную функцию Foo () из базового класса. Каждый из производных вызовов Foo () должен принимать разные параметры в качестве входных данных, но все параметры также должны быть получены из класса BaseInput.

Пока мы видим два возможных решения, и мы не очень довольны:

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

  2. Выполните какое-либо динамическое приведение внутри получающей функции, чтобы убедиться, что полученный тип правильный. Однако это не мешает программисту сделать ошибку и передать неверный тип входных данных. Мы бы хотели, чтобы тип, передаваемый в функцию Foo (), был корректным во время компиляции.

Есть ли какая-то модель, которая могла бы навязать такое поведение? Разрушает ли вся эта идея фундаментальную идею, лежащую в основе ООП? Мы бы очень хотели услышать ваше мнение о возможных решениях помимо того, что мы придумали.

Большое спасибо!

#include <iostream>

// these inputs will be sent to our Foo function below
class BaseInput {};
class Derived1Input : public BaseInput { public: int   d1Custom; };
class Derived2Input : public BaseInput { public: float d2Custom; };

class Base
{
public:
    virtual void Foo(BaseInput& i) = 0;
};

class Derived1 : public Base
{
public:
    // we don't know what type the input is -- do we have to try to cast to what we want
    // and see if it works?
    virtual void Foo(BaseInput& i) { std::cout << "I don't want to cast this..." << std::endl; }

    // prefer something like this, but then it's not overriding the Base implementation
    //virtual void Foo(Derived1Input& i) { std::cout << "Derived1 did something with Derived1Input..." << std::endl; }
};

class Derived2 : public Base
{
public:
    // we don't know what type the input is -- do we have to try to cast to what we want
    // and see if it works?
    virtual void Foo(BaseInput& i) { std::cout << "I don't want to cast this..." << std::endl; }

    // prefer something like this, but then it's not overriding the Base implementation
    //virtual void Foo(Derived2Input& i) { std::cout << "Derived2 did something with Derived2Input..." << std::endl; }
};

int main()
{
    Derived1 d1; Derived1Input d1i;
    Derived2 d2; Derived2Input d2i;

    // set up some dummy data
    d1i.d1Custom = 1;
    d2i.d2Custom = 1.f;

    d1.Foo(d2i);    // this compiles, but is a mistake! how can we avoid this?
                    // Derived1::Foo() should only accept Derived1Input, but then
                    // we can't declare Foo() in the Base class.

    return 0;
}

Ответы [ 6 ]

5 голосов
/ 25 августа 2011

Так как ваш Derived класс является классом Base, он никогда не должен ужесточать предварительные условия базового контракта : если он должен вести себя как Base, он следует принять BaseInput все в порядке. Это известно как принцип замещения Лискова.

Несмотря на то, что вы можете выполнять проверку аргумента во время выполнения, вы никогда не сможете достичь полностью безопасного для типов способа сделать это: ваш компилятор может соответствовать DerivedInput, когда он видит объект Derived (статический тип) , но он не может знать, какой подтип будет за Base объектом ...

Требования

  1. DerivedX должен занять DerivedXInput
  2. DerivedX::Foo должен быть равен интерфейсу DerivedY::Foo

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

Это, на мой взгляд, проблема.

Эта проблема возникла и у меня, когда я писал тесно связанные классы, которые обрабатываются в не зависящей от типа среде:

class Fruit {};
class FruitTree { 
   virtual Fruit* pick() = 0;
};
class FruitEater {
   virtual void eat( Fruit* ) = 0;
};

class Banana : public Fruit {};
class BananaTree {
   virtual Banana* pick() { return new Banana; }
};
class BananaEater : public FruitEater {
   void eat( Fruit* f ){
      assert( dynamic_cast<Banana*>(f)!=0 );
      delete f;
   }
};

и рамки:

struct FruitPipeLine {
    FruitTree* tree;
    FruitEater* eater;
    void cycle(){
       eater->eat( tree->pick() );
    }
};

Теперь это доказывает, что дизайн слишком легко сломать: в дизайне нет части, которая бы выравнивала деревья с едоками:

 FruitPipeLine pipe = { new BananaTree, new LemonEater }; // compiles fine
 pipe.cycle(); // crash, probably.

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

template<class F> class Tree {
   F* pick(); // no implementation
};
template<class F> class Eater {
   void eat( F* f ){ delete f; } // default implementation is possible
};
template<class F> PipeLine {
   Tree<F> tree;
   Eater<F> eater;
   void cycle(){ eater.eat( tree.pick() ); }
};

Реализации действительно являются шаблонными специализациями:

template<> class Tree<Banana> {
   Banana* pick(){ return new Banana; }
};


...
PipeLine<Banana> pipe; // can't be wrong
pipe.cycle(); // no typechecking needed.
4 голосов
/ 25 августа 2011

Возможно, вы сможете использовать вариацию любопытно повторяющегося шаблона .

class Base {
public:
    // Stuff that don't depend on the input type.
};

template <typename Input>
class Middle : public Base {
public:
    virtual void Foo(Input &i) = 0; 
};    

class Derived1 : public Middle<Derived1Input> {
public:
    virtual void Foo(Derived1Input &i) { ... }
};

class Derived2 : public Middle<Derived2Input> {
public:
    virtual void Foo(Derived2Input &i) { ... }
};
2 голосов
/ 25 августа 2011

Это не проверено, просто выстрел из бедра!

Если вы не возражаете против динамического броска, как насчет этого:

Class BaseInput;

class Base
{
public:
  void foo(BaseInput & x) { foo_dispatch(x); };
private:
  virtual void foo_dispatch(BaseInput &) = 0;
};

template <typename TInput = BaseInput> // default value to enforce nothing
class FooDistpatch : public Base
{
  virtual void foo_dispatch(BaseInput & x)
  {
    foo_impl(dynamic_cast<TInput &>(x));
  }
  virtual void foo_impl(TInput &) = 0;
};

class Derived1 : public FooDispatch<Der1Input>
{
  virtual void foo_impl(Der1Input & x) { /* your implementation here */ }
};

Таким образом, вы построилидинамическая проверка типов в промежуточном классе, и ваши клиенты всегда получают только из FooDispatch<DerivedInput>.

1 голос
/ 25 августа 2011

То, о чем вы говорите, - это ковариантные типы аргументов, и это довольно редкая возможность в языке, поскольку это нарушает ваш контракт: вы обещали принять объект base_input, потому что вы наследуете от base, но вы хотитекомпилятор отклоняет все, кроме небольшого подмножества base_input s ...

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

В любом случае, C ++ также не предлагает контравариации в типах аргументов, только ковариациюв типе возврата.

0 голосов
/ 30 декабря 2016

Это, конечно, ограничено, но вы можете использовать / моделировать ковариацию в параметрах конструкторов.

0 голосов
/ 25 августа 2011

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

Я уверен, что приведение может быть использовано безболезненно, хотя, возможно, путем предоставления базовому классу члена типа Enum, который уникально устанавливается конструктором каждого возможного потомка, который мог бы его наследовать,Foo () может затем проверить этот «тип» и определить, к какому типу он относится, прежде чем что-либо делать, и выдать утверждение, если он удивлен чем-то неожиданным.Это не время компиляции, но это самый близкий компромисс, который я могу придумать, но при этом все еще есть преимущества, требующие определения Foo ().

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...