在派生虚函数中强制正确的参数类型
Enforcing correct parameter types in derived virtual function
我发现很难非常简洁地描述这个问题,所以我附上了一个演示程序的代码。
一般的想法是,我们想要一组派生类,它们被迫从基类实现一些抽象的Foo()函数。每个派生的Foo()调用必须接受一个不同的参数作为输入,但是所有的参数也应该派生自一个BaseInput类。
到目前为止,我们看到了两种可能的解决方案,我们都不太满意:
-
从基类中删除Foo()函数,并在每个派生类中使用正确的输入类型重新实现它。然而,这消除了在每个派生类中以相同方式实现的强制。
-
在接收函数内部执行某种动态强制转换,以验证接收到的类型是否正确。然而,这并不能防止程序员犯错误并传递不正确的输入数据类型。我们希望传递给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
对象后面是什么子类型…
-
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.
您可以使用奇怪的反复出现的模板模式的变体。
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()的好处。
这当然是有限制的,但是您可以在构造函数参数中使用/模拟一致性。
- 扩展C++生成的代码的模板参数类型名称
- 如何在 c++ 中定义接受不同参数类型的函数向量?
- 在 C++ 中运行时调用模板时,是否可以切换模板的参数类型?
- 将函数参数类型声明为 auto
- 将函数的参数 - 签名从使用 'std::function<T>' 转换为模板参数类型
- 在 C++17 中调用具有不同参数类型的构造函数
- 具有先前参数类型匹配的参数包
- 我想知道为什么"std::unique_ptr<int> foo(新 int)"是合法的,因为"std::<int>unique_ptr"要求输入参数类型应该是"int"?
- 将可变参数类型列表的扩展打包为复杂类型的初始值设定项列表 - 合法吗?
- MSVC 错误:4 个重载中的任何一个都无法转换所有参数类型
- 使用constexpr + auto作为返回和参数类型的奇怪类型推导
- 如何从第一个参数推断第二个参数类型?
- C++模板函数中,指定回调函子/lambda 的参数类型,同时仍允许内联?
- 如何用不同的参数类型和数字回调函数
- C++stoi:这两个重载都无法转换所有参数类型
- 为什么std::{container}::template不能推导其参数类型
- 为模板参数类型中的新对象分配内存
- 为指向成员模板参数的指针推导额外模板参数类型的紧凑方式
- 使用std::conditional中的模板来确定函数参数类型
- C++中的短参数类型