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

时间:2011-08-24 19:46:54

标签: c++ oop

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

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

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

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

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

  3. 是否存在可以强制执行此类行为的某种模式?这整个想法是否打破了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;
    }
    

6 个答案:

答案 0 :(得分:5)

由于您的Derived是一个 Base类,它应该永远不会收紧基本合同前提条件:如果它必须表现得像一个Base,它应该接受BaseInput。这被称为Liskov替代原则。

虽然您可以对参数进行运行时检查,但您永远无法实现完全类型安全的方法:当编译器看到DerivedInput对象时,您可能能够匹配Derived (静态类型),但它无法知道Base对象背后的子类型...

要求

  1. DerivedX应该DerivedXInput
  2. DerivedX::Foo应该是接口等于DerivedY::Foo
  3. 矛盾: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.
    

答案 1 :(得分:4)

您可以使用curiously recurring template pattern的变体。

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 :(得分:2)

这是未经测试的,只是从臀部拍摄的!

如果你不介意动态演员,那怎么样:

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

答案 3 :(得分:1)

你所谈论的是协变参数类型,这在语言中是一个非常罕见的特性,因为它违反了你的合同:你承诺接受一个base_input对象,因为你继承自base ,但是你希望编译器拒绝除了base_input的一小部分之外的所有内容......

编程语言更常见的是提供相反的: contra-variant 参数类型,因为派生类型不仅会接受合约必须接受的所有内容,而且还会接受其他类型。

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

答案 4 :(得分:0)

C ++有很多黑暗区域,所以很难说任何特定的东西都是可以撤销的,但是从我知道的黑暗区域开始,没有演员,这是不可能的。基类中指定的虚函数要求参数类型在所有子类中保持相同。

我确信可以以非痛苦的方式使用强制转换,可能是通过为基类提供一个Enum'type'成员,该成员由可能继承它的每个可能子进程的构造函数唯一设置。然后,Foo()可以检查'type'并确定在执行任何操作之前它是什么类型,如果它被意外的事情感到惊讶,则抛出一个断言。这不是编译时间,但它是我能想到的最接近的折衷方案,同时仍然具有要求定义Foo()的好处。

答案 5 :(得分:0)

它肯定受到限制,但你可以在构造函数参数中使用/模拟covia。