Providing the correct parameter types in a derived virtual function

It is difficult for me to describe this problem very briefly, so I attached the code for the demo program.

The general idea is that we want a set of Derived classes to be forced to implement some abstract function Foo () from the base class. Each of the received Foo () calls must accept a different parameter as an input parameter, but all parameters must also be obtained from the BaseInput class.

So far, we see two possible solutions, and we are not very satisfied:

  • Remove the Foo () function from the base class and override it with the correct input types in each Derived class. This, however, removes the enforcement that must be implemented in the same way in each derived class.

  • Make some kind of dynamic click inside the receiving function in order to check the correctness of the received type. However, this does not prevent the programmer from making a mistake and passing the wrong type of input data. We would like the type to be passed to the Foo () function for proper compilation.

Is there some kind of pattern that could provide this behavior? Does this whole idea violate some fundamental idea underlying the OOP? We would really like to hear your data on possible solutions beyond what we have come up with.

Many thanks!

#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 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 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
source share
6 answers

Since the Derived class is Base , it should never delay the prerequisites of the base contract : if it should behave like a Base , it should accept BaseInput perfectly. This is known as the Liskov substitution principle.

Although you can check the execution time of your argument, you can never achieve a completely safe type: this compiler can match DerivedInput when it sees a Derived object (static type), but it cannot know which subtype will be behind the object Base ...

Requirements

  • DerivedX should take DerivedXInput
  • DerivedX::Foo must be equal to the DerivedY::Foo interface

contradict: either the Foo methods are implemented in terms of BaseInput , and therefore have the same interfaces in all derived classes, or the DerivedXInput types DerivedXInput different and they cannot have the same interface.

This, in my opinion, is a problem.

This problem arose when writing closely related classes that are processed in a non-standard form:

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

And framework:

 struct FruitPipeLine { FruitTree* tree; FruitEater* eater; void cycle(){ eater->eat( tree->pick() ); } }; 

Now this proves that the design is too easily broken: there is no part in the design that aligns trees with feeders:

  FruitPipeLine pipe = { new BananaTree, new LemonEater }; // compiles fine pipe.cycle(); // crash, probably. 

You can improve design consistency and remove the need for virtual dispatch by creating a template:

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

Implementations are indeed specialized specializations:

 template<> class Tree<Banana> { Banana* pick(){ return new Banana; } }; ... PipeLine<Banana> pipe; // can't be wrong pipe.cycle(); // no typechecking needed. 
+5
source

Perhaps you can use a variation of a curiously repeating pattern template .

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

This is untested, just a shot from the hip!

If you don't mind a dynamic throw, how about this:

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

Thus, you have built a dynamic type check in an intermediate class, and your clients are always extracted from FooDispatch<DerivedInput> .

+2
source

What you are talking about is covariant argument types, and this is a rather unusual function in the language, since it violates your contract: you promised to accept the base_input object because you inherit from base , but you want the compiler rejects everything except a small subset of base_input s ...

For programming languages, the proposal of opposite options is much more common: contra-variant arguments, since the derived type will not only accept everything that it should accept under the contract, but also other types.

In any case, C ++ does not offer contravariance in argument types, but only covariance in the return type.

+1
source

C ++ has many dark areas, so it’s hard to say that a particular thing cannot be undone, but based on the dark areas that I know, without casting, this is not possible. The virtual function specified in the base class requires that the type of the argument remain unchanged in all children.

I am sure that the cast can be used in a painless way, although it is possible by providing the base class with a member of the "Enum" type, which is uniquely specified by the constructor of each possible child element that could inherit it. Foo () can then check what the “type” is and determine what type it is before doing anything, and throw a statement if it is surprised by something unexpected. This is not compilation time, but it is the closest compromise I can come up with, while retaining the advantages that require the definition of Foo ().

0
source

It is definitely limited, but you can use / model a match in the parameters of the constructors.

0
source

Source: https://habr.com/ru/post/895800/


All Articles