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

Enforcing correct parameter types in derived virtual function

本文关键字:参数 类型 派生 函数      更新时间:2023-10-16

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

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

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

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

  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;
}

因为你的Derived是一个 Base类,它应该永远不会收紧基本契约先决条件:如果它必须像Base一样,它应该接受BaseInput。这就是所谓的利斯科夫替代原理。

虽然你可以在运行时检查你的参数,但你永远无法实现一个完全类型安全的方法:当你的编译器看到一个Derived对象(静态类型)时,它可能能够匹配DerivedInput,但它不能知道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.

您可以使用奇怪的反复出现的模板模式的变体。

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) { ... }
};

这是未经测试的,只是从臀部射击!

如果你不介意动态强制转换,这样如何:

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>派生。

您所谈论的是协变参数类型,这在语言中是相当不常见的特性,因为它打破了您的契约:您承诺接受base_input对象,因为您继承了base,但您希望编译器拒绝除了base_input s的一小部分…

对于编程语言来说,提供相反的反变参数类型更为常见,因为派生类型不仅接受契约必须接受的所有内容,而且还接受其他类型。

无论如何,c++也不提供参数类型的逆变性,只提供返回类型的协方差。

c++有很多黑暗的区域,所以很难说任何具体的事情是可以撤销的,但是从黑暗的区域我知道,没有cast,这是不可能做到的。在基类中指定的虚函数要求参数类型在所有子类中保持相同。

我确信cast可以以一种不痛苦的方式使用,也许通过给基类一个Enum"类型"成员,该成员由每个可能继承它的可能的子类的构造函数唯一设置。然后,Foo()可以在做任何事情之前检查该"类型"并确定它是哪种类型,并在遇到意外情况时抛出断言。这不是编译时间,但它是我能想到的最接近的折衷方案,同时仍然具有要求定义Foo()的好处。

这当然是有限制的,但是您可以在构造函数参数中使用/模拟一致性。