My Way of Implementing Streams #2

Uwaga! Informacje na tej stronie mają ponad 5 lat. Nadal je udostępniam, ale prawdopodobnie nie odzwierciedlają one mojej aktualnej wiedzy ani przekonań.

Aug 2009

In my previous post My Way of Implementing Streams #1 I've revealed what's my attitude towards stream class hierarchy. I want to explore this subject further by looking into details of my code. Let's start with writing and reading a single value to/from a stream. I know that processing bigger chunk of data with single call is more optimal, but sometimes we want to save a single number, boolean value, vector or whatever. We traditionally do it by passing address of the value and its size, just like that:

fwrite(&value, sizeof(value), 1, file);

But in C++ we can automate this by defining a template method, which I like to call "WriteEx":

template <typename T>
void WriteEx(const T &x)
  return Write(&x, sizeof(x));

template <typename T>
void ReadEx(T *x)
  MustRead(x, sizeof(*x));

Now I can read and write single values from/to my streams much easier:


Character strings are special kinds of values because they have variable size. Traditional C approach is to keep strings NULL-terminated, but I don't like this. I prefer to remember string length together with characters because: 1. I can do stream read and write with just two calls (one for length and one for data) instead of reading/writing byte after byte until I meet '\0', 2. I know how many bytes to allocate for character buffer when reading a string, 3. string can contain any characters including 0. On the other hand, I have to decide how many bytes do I want to use as string length, thus limiting maximum string length to 255 B (1 B), 64 KB (2 B) or 4 GB (4 B). To leave this choice to the user, I've defined many functions for reading and writing string to my streams:

// Write string preceded by length as 1 B
void WriteString1(const string &s);
// Write string preceded by length as 2 B
void WriteString2(const string &s);
// Write string preceded by length as 4 B
void WriteString4(const string &s);
// Write only string content
void WriteStringF(const string &s);

// Read string preceded by length as 1 B
void ReadString1(string *s);
// Read string preceded by length as 2 B
void ReadString2(string *s);
// Read string preceded by length as 4 B
void ReadString4(string *s);
// Read only string content
void ReadStringF(string *s, size_t NumChars);
// This one does not need a comment :)
void ReadStringToEnd(string *s);

Another thing I want to mention is the concept of skipping bytes while reading from a stream. Sometimes we want to ignore some bytes, so I've defined Skip method for this purpose.

virtual size_t Skip(size_t MaxLength);
void MustSkip(size_t Length);

Interesting here is that I provide default implementation in my both base stream classes. Default implementation in Stream class just reads given number of bytes to an internal buffer and ignores it:

size_t Stream::Skip(size_t MaxLength)
  if (MaxLength == 0) return 0;
  char Buf[BUFFER_SIZE];
  uint BlockSize, ReadSize, Sum = 0;
  while (MaxLength > 0)
    BlockSize = std::min(MaxLength, BUFFER_SIZE);
    ReadSize = Read(Buf, BlockSize);
    Sum += ReadSize;
    MaxLength -= ReadSize;
    if (ReadSize < BlockSize)
  return Sum;

In the SeekableStream class more optimal algorithm can be used, which just sets cursor position. Derived classes of specific types of streams are still free to reimplement Skip method to provide even better versions.

size_t SeekableStream::Skip(size_t MaxLength)
  uint Pos = GetPos();
  uint Size = GetSize();
  uint BytesToSkip = std::min(MaxLength, Size - Pos);
  return BytesToSkip;

Last thing I want to tell about today are some utility methods and functions which I've created just for convenience. I think their names are self-explanatory :)

class Stream
  size_t CopyFrom(Stream *src, size_t Size);
  void MustCopyFrom(Stream *src, size_t Size);
  void CopyFromToEnd(Stream *src);

void SaveStringToFile(const std::string &FileName, const std::string &Data);
void LoadStringFromFile(const std::string &FileName, std::string *Data);
void SaveDataToFile(const std::string &FileName, const void *Data, size_t NumBytes);

Next time I will show specific classes of my streams with some examples of how do I use them.

Comments | #c++ Share


[Download] [Dropbox] [pub] [Mirror] [Privacy policy]
Copyright © 2004-2020