Stream Class Inheritance

Started by
8 comments, last by Ectara 10 years, 12 months ago

I have a stream class that I'm writing in C++ that is based on my stream hierarchy in C. I had decided that I would opt for a better design, and instead of having it be an error condition at runtime when you do something that the stream doesn't support, I would divide the functionality up through an inheritance tree, so that an output stream would not have the functionality to perform input, and vice-versa. This way, the compiler would immediately report an error, since the functionality does not exist within the class. But, that's old news. I presume that multiple-inheritance is the best way to accomplish combining read and write functionality, since they both operate upon a common state, so composition would be out, since having two separate states would break the functionality. I could write a separate InputOutputStream class and manually splice together their behaviors, but that seems like a foolish way to avoid multiple-inheritance through code duplication.

Now, the problem, where things get interesting, is that there were some stream types that could not be rewound. It was a one-way, forward stream, and the "file pointer" could not be moved backward. My idea to allow this was to have the BasicStream class not have any functionality that would allow it to be rewound, and have all constants related to seeking backward removed. I would then have a RewindableStream class, that would derive from BasicStream, and would in turn allow RewindableInputStream, RewindableOutputStream, and others. This drastically complicates things, and requires at least twice as many derived classes.

I previously implemented this through a flag that defined whether or not rewinding was possible. This seemed fragile to me, because I feel it shouldn't be a run-time error if you do something that you to be impossible before you ever run the code, like trying to rewind a non-rewindable stream. I had no better way of doing it then, but I feel that it should be on the same level of severity like attempting to read from an output stream. For an example of an unrewindable stream, consider special output devices like a printer or printing bitmap fonts to a framebuffer. You can't very well rewind a printer, and when printing to a frame buffer, once it's printed, it's pixels on an image. There's no information available on how to move backward, and even if there ways, there's no information on how to remove its previous contents.

It would seem that allowing one to attempt to rewind unrewindable streams and have it report an error that doesn't always get checked is kind of backward, and results in confusion (there have been topics here on why rewinding stdout doesn't work).

So, my question, is how should I implement functionality that isn't always present and I would like to remove from streams that don't use it, while maintaining the relatively simple diamond that exists from BasicStream to InputStream and OutputStream, to InputOutputStream?

Advertisement

So, my question, is how should I implement functionality that isn't always present and I would like to remove from streams that don't use it, while maintaining the relatively simple diamond that exists from BasicStream to InputStream and OutputStream, to InputOutputStream?

Probably not through inheritance. Inheritance should be used to create an "is a" relationship. What you're trying to do violates the Liskov Substitution Principle. Perhaps use components instead of inheritance? You rule out components, but I'm not sure why (you could potentially have a "reading" component, a "writing" component, a "rewinding" component, etc.)

Besides, what point is the base class BasicStream? I don't see how it really serves a purpose.

[size=2][ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]

You rule out components, but I'm not sure why (you could potentially have a "reading" component, a "writing" component, a "rewinding" component, etc.)

since they both operate upon a common state, so composition would be out, since having two separate states would break the functionality.

Composition wouldn't be feasible, because the reading and writing depend upon a single offset within the file. In order to do this, the components would have to know about the base stream class' member variables, and interact with them, in order to manipulate a shared state. It seems like this is counter-intuitive, and that it would make more sense if the higher order classes were derived classes, since all of InputStream, OutputStream, and RewindableStream have an is-a relationship with BasicStream.

What you're trying to do violates the Liskov Substitution Principle.

I can't quite see how that is so. An InputOutputStream is an OutputStream. Where an OutputStream is expected, an InputOutputStream can be used. An OutputStream is a RewindableStream. If the few places that a RewindableStream is expected, which is during seeking, an OutputStream can be used. A RewindableStream is a BasicStream. In the places that a BasicStream is expected, which are setting and querying the flags and state, a RewindableStream can be used.

Besides, what point is the base class BasicStream? I don't see how it really serves a purpose.

The BasicStream class houses things common to all of the classes below it; my streams have a single "file pointer", and there are some operations that can be performed on both input and output streams alike. Additionally, the class defines enums and constants, and facilitates keeping track of the state, such as the error condition, and trivial flags. Sort of like std::ios_base.

