C ++ Polymorphism and Derived Class Types - "Ugly Programming" Using Pointer Types

Firstly, I'm not sure how to describe what I'm doing on one line ... hence the slightly blurry headline.

The shortest description of the problem that I can give is that "I have a function, and it should be able to accept any of the many possible types of the class as an argument, and these classes are all derived from the base class."

In particular, I have 2 categories of the class and both implement different types of methods that are similar but not exactly the same.

Maybe it's better if I just give an example? You will see that I am doing some strange things with a pointer type. I do not think this is a good programming practice. At least they are a little weird, and I wonder if there is an alternative, better way to do something.

So here is my attempt at a simplified example:

class device { // Nothing here - abstract base class } class inputDevice : device // inherit publicly, but it doesn't matter { virtual input* getInput() { return m_input; } // input is a class } class outputDevice : device { virtual output* getOutput() { return m_output; } // output is also a class } class inputoutputDevice : public inputDevice, public outputDevice { // Inherits the get methods from input and output types } // elsewhere in program void do_something(device* dev, int mode_flag) { if(mode_flag == 1) // just an example { input* = ((inputDevice*)dev)->getInput(); // doing strange things with pointers } else if(mode_flag == 2) { output* = ((outputDevice*)dev)->getOutput(); // strange things with pointers } else if(mode_flag == 3) { } } 

So, you see, what’s subtle here is that the function has some behavior, depending on whether we are dealing with an argument, which is an input device or an output device.

I think I could overload the function many times, but there could be many different types of input, output, or both input and output devices ... so that would be a rather confusing method.

Putting get labels in the base class doesn't seem like a good idea either, because derived classes should NOT have the getInput() method if the device is an OUTPUT device. And similarly, an INPUT device should not have a getOutput() method. Conceptually, this just doesn't seem right.

I hope that I explained this clearly enough and made no mistakes.

+5
source share
3 answers

To expand my comment if you look, for example. This I / O reference library, you will see a class diagram that in some way resembles most of your class hierarchy: a class (actually two), an input and output class, and an input / output class that inherits from class "input" and "exit".

However, you never refer directly to the std::basic_ios or std::ios_base base classes; instead, it uses the std::ostream reference std::ostream for any output stream and std::istream for any input stream (and std::iostream for any input and output stream).

For example, to overload the input operator >> , your function accepts a reference to the std::istream :

 std::istream& operator>>(std::istream& input_stream, some_type& dest); 

Even for more general functions, you take a reference to an object std::istream , std::ostream or std::iostream . You never use the std::basic_ios base class just because of the problems you have.


To learn more about your problem and how to solve it, use two overload functions, one for the input device and one for the output device. This makes more sense, because first of all you will not have problems with type checking and casting, and also because the two functions will work completely differently depending on whether you will do input or output anyway and trying to mix it with one function just makes the code much more indispensable.

So you must have, for example,

 void do_something(inputDevice& device); 

and

 void do_something(outputDevice& device); 
+3
source

There is an interesting design problem in your do_something() function: you think that the device type matches the mode parameter, but you cannot check it.

Alternative 1: using dynamic casting

First of all, since you expect your device class to be polymorphic, you must provide a virtual destructor. This ensures that the device is also polymorphic.

Then you can use dynamic casting to make your code reliable (here I assumed that mdode 3 is for I / O, but this is just for a general idea):

 void do_something(device* dev, int mode_flag) { if(mode_flag == 1 || mode_flag==3) // just an example { inputDevice* id=dynamic_cast<inputDevice*>(dev); // NULL if it not an input device if (id) { input* in = id->getInput(); // doing strange things with pointers } else cout << "Invalid input mode for device"<<endl; } if(mode_flag == 2 || mode_flag==3) { outputDevice* od=dynamic_cast<outputDevice*>(dev); if (od) { output* out = od->getOutput(); } else cout << "Invalid output mode for device"<<endl; } // ... } 

Alternative 2: make do_something method

I don’t know how complicated it is, but if you intend to do something using any devices, you can simply make it a method.

 class device { public: virtual void do_something(int mode_flag) = 0; virtual ~device() {} }; 

You will get an idea. Of course, you can also have a mix that has a global do_something() function that performs common actions and calls member functions for the part, which should depend on the type of device.

Other comments

Note that your inputoutputDevice inherits twice from the device. Once you have members in the device, this can lead to ambiguity. Therefore, I suggest you consider virtual inheritance for a device class.

 class inputDevice : public virtual device ...; class outputDevice : public virtual device ...; 

Another approach might be to have a more complex I / O interface in the device:

 class device { public: virtual bool can_input() = 0; // what can the device do ? virtual bool can_output() = 0; virtual input* getInput() = 0; virtual void setOutput(output*) = 0; virtual ~device() {}; }; class inputDevice : public virtual device { bool can_input() { return true; } bool can_output() { return false; } input* getInput() { return m_input; } // input is a class void setOutput(output*) { throw 1; } // should never be called ! }; ... void do_something(device* dev, int mode_flag) { if(mode_flag == 1 && dev->can_input() ) // just an example ... ... } 
+1
source

Since the problem area is quite wide, it is impossible to give an exact answer, but since it refers to the device, the Linux kernel device model may be appropriate.

See the wiki linux-kernel for a deep dive. Look at LDD3 there, as it is a free e-book, you can see how the kernel works inside.

The general concept of the linux kernel is that each device is represented by files. Therefore, the driver exports file descriptors that have vtable (see fs.h ).

One of the simplest character devices is a named pipe (see its vtable , as well as all function definitions in a file).

A simple C ++ conversion might look like this:

 struct abstract_dev { virtual int read(input *) { return -1; /* fail */ } virtual int write(output *) { return -1; /* fail */ } virtual int ioctl(int cmd, void **args) { return -1; } }; struct input_dev : public abstract_dev { input_dev() : state(0) {} int state; int read(input *) override { if (state != 2) { return -1; } /* do smth */ return 0; } int ioctl(int cmd, void **args) override { if (cmd == 2) { state = 2; return 0;} return -1; } }; 

For modes, the kernel uses the ioctl system call to set the mode (as a control plane) and saves the state in the device driver. and subsequent reads and writes take this mode into account. In the named pipe example, you can resize the internal buffer by setting the value to FIONREAD .

Hope this helps find a solution to your problem.

0
source

All Articles