在派生的虚函数中强制执行正确的参数类型

aar*_*rkk 7 c++ oop

我发现很难简洁地描述这个问题,所以我附上了演示程序的代码.

一般的想法是我们想要一组Derived类,它们被迫从Base类实现一些抽象的Foo()函数.每个派生的Foo()调用必须接受不同的参数作为输入,但所有参数也应该从BaseInput类派生.

到目前为止,我们看到了两种可能的解决方案,我们都不满意:

  1. 从基类中删除Foo()函数,并使用每个Derived类中的正确输入类型重新实现它.但是,这消除了在每个派生类中以相同方式实现它的强制执行.

  2. 在接收函数内部进行某种动态转换,以验证接收的类型是否正确.但是,这不会阻止程序员发出错误并传递错误的输入数据类型.我们希望传递给Foo()函数的类型是编译时正确的.

是否存在某种可以强制执行此类行为的模式?这整个想法是否打破了OOP背后的某种基本理念?我们非常希望听到您对我们提出的解决方案之外的可能解决方案的意见.

非常感谢!

#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;
}
Run Code Online (Sandbox Code Playgroud)

xto*_*ofl 5

因为你的Derived是一个 Base类,所以它永远不应该收紧基本契约前提条件:如果它必须表现得像a 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;
   }
};
Run Code Online (Sandbox Code Playgroud)

和框架:

struct FruitPipeLine {
    FruitTree* tree;
    FruitEater* eater;
    void cycle(){
       eater->eat( tree->pick() );
    }
};
Run Code Online (Sandbox Code Playgroud)

现在,这证明了一个太容易破解的设计:设计中没有任何部分将树木与食客对齐:

 FruitPipeLine pipe = { new BananaTree, new LemonEater }; // compiles fine
 pipe.cycle(); // crash, probably.
Run Code Online (Sandbox Code Playgroud)

您可以通过将其设为模板来提高设计的内聚力,并消除虚拟调度的需要:

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() ); }
};
Run Code Online (Sandbox Code Playgroud)

实现实际上是模板特化:

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


...
PipeLine<Banana> pipe; // can't be wrong
pipe.cycle(); // no typechecking needed.
Run Code Online (Sandbox Code Playgroud)