You could eliminate all the inheritance by simply having an internal/private helper class that is responsible for maintaining the current seek pointer, and has methods that read/write/seek/etc....

Then have publicly visible wrapper classes (OutputStream, RewindableStream, RewindableInputStream, etc....) that only implement the specific functionality you need. Internally they just delegate to the helper class which has all the functionality.

Well, you could still use composition and a shared state, but I'm not necessarily pushing for composition.

What you're trying to do violates the Liskov Substitution Principle.

I can't quite see how that is so.

I was talking about this part:

So, my question, is how should I implement functionality that isn't always present and I would like to remove from streams that don't use it, while maintaining the relatively simple diamond that exists from BasicStream to InputStream and OutputStream, to InputOutputStream?

Perhaps it's just the way it's worded and I'm reading it wrong, but the way I'm reading it you're removing functionality down the inheritance tree, which violates LSP.

Besides, what point is the base class BasicStream? I don't see how it really serves a purpose.

The BasicStream class houses things common to all of the classes below it; my streams have a single "file pointer", and there are some operations that can be performed on both input and output streams alike. Additionally, the class defines enums and constants, and facilitates keeping track of the state, such as the error condition, and trivial flags. Sort of like std::ios_base.

Ah, I see.

If you wanted to maintain a design similar to C++'s standard library streams, you could have an inheritance diagram like:

class BasicStream
{ /* implement all BasicStream stuff */ };
 
class InputStream : virtual public BasicStream
{ /* implement all input functions */ };
 
class RewindableStream : virtual public BasicStream
{ /* implement all rewinding functions */ };
 
class OutputStream : public RewindableStream
{ /* implement all output functions */ };
 
 
 
// Now build up the other "mix-in" classes:
class InputOutputStream : public InputStream, public OutputStream
{ /* shouldn't need any code */ };
 
class RewindableInputStream : public InputStream, public RewindableStream
{ /* shouldn't need any code */ };
[size=2][ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]

You could eliminate all the inheritance by simply having an internal/private helper class that is responsible for maintaining the current seek pointer, and has methods that read/write/seek/etc....

Then have publicly visible wrapper classes (OutputStream, RewindableStream, RewindableInputStream, etc....) that only implement the specific functionality you need. Internally they just delegate to the helper class which has all the functionality.

Essentially like what I had in C, but with a wrapper around it. The problem that I see now is that I can't pass an InputOutputStream as an OutputStream; since they aren't derived from each other, I would need some sort of unwieldy mechanism to convert between the two, rather than just passing it. I'm not trying to eliminate all inheritance, just to make my goals possible without making a monster that I won't want to use.

Perhaps it's just the way it's worded and I'm reading it wrong, but the way I'm reading it you're removing functionality down the inheritance tree, which violates LSP.

That's poor wording on my part; that's the kind of thing I want to avoid. Previously, I had all of the functionality in one object, and through the use of flags it would limit what is not allowed. Now, I'm looking for a way to do only inherit the features that it is supposed to have, so that trying to do anything else with it is a compile error.

If you wanted to maintain a design similar to C++'s standard library streams, you could have an inheritance diagram like:

Ah, so multiple-inheritance would be the best tool for this job, while maintaining the ability to cast the pointer of the derived to a pointer of the base, and not having to handle child objects that require workarounds and more complex syntax? If I don't have to use dynamic_cast, and keep the vtable size down, will this become problematic for any reason?

You could eliminate all the inheritance by simply having an internal/private helper class that is responsible for maintaining the current seek pointer, and has methods that read/write/seek/etc....

Then have publicly visible wrapper classes (OutputStream, RewindableStream, RewindableInputStream, etc....) that only implement the specific functionality you need. Internally they just delegate to the helper class which has all the functionality.

Essentially like what I had in C, but with a wrapper around it. The problem that I see now is that I can't pass an InputOutputStream as an OutputStream; since they aren't derived from each other, I would need some sort of unwieldy mechanism to convert between the two, rather than just passing it. I'm not trying to eliminate all inheritance, just to make my goals possible without making a monster that I won't want to use.

