我发现很难简洁地描述这个问题,所以我附上了演示程序的代码。
一般的想法是我们想要一组Derived类,它们被迫从Base类实现一些抽象的Foo()函数。每个派生的Foo()调用必须接受不同的参数作为输入,但所有参数也应该从BaseInput类派生。
到目前为止,我们看到了两种可能的解决方案,我们都不满意:
从基类中删除Foo()函数,并使用每个Derived类中的正确输入类型重新实现它。但是,这会消除在每个派生类中以相同方式实现它的强制执行。
在接收函数内部进行某种动态转换,以验证收到的类型是否正确。但是,这不会阻止程序员发出错误并传递错误的输入数据类型。我们希望将类型传递给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;
}
答案 0 :(得分:5)
由于您的Derived
类是一个 Base
类,它应该永远不会收紧基本合同前提条件:如果它必须表现得像一个Base
,它应该接受BaseInput
。这被称为Liskov替代原则。
虽然您可以对参数进行运行时检查,但您永远无法实现完全类型安全的方法:当编译器看到DerivedInput
对象时,您可能能够匹配Derived
(静态类型),但它无法知道Base
对象背后的子类型...
要求
DerivedX
应该DerivedXInput
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.
答案 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。