C ++ binary input/output stream interface design
When it comes to input and output streams, as CPPer, it is natural to think of std: iostream. For text stream processing, iostream can be said to be powerful enough to deal with general complexity requirements without pressure. Binary stream processing can only be described as "simple". Sadly, as a programmer who has worked hard for many years in the multimedia software field, the most common thing is binary stream. A popular book in the past few years is called what men come from Mars and women come from Venus. Similarly, if the text stream comes from Mars, the binary stream comes from. For a text stream, we may expect this interface function: 1 string text = stream. get_line (); // basic interface 2 string word = stream. get_word (); // enhancement interface for binary stream, the interface we expect may look like this: 1 int read_bytes = stream. read (buffer, size); // basic interface 2 int value = stream. read_int32 (); // The enhancement interface is used as the insert/extract operator ("<"/">") of the iostream soul. The overload is a God-like existence for text streams, however, binary streams cannot be used at all. Because binary streams require precise control of byte (or even bit), the following interface is available: int v1 = stream. read_uint16 (); // read short int v2 = stream. read_int16 (); // Read unsigned short intint v3 = stream. read_uint24 (); // read 3-byte unsigned integer, which is a 3-byte 24-bit int v4 = stream. read_int24 (); // think about how abnormal this function is! The operator overloading Based on the derivation during compilation is difficult to meet similar requirements. In my opinion, it is not worth the candle to merge the two methods into the same class. There is almost no similarity between the core requirements and the non-core requirements are irrelevant, basically, there will be no text stream or binary stream. Iostream is just doing this. For this, only (wo) can (cuo) OH (le) OH (ma. Binary streams can be divided into input streams and output streams (nonsense) in the direction of the stream. At another latitude, binary streams can be divided into sequential access streams and random access streams, the main difference between the two is whether positioning operations (seek) are supported. The former is not supported, and the latter is supported. For example, standard input and output streams are sequential access streams, file streams generally allow random access to the stream. At a higher level, the sequential access stream has a built-in Time Arrow, neither turning back nor jumping. A Random Access stream is a built-in space axis, no directionality (or weak directionality). If you want to, you can read from the end of a file to the header. Therefore, real-time streams with Time Attributes are generally sequential access streams, such as the data streams generated by recording and screen recording, such as live streams of online live videos. There are four combinations of two latitudes and two categories, so we can design a newbility architecture: Haha, is it very powerful ...... Afraid ?...... If you are afraid, this is just an abstract interface class. If you consider implementation classes and various derivatives, the complexity of the system will be increased by at least two times. ...... The figure above is a joke. The system in the figure is a typical hypothetical abstraction, and it cannot even be used for over-design. Although exaggerated, in reality, it is not a system that has not made similar errors. For example, the implementation code inside the base classes of DirectShow is quite a bit of a god-like place (to say this, how deep is my resentment against DirectShow ......). The first step to solve any problem is to simplify the problem, that is, to grasp the main contradiction and ignore the secondary contradiction. At least for some time, CPPer was particularly keen on exquisite design, and exquisite design often first complicate the simple problem, do you still remember the Classic C ++ edition Hello World? The purpose of the exquisite design is to reuse the code, but the reality is: the simpler the code, the easier it is to reuse, and the more elaborate the code, the more complicated it is to reuse. Back to our problem, we should first find the main contradiction, that is, the core requirement: A group of simple input and output streams that can cope with most daily tasks. Note, here we use "input and output streams" instead of "input and output streams ". In fact, in actual development work, there are few requirements for a stream, that is, the input stream and the output stream. If it is encountered, it is often because of complicated business requirements, in this case, it is not unacceptable to write a stream specifically for special needs. Therefore, the requirement of "both the input stream and the output stream" was cut down as a secondary contradiction. We have not yet considered the clear inheritance relationship and temporarily cut down the derivative extensions such as the file stream, the rest of the system is simple and clear: there are only four isolated abstract classes. The output stream is similar to the input stream but not associated with it. Therefore, we take the input stream as an example to further investigate the two abstract classes: random_istream and sequential_istream. As mentioned above, the main difference between a sequential access stream and a random access stream is that a sequential access Stream does not support location operations (seek) and a random access stream. That is to say, if sequential_istream is designed as follows: 1 class sequential_istream2 {3 public: 4 virtual void read (void * buffer, int bytes) = 0; 5}; then random_istream is like this: class random_istream {public: virtual void read (void * buffer, int bytes) = 0; virtual void seek (int offset) = 0 ;}; what is found? Random_istream is the superset of sequential_istream, which means that random_istream can be inherited from sequential_istream. It neither needs to be designed to be two isolated classes nor to forcibly extract a public base class for two classes for general purpose. In terms of concept, it is also perfect. a Random stream can be accessed as a Sequential stream. This is a typical "is-a" relationship. The adjusted design is as follows: Copy code 1 class input_stream 2 {3 public: 4 virtual void read (void * buffer, int bytes) = 0; 5}; 6 7 class random_istream: public input_stream 8 {9 public: 10 virtual void seek (int offset) = 0; 11}; copy the Code to remove the sequential keyword of the sequential stream, so that the concept inheritance logic is smoother. In fact, there is another design scheme. You can design input_stream as a fat interface and support sequential streams and random access streams: Copy code 1 class input_stream2 {3 public: 4 virtual void read (void * buffer, int bytes) = 0; 5 virtual void seek (int offset) = 0; 6 virtual bool seekable () const = 0; 7 }; copy the code and pay attention to the seekable method. It returns a Boolean value indicating whether the seek method is valid. The valid value indicates that this is a random stream, and the invalid value indicates that it is a Sequential stream. We didn't adopt this scheme. Although it seems simple to have a lower level of inheritance relationship, the actual application is not simpler than the previous scheme. In terms of semantics, seekable is a state attribute (do you have this argument) that represents a class of State of an object. A boolean state can represent two states, each increase doubles the application complexity and increases exponentially (don't trust me, I'll just talk about it ). Although there is only one status attribute, it is enough to cause us a lot of trouble: Is the status returned by seekable temporary or permanent? Is it possible to change midway through? At least we cannot see the answer from the interface; what if seekable returns false and the seek method is still called? A derived class of an ordered Stream does not need to implement the seekable and seek methods at all, even if it simply returns false and throws an exception; each time you try to call the seek method, you must first call seekable to determine, and finally there will be a bunch of if-else; the above problem is also inherent in the fat interface, therefore, we must use the fat interface with caution. This time we chose to discard it. Add all kinds of derived classes and auxiliary classes to get the final structure: At this point, our work has been basically completed, and the rest is boring. Input_stream and random_istream interfaces: Copy code 1 class input_stream 2 {3 public: 4 virtual void read (void * buffer, uint32_t bytes) = 0; 5 virtual void skip (uint32_t bytes) = 0; 6 virtual uint64_t tell () const = 0; 7}; 8 9 10 class random_istream: public input_stream11 {12 public: 13 virtual void seek (int64_t offset, seek_origin) = 0; 14 virtual uint64_t size () const = 0; 15}; copy the code to note the skip method. This method is used to read data in sequence. When skipping the specified number of bytes, the function can also be achieved by dropping data after reading. In random_istream, you can directly use the seek method to implement the function. Finally, we decided to add this method mainly for ease of use, the efficiency is the same as the optimal substitution method of the current stream. Output_stream and random_ostream: Copy code 1 class output_stream 2 {3 public: 4 virtual void write (void * buffer, uint32_t bytes) = 0; 5 virtual void flush () = 0; 6 virtual uint64_t tell () const = 0; 7}; 8 9 class random_ostream: public output_stream10 {11 public: 12 virtual void seek (int64_t offset, seek_origin) = 0; 13}; the replication code binary_reader and binary_writer are extensions of input_stream and output_stream. External extensions are used. It is more flexible than inheritance extension. If inheritance extension is used, does binary_reader inherit from input_stream or random_istream, or designs binary_reader as an independent interface class, and implements classes such as file_istream inherit both binary_reader and random_istream? These are tangled issues, and every solution is not perfect. The external extension method is perfect and easy to implement. You only need to insert an input_stream pointer to binary_reader, as shown below: copy code 1 input_stream * ist =... 2 3 binary_reader reader (ist); 4 5 int v1 = reader. read_uint8 (); 6 7 reader. skip (4); 8 int v2 = reader. read_uint16_be (); 9... 10 11 // The seek operation can also support 12 13 random_istream * ist =... 14 15 binary_reader reader (ist); 16 17 int v1 = reader. read_uint8 (); 18 19 ist-> seek (4, see_origin: current); 20 int v2 = r Eader. read_uint16_be (); 21... the complete declaration of the copy code binary_reader is roughly as follows. binary_writer is similar to this: Copy code 1 class binary_reader 2 {3 public: 4 binary_reader (input_stream * stream ); 5 6 void read (void * buffer, uint32_t read_bytes); 7 void skip (uint32_t offset); 8 9 uint64_t tell () const; 10 11 uint8_t read_uint8 (); 12 13 uint16_t read_uint16_be (); 14 uint32_t read_uint24_be (); 15 uint32_t read_uint32_be (); 16 uint64_t read_uint 64_be (); 17 18 uint16_t read_uint16_le (); 19 uint32_t minute (); 20 uint32_t minute (); 21 uint64_t read_uint64_le (); 22 23 .... 24 25 private: 26 input_stream * _ stream; 27}; copy the code ...... It took a week to finish writing this article. It is not that easy to explain a simple design scheme. This solution, especially the design of specific interface functions, is still not perfect, and needs to be gradually improved in the process of practical application. Of course, it will not be too difficult to make improvements in a simple solution. Next, I want to write down what is behind this solution-error handling. Specifically, it is an exception-based error handling solution.