Out of curiosity, could templates work? It's an option to consider, at least.

If you wanted to maintain a design similar to C++'s standard library streams, you could have an inheritance diagram like:

Ah, so multiple-inheritance would be the best tool for this job, while maintaining the ability to cast the pointer of the derived to a pointer of the base, and not having to handle child objects that require workarounds and more complex syntax? If I don't have to use dynamic_cast, and keep the vtable size down, will this become problematic for any reason?

It should work. You won't have to dynamic cast things either. This is what C++'s stream classes do (basic_istream and basic_ostream virtually inherit from basic_ios, and basic_iostream inherits from both basic_istream and basic_ostream). You can pass a basic_iostream to a function that takes a basic_ostream by reference without any casting.

[size=2][ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]

(basic_istream and basic_ostream virtually inherit from basic_stream, and basic_iostream inherits from both basic_istream and basic_ostream)

I'm a bit rusty on my C++ now, but how does basic_iostream avoid having two sets of internal stream state if it inherits from basic_stream twice? (Or maybe two sets of underlying state is not a problem?).

(basic_istream and basic_ostream virtually inherit from basic_stream, and basic_iostream inherits from both basic_istream and basic_ostream)

I'm a bit rusty on my C++ now, but how does basic_iostream avoid having two sets of internal stream state if it inherits from basic_stream twice? (Or maybe two sets of underlying state is not a problem?).

By virtual inheritance (basic_istream virtually inherits from basic_ios, and basic_ostream virtually inherits from basic_ios). Here are the definitions of these classes:


// Note: I'm omitting the template stuff for simplicity
 
// Section 27.7.2.1 of the C++ standard, for the curious
class basic_istream :
    virtual public basic_ios { // Notice the virtual inheritance
  /* ... */
};
 
// Section 27.7.3.1 of the C++ standard, for the curious
class basic_ostream :
    virtual public basic_ios { // Notice the virtual inheritance
  /* ... */
};
 
// Section 27.7.2.5 of the C++ standard, for the curious
class basic_iostream : // This works fine, since basic_istream and basic_ostream virtually inherit from basic_ios
    public basic_istream,
    public basic_ostream {
  /* ... */
};

Also, sorry, I meant basic_ios instead of basic_stream in that post (which I've fixed now).

[size=2][ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]

Out of curiosity, could templates work? It's an option to consider, at least.


I tried to think of how I do it, especially since it is already templated for other parameters. However, it seems that while it would save on writing more than one nearly identical class, it would compile more that one nearly identical class, since it'd have to make a separate instantiation of the class and its members for each combination of template parameters, regardless of whether or not one parameter is just a bool stating whether or not to include rewinding functionality. I would think that if I derived one class from a base class, it wouldn't make a separate instance of the base class' member constants and functions for each derived class. Additionally, every class derived from this templated base class becomes inherently templated, itself, due to relying on a templated base class; some classes derived from them would work with either variation of the base class, so it becomes templated to allow whichever template parameter combination that is being used for the base class. The derived class may now be duplicated, since there are two or more versions that inherit from two or more template instantiations. Additionally, functions that accept the templated base class as a parameter must be templated as well, if it can work with any variation of the templated class. The functions may now get duplicated. For each level of depth in the tree, the possible duplication grows exponentially, especially if there are other options that I allow through templates. The compiler and linker are allowed to find and remove redundant copies of functions, but inevitably, many of them will remain because they must. So, for this reason, I would rule out templates due to requiring a greater executable size and a more complicated hierarchy. If it becomes the only way, I'll consider it.

I'm a bit rusty on my C++ now, but how does basic_iostream avoid having two sets of internal stream state if it inherits from basic_stream twice? (Or maybe two sets of underlying state is not a problem?).

It doesn't, per se: it has a separate read state, and write state. It has two "file pointers" that move independently of each other. Past that, all it has is a couple bitfields that are manipulated through functions in the base class. It isn't problematic, but I don't like this approach.

This topic is closed to new replies.

Advertisement