An Introduction to C++ Programming - Part 9/13

From EDM2
Jump to: navigation, search
An Introduction to C++ Programming / Part
1 2 3 4 5 6 7 8 9 10 11 12 13

Written by Björn Fahller

File I/O and Binary Streams

In parts 5 and 6, the basics of I/O were introduced, with formatted reading and writing from standard input and output. We'll now have a look at I/O for files. In a sense, it's better to stop using the term I/O here, and instead use streams and streaming, since the ideas expressed here and in parts 5 and 6 can be used for other things than I/O, for example in-memory formatting of data (we'll see that at the very end of this article.)

Files

In what way is writing ``Hello world'' on standard output different from writing it to a file? The question is worth some thought, since in many programming languages there is a distinct difference. Is the message different? Is the format (as seen from the program) different? I cannot see any difference in those aspects. The only thing that truly differs is the media where the formatted message ends up. In the former case, it's on your screen, but for file I/O it's in a file somewhere on your hard disk. In other words, there is very little difference, or at least, there's very much in common.

As we've seen so far, commonality is expressed either through inheritance or templates, depending on what's common and what's not. To refresh your memory, templates are used when we want the same kind of behaviour, independent of data. For example a stack of some data type. Inheritance is used when you want similar, but in some important aspects different, behaviour at runtime for the same kind of data. We saw this for the staff hierarchy and mailing addresses in parts 7 and 8. In this case it's inheritance that's the correct solution, since the data will be the same, but where it will end up (and most notably, how it does end up there) differs. (Incidentally, there's a good case for using templates too, regarding the type of characters used. The C++ standard does indeed have templatized streams, just for differing between character types. Few compilers today support this, however. See the ``Standards Update'' towards the end of the article for more information.)

The inheritance tree for stream types look like this:

IntroC++-streaminh.gif

The way to read this is that there's a base class named ``ios'', from which the classes ``istream'' and ``ostream'' inherit. The classes ``ifstream'' and ``ofstream'' in their turn inherit from ``istream'' and ``ostream'' respectively. The ``f'' in the names imply that they're file streams. Then there's the odd ones, ``iostream'', which inherits from both ``istream'' and ``ostream'', and ``fstream'' which inherits from both ``ifstream'' and ``ofstream.'' Inheriting from two bases is called multiple inheritance, and is by many seen as evil. Many programming languages have banned it: Objective-C, Java, Smalltalk to mention a few, while other programming languages, like Eiffel, go to the other extreme and allow you to inherit the same base several times Personally I think multiple inheritance is very useful if used right, but it can cause severe problems. Here is a situation where it's used in the right way. Anyway, this means that ``fstream'' is a file stream for both reading and writing, while ``iostream'' is an abstract stream for both reading and writing. More often than you think, you probably don't want to use the ``iostream'' or ``fstream'' classes.

This inheritance, however, means that all the stream insertion and extraction functions (the ``operator>>'' and ``operator<<'') you've written, will work just as they do with file streams. Now, wasn't that neat? In other words, the only things you need to learn for file based I/O are the details that are specific to files.

File Streams

The first thing you need to know before you can use file streams is how to create them. The parts of interest look like this:


  class ifstream : public istream
  {
    ifstream();
    ifstream(const char* name,
             int mode=ios::in);
    void open(const char* name,
              int mode=ios::in);
    ...
  };

  class ofstream : public ostream
  {
    ofstream();
    ofstream(const char* name,
             int mode=ios::out);
    void open(const char* name,
              int mode=ios::out);
    ...
  };

  class fstream : public ofstream, public ifstream
  {
    fstream();
    fstream(const char* name,
            int mode);
    void open(const char* name,
              int mode);
    ...
  };

You get access to the classes by #including <fstream.h>. The empty constructors always create a file stream object that is not tied to any file. To tie such an object to a file, a call to ``open'' must be made. ``open'' and the constructors with parameters behaves identically. ``name'' is of course the name of the file. Since you normally use either ``ifstream'' or ``ofstream'' and rarely ``fstream'', this is normally the only parameter you need to supply. Sometimes, however, you need to use the ``mode'' parameter. It's a bit field, in which you use bitwise or (``operator|'') for any of the values ``ios::in'', ``ios::out'', ``ios::ate'', ``ios::app'', ``ios::trunc'', and finally ``ios::binary.'' Some implementations also provide ``ios::nocreate'' and ``ios::noreplace,'' but those are extensions. Some implementations do not have ``ios::binary,'' while others call it ``ios::bin.'' These variations of course makes it difficult to write portable C++ today. Fortunately, the six ones listed first are required by the standard (although, they belong to class ``ios_base,'' rather than ``ios.'') The meaning of these are:


  ios::in        open for reading

  ios::out       open for writing

  ios::ate       open with the get and set pointer at the end
                 (see Seeking for info) of the file.

  ios::app       open for append, that is, any write you make
                 to the file will be appended to the file.

  ios::trunc     scrap all data in the file if it already exists.

  ios::binary    open in binary mode, that is, do not do the brain
                 damaged LF<->CR/LF conversions that OS/2,
                 DOS, CP/M (RIP), Windows, and probably other
                 operating systems, so often insist on. The reason
                 some implementations do not have ios::binary
                 is that many operating systems do not have this
                 conversion, so there's no need for it.

  ios::noreplace cause the open to fail if the file already exists.

  ios::nocreate  cause the open to fail if the file doesn't exist.

Of course combinations like ``ios::noreplace | ios::nocreate'' doesn't make sense -- the failure is guaranteed.

On many implementations today there's also a third parameter for the constructors and ``open;'' a protection parameter. How this parameter behaves is very operating system dependent.

Now for some simple usage:


   #include <fstream.h>
 
   int main(int argc, char* argv[])
   {
     if (argc != 2) {
       cout << ``Usage: `` << argv[0] << ``filename'' << endl;
       return 1; // error code
     }
 
     ofstream of(argv[1]); // create the ofstream object
                           // and open the file.
 
     if (!of) { // something went wrong
       cout << ``Error, cannot open `` << argv[1] << endl;
       return 2;
     }
 
     // Now the file stream object is created. Write to it!
     of << ``Hello file!'' << endl;
     return 0;
   }
 

As you can see, once the stream object is created, its usage is analogous to that of ``cout'' that you're already familiar with. Of course reading with ``ifstream'' is done the same way, just use the object as you've used ``cin'' earlier.

The file stream classes also have a member function ``close'', that by force closes the file and unties the stream object from it. Few are the situations when you need to call this member function, since the destructors do close the file.

Actually this is all there is that's specific to files.

Binary streaming

So far we've dealt with formatted streaming only, that is, the process of translating raw data into a human readable form, or translating human readable data into the computer's internal representation. Some times you want to stream raw data as raw data, for example to save space in a file. If you look at a file produced by, for example a word processor, it's most likely not in a human readable form. Note that binary streaming does not necessarily mean using the ``ios::binary'' mode when opening a file (although, that is indeed often the case.) They're two different concepts. Binary streaming is what you use your stream for, raw data that is, and opening a file with the ``ios::binary'' mode, means turning the brain damaged LF<->CR/LF translation off.

Binary streaming is done through the stream member functions :


  class ostream ...
  {
  public:
    ostream& write(const char* s, streamsize n);
    ostream& put(char c);
    ostream& flush();
  ...
  };

  class istream ...
  {
  public:
    istream& read(char* s, streamsize n);
    int get();
    istream& get(char& c);
    istream& get(char* s, streamsize n, char delim='\n');
    istream& getline(char* s, streamsize n,
                     char delim='\n');
    istream& ignore(streamsize n=1, int delim=EOF);
  };

The writing interface is extremely simple and straight forward, while the reading interface includes a number of small but important differences. Note that these member functions are implemented in classes ``istream'' and ``ostream,'' so they're not specific to files, although files are where you're most likely to use them. Let's have a look at them, one by one:


  ostream& ostream::write(const char* s, streamsize n);

Write ``n'' characters to the stream, from the array pointed to by ``s.'' ``streamsize'' is a signed integral data type. Despite ``streamsize'' being signed, you're of course not allowed to pass a negative size here (what would that mean?) Exactly the characters found in ``s'' will be written to the stream, no more, no less.


  ostream& ostream::put(char c);

Inserts the character into the stream.


  ostream& ostream::flush();

Force the data in the stream to be written (file streams are usually buffered.)


  istream& istream::read(char* s, streamsize n);

Read ``n'' characters into the array pointed to by ``s.'' Here you better make sure that the array is large enough, or unpleasant things will happen. Note that only the characters read from the stream are inserted into the array. It will not be zero terminated, unless the last character read from the stream indeed is '\0'.


  int istream::get();

Read one character from the stream, and return it. The value is an ``int'' instead of ``char'' since the return value might be ``EOF'' (which is not uniquely representable as a ``char.'')


  istream& istream::get(char& c);

Same as above, but read the character into ``c'' instead. Here a ``char'' is used instead of an ``int,'' since you can check the value directly by calling ``.eof()'' on the reference returned.


  istream& istream::get(char* s, streamsize n,
                        char delim='\n');

This one's similar to ``read'' above, but with the difference that it reads at most ``n'' characters. It stops if the delimiter character is found. Note that when the delimiter is found, it is not read from the stream.


  istream& istream::getline(char* s, streamsize n,
                            char delim='\n');

The only difference between this one and ``get'' above, is that this one does read the delimiter from the stream. Note, however, that the delimiter is not stored in the array.


  istream& istream::ignore(streamsize n=1,
                           int delim=EOF);

Reads at most ``n'' characters from the stream, but doesn't store them anywhere. If the delimiter character is read, it stops there. Of course, if the delimiter is ``EOF'' (as is the default) it does not read past ``EOF,'' that's physically impossible.

Array on file

An example: Say we want to store an array of integers in a file, and we want to do this in raw binary format. Naturally we want to be able to read the array as well. A reasonable way is to first store a size (in elements) followed by the data. Both the size and the data will be in raw format.


  #include <fstream.h>

  void storeArray(ostream& os, const int* p, size_t elems)
  {
    os.write((const char*)&elems,sizeof(elems));
    os.write((const char*)p, elems*sizeof(*p));
  }

The above code does a lot of ugly type casting, but that's normal for binary streaming. What's done here is to use brute force to see the address of ``elems'' as a ``const char*'' (since that's what ``write'' expects) and then say that only the ``sizeof(elems)'' bytes from that pointer are to be read. What this actually does is to write out the raw memory that ``elems'' resides in to the stream. After this, it does the same kind of thing for the array. Note that ``sizeof(*p)'' reports the size of the type that ``p'' points to. I could as well have written ``sizeof(int),'' but that is a dangerous duplication of facts. It's enough that I've said that ``p'' is a pointer to ``int.'' Repeating ``int'' again just means I'll forget to update one of them when I change the type to something else.

To read such an array into memory requires a little more work:


  #include <fstream.h>

  size_t readArray(istream& is, int*& p)
  {
    size_t elems;
    is.read((char*)&elems, sizeof(elems));
    p = new int[elems];
    is.read((char*)elems, elems*sizeof(*p));
    return elems;
  }

It's not particularly hard to follow; first read the number of elements, then allocate an array of that size, and read the data into it.

Seeking

Up until now we have seen streams as, what it sounds like, continuous streams of data. Sometimes however, there's a need to move around, both backward and forward. Streams like standard input and standard output are truly continuous streams, within which you cannot move around. Files, in contrast, are true random access data stores. Random access streams have something called position pointers. They're not to be confused with pointers in the normal C++ sense, but it's something referring to where in the file you currently are. There's the put pointer, which refers to the next position to write data to, if you attempt to write anything, and the get pointer, which refers to the next position to read data from. An ostream of course only has the put pointer, and an istream only the get pointer. There's a total of 6 new member functions that deal with random access in a stream:


  streampos istream::tellg();

  istream& istream::seekg(streampos);

  istream& istream::seekg(streamoff, ios::seek_dir);

  streampos ostream::tellp();

  ostream& ostream::seekp(streampos);

  ostream& ostream::seekp(streamoff, ios::seek_dir);

``streampos'', which you get from ``tellg'' and ``tellp'' is an absolute position in a stream. You cannot use the values for anything other than ``seekg'' and ``seekp''. You especially cannot examine a value and hope to find something useful there (i.e. you can, but what you find out might hold only for the current release of your specific compiler, other compilers, or other releases of the same compiler, might show different characteristics for ``streampos.'') Well, there are two other things you can do with ``streampos'' values. You can subtract two values, and get a ``streamoff'' value, and you can add a ``streamoff'' value to a ``streampos'' value. ``streamoff,'' by the way, is some signed integral type, probably a ``long.''

By using the value returned from ``tellg'' or ``tellp,'' you have a way of finding your way back, or do relative searches by adding/subtracting ``streamoff'' values.

The ``seekg'' and ``seekp'' methods accept a ``streamoff'' value and a direction, and work in a slightly different way. You search your way to a position relative to the beginning of the stream, the end of the stream, or the current position, the selection of which, is done through the ``ios::seek_dir'' enum, which has these three values ``ios::beg'', ``ios::end'' and ``ios::cur.'' To make the next write occur on the very first byte of the stream, call ``os.seekp(0,ios::beg),'' where ``os'' is some random access ``ostream.''

In any reasonable implementation, any of the seek member functions use lazy evaluation. That is, when you call any of the seek member functions, the only thing that happens is that some member variable in the stream object changes value. It's not until you actually read or write, something truly happens on disk (or wherever the stream data resides.)

A stream array, for really huge amounts of data

Suppose we have a need to access enormous amounts of simple data, say 10 million floating point numbers. It's not a very good idea to just allocate that much memory, at least not on my machine with a measly 64Mb RAM. It'll not just make this application crawl, but probably the whole system due to excessive paging. Instead, let's use a file to access the data. This makes for slow access, for sure, but nothing else will suffer.

Here's the idea. The array must be possible to use with any data type, including user defined classes. Its usage must resemble that of real arrays as much as possible, but extra functionality that arrays do not have, such as asking for the number of elements in it, is OK. There must be a type, resembling pointers to arrays, that can be used for traversing it. We do not want the size of the array to be part of its type (if you've programmed in Pascal, you know why.) In addition to arrays, we want some measures of safety from stupid mistakes, such as addressing beyond the range of the array, and also for errors that arrays cannot have (disk full, cannot create file, disk corruption, etc.) We also want to say that an array is just a part of a file and not necessarily an entire file. This would allow the user to create several arrays within the same file. To prevent this article from growing way too long, quite a few of the above listed features will be left for next month. The things to cover this month are: An array of built-in fundamental types only, which lacks pointers and is limited to one file per array. We'll also skip error handling for now (you can add it as an exercise, I'll raise some interesting questions along the way,) and add that too next month.

First of all, the array must be a template, so it can be used to store arbitrary types. Since we do not want the size to be part of the type signature, the size is not a template parameter, but a parameter for the constructor. Of course, we cannot have the entire array duplicated in memory (then all the benefits will be lost,) instead we will search for the data on file every time it's needed.

Here's the outline for the class.


  template <class T>

  class FileArray
  {
  public:
    FileArray(const char* name, size_t elements);
    // Create a new array and set the size.

    FileArray(const char* name);
    // Create an array from an existing file, get the
    // size from the file.

    // use compiler defined destructor.

    T operator[](size_t index) const;
    ??? operator[](size_t index);

    size_t size() const;
  private:
    // don't want these to be used.
    FileArray(const FileArray&);
    FileArray& operator=(const FileArray&);
    ...
  };

As can be expected, ``operator[]'' can be overloaded, which is handy for providing a familiar syntax. However, already here we see a problem. What's the non-const ``operator[]'' to return? To see why this is a problem, ask yourself what you want ``operator[]'' to do. I want ``operator[]'' to do two things, depending on where it's used; like this:


  FileArray<int> x;
  ...
  x[5] = 4;
  int y = x[3];

When ``operator[]'' is on the left hand side of an assignment, I want to write data to the file, and if its on the right hand side of an assignment, I want to read data from the file. Ouch.

Warning: I've often seen it suggested that the solution is to have the const version read and return a value, and the non-const version write a value. As slick as it would be, it's wrong and it won't work. The const version is called for const array objects, the non-const version for non-const array objects.

Instead what we have to do is to pull a little trick. The trick is, as so often in computer science, to add another level of indirection. This is done by not taking care of the problem in ``operator[],'' but rather let it return a type, which does the job. We create a class template, looking like this:


  template <class T>
  class FileArrayProxy
  {
  public:
    FileArrayProxy<T>& operator=(const T&); // write value
    operator T() const; // read a value

    // compiler generated destructor

    FileArrayProxy<T>&
    operator=(const FileArrayProxy<T>& p);

    FileArrayProxy(const FileArrayProxy<T>&);
  private:
    ... all other constructors.
    FileArray<T>& array;
    const size_t index;
  };

We have to make sure, of course, that there are member functions in ``FileArray<T>'' that can read and write (and of course, those functions are not the ``operator[],'' since then we'd have an infinite recursion.) All constructors, except for the copy constructors, are made private to prevent users from creating objects of the class whenever they want to. After all, this class is a helper for the array only, and is not intended to ever even be seen. This, however, poses a problem; with the constructors being private, how can ``FileArray<T>::operator[]()'' create and return one?

Enter another C++ feature: friends. Friends are a way of breaking encapsulation. What?!?! Yes, what you read is right. Friends break encapsulation, and (this is the real shock) that's a good thing! Friends break encapsulation in a controlled way. We can, in ``FileArrayProxy<T>'' declare ``FileArray<T>'' to be a friend. This means that ``FileArray<T>'' can access everything in ``FileArrayProxy<T>,'' including things that are declared private. Paradoxically, violating encapsulation with friendship strengthens encapsulation when done right. The only alternative here to using friendship, is to make the constructors public, but then anyone can create objects of this class, and that's what we wanted to prevent. Friends are useful for strong encapsulation, but it's important to use it only in situations where two (or more classes) are so tightly bound to one another that they're meaningless on their own. This is the case with ``FileArrayProxy<T>.'' It's meaningless without ``FileArray<T>,'' thus ``FileArray<T>'' is declared a friend of ``FileArrayProxy<T>.'' The declaration then becomes:


  template <class T>

  class FileArrayProxy
  {
  public:
    FileArrayProxy& operator=(const T&); // write a value
    operator T() const; // read a value
    // compiler generated destructor

    FileArrayProxy<T>& // read from p and then write
    operator=(const FileArrayProxy<T>& p);

    // compiler generated copy contructor
  private:
    FileArrayProxy(FileArray<T>& fa, size_t n);
    // for use by FileArray<T> only.

    FileArray<T>& array;
    const size_t index;

    friend class FileArray<T>;
  };

We can now start implementing the array. Some problems still lie ahead, but I'll mention them as we go.


  // farray.hpp
  #ifndef FARRAY_HPP
  #define FARRAY_HPP

  #include <fstream.h>
  #include <stdlib.h> // size_t

  template <class T> class FileArrayProxy;
  // Forward declaration necessary, since FileArray<T>
  // returns the type.

  template <class T> class FileArray
  {
  public:
    FileArray(const char* name, size_t size); // create
    FileArray(const char* name); // use existing array
    T operator[](size_t size) const;
    FileArrayProxy<T> operator[](size_t size);
    size_t size() const;
  private:
    FileArray(const FileArray<T>&); // illegal
    FileArray<T>& operator=(const FileArray<T>&);

    // for use by FileArrayProxy<T>
    T readElement(size_t index) const;
    void storeElement(size_t index, const T&);

    fstream stream;
    size_t max_size;

    friend class FileArrayProxy<T>;
  };

The functions for reading and writing are made private members of the array, since they're not for anyone to use. Again, we need to make use of friendship to grant ``FileArrayProxy<T>'' the right to access them. Let's define them right away


  template <class T>
  T FileArray<T>::readElement(size_t index) const
  {
    T t;
    stream.seekg(sizeof(max_size)+index*sizeof(T));
    // what if seek fails?

    stream.read((char*)&t, sizeof(t));
    // what if read fails?

    return t;
  }

All of a sudden, we face an unexpected problem. The above code won't compile. The member function is declared ``const'', and as such, all member variables are ``const'', and neither ``seekg'' nor ``read'' are allowed on constant streams. The problem is one of differing between logical constness and bitwise constness. This member function is logically ``const'', as it does not alter the array in any way. However, it is not bitwise const; the stream member changes. C++ cannot understand logical constness, only bitwise constness. If you have a modern compiler, the solution is very simple; you declare ``stream'' to be ``mutable fstream stream;'' in the class definition. I, however, have a very old compiler, so I have to find a different solution. This solution is, yet again, one of adding another level of indirection. I can have a pointer to an ``fstream.'' When in a ``const'' member function, the pointer is also ``const'', but not what it points to (there's a difference between a constant pointer, and a pointer to a constant.) The only reasonable way to achieve this is to store the stream object on the heap, and in doing this I introduce a possible danger; what if I forget to delete the pointer? Sure, I'll delete it in the destructor, but what if an exception is thrown already in the constructor, then the destructor will never execute (since no object has been created that must be destroyed.)

Do you remember the ``thing to think of until this month?'' The clues were, destructor, pointer and delete. Thought of anything? What about this extremely simple class template?


  template <class T>
  class ptr
  {
  public:
    ptr(T* pt);
    ~ptr();

    T& operator*() const;
  private:
    ptr(const ptr<T>&); // we don't want copying
    ptr<T>& operator=(const ptr<T>&); // nor assignment

    T* p;
  };

  template <class T>
  ptr<T>::ptr(T* pt)
    : p(pt)
  {
  }

  template <class T>
  ptr<T>::~ptr()
  {
    delete p;
  }

  template <class T>
  T& ptr<T>::operator*() const
  {
    return *p;
  }

This is probably the simplest possible of the family known as ``smart pointers.'' I'll probably devote a whole article exclusively for these some time. Whenever an object of this type is destroyed, whatever it points to is deleted. The only thing we have to keep in mind when using it, is to make sure that whatever we feed it is allocated on heap (and is not an array) so it can be deleted with operator delete.

This solves our problem nicely. When this thing is a constant, the thing pointed to still isn't a constant (look at the return type for ``operator*,'' it's a ``T&,'' not a ``const T&.'') So, instead of using an ``fstream'' member variable called ``stream,'' let's use a ``ptr<stream>'' member named ``pstream.'' With this change, ``readElement'' must be slightly rewritten:


  template <class T>
  T FileArray<T>::readElement(size_t index) const
  {
    (*pstream).seekg(sizeof(max_size)+index*sizeof(T));
    // what if seek fails?

    T t;
    (*pstream).read((char*)&t, sizeof(t));
    // what if read fails?

    return t;
  }

I bet the change wasn't too horrifying.


  template <class T>
  void FileArray<T>::storeElement(size_t index,
                                  const T& elem)
  {
    (*pstream).seekp(sizeof(max_size)+index*sizeof(T),
                     ios::beg);
    // what if seek fails?

    (*pstream).write((char*)&elem, sizeof(elem));
    // what if write failed?
  }

Now for the constructors:


  template <class T>
  FileArray<T>::FileArray(const char* name, size_t size)
    : pstream(new fstream(name, ios::in|ios::out|ios::binary)),
      max_size(size)
  {
    // what if the file could not be opened?

    // store the size on file.
    (*pstream).write((const char*)&max_size,
                     sizeof(max_size));
    // what if write failed?

    // We want to write a value (any value) at the end
    // to make sure there is enough space on disk.

    T t;
    storeElement(max_size-1,t);
    // What if this fails?
  }

  template <class T>
  FileArray<T>::FileArray(const char* name)
    : pstream(new fstream(name, ios::in|ios::out|ios::binary)),
      max_size(0)
  {
    // get the size from file.
    (*pstream).read((char*)&max_size,
                    sizeof(max_size));
    // what if read fails or max_size == 0?
    // How do we know the file is even an array?
  }

The access members:

  template <class T>
  T FileArray<T>::operator[](size_t size) const
  {
    // what if size >= max_size?
    return readElement(size);
    // What if read failed because of a disk error?
  }

  template <class T>
  FileArrayProxy<T> FileArray<T>::operator[](size_t size)
  {
    // what if size >= max_size?
    return FileArrayProxy<T>(*this , size);
  }

Well, this wasn't too much work, but then, as can be seen by the comments, there's absolutely no error handling here. I've left out the ``size'' member function, since its implementation is trivial.

Next in line is ``FileArrayProxy<T>.''


  template <class T>
  class FileArrayProxy
  {
  public:
    // copy constructor generated by compiler
    operator T() const;
    FileArrayProxy<T>& operator=(const T& t);
    FileArrayProxy<T>&
      operator=(const FileArrayProxy<T>& p);
    // read from one array and write to the other.
  private:
    FileArrayProxy(FileArray<T>& f, size_t i);

    size_t index;
    FileArray<T>& fa;

    friend class FileArray<T>;
  };

The copy constructor is needed, since the return value must be copied (return from ``FileArray<T>::operator[],'') and it must be public for this to succeed. The one that the compiler generates for us, which just copies all member variables, will do just fine. The compiler doesn't generate a default constructor (one which accepts no parameters,) since we have explicitly defined a contructor. The assignment operator is necessary, however. Sure, the compiler will try to generate one for us if we don't, but it will fail, since references (``fa'') can't be rebound. Note, however, that if we instead of a reference had used a pointer, it would succeed, but the result would *NOT* be what we want. What it would do is to copy the member variables, but what we want to do is to read data from one array and write it to another.

Now for the implementation:


   template <class T>
   FileArrayProxy<T>::FileArrayProxy(FileArray<T>& f,
                                     size_t i)
     : index(i),
       fa(f)
   {
   }
 
   template <class T>
   FileArrayProxy<T>::operator T() const
   {
     return fa.readElement(index);
   }
 
   template <class T>
   FileArrayProxy<T>&
   FileArrayProxy<T>::operator=(const T& t)
   {
     fa.storeElement(index,t);
     return *this;
   }
 
   template <class T>
   FileArrayProxy<T>& FileArrayProxy<T>::operator=(
     const FileArrayProxy<T>& p
   )
   {
     fa.storeElement(index,p);
     return *this;
   }
 
 #endif // FARRAY_HPP
 
That was it. Can you see what happens with the proxy? Let's analyze a small code snippet:

  1 FileArray<int> arr("file",10);
  2 arr[2]=0;
  3 int x=arr[2];
  4 arr[0]=arr[2];

On line two, ``arr.operator[](2)'' is called, which creates a ``FileArrayProxy<int>'' from ``arr'' with the index 2. The object, which is a temporary and does not have a name, has as its member ``fa'' a reference to ``arr'', and as its member ``index'' the value 2. On this temporary object, ``operator=(int)'' is executed. This operator in turn calls ``fa.storeElement(index, t),'' where ``index'' is still 2 and the value of ``t'' is 0. Thus, ``arr[2]=0'' ends up as ``arr.storeElement(2,0)''. On line 3, a similar proxy is created through the call to ``operator[](2)'' This time, however, the ``operator int() const'' is called. This member function in turn calls ``fa.readElement(2)'' and returns its value, thus ``int x=arr[2]'' translates to ``int x=arr.readElement(2).'' On line 4, finally, ``arr[0]=arr[2]'' creates two temporary proxies, one referring to index 0, and one to index 2. The assignment operator is called, which in turn calls ``fa.storeElement(0,p)'', where p is the temporary proxy referring to element 2. Since ``storeElement'' wants an ``int,'' ``p.operator int() const'' is called, which calls ``arr.readElement(2).'' In other words ``arr[0] = arr[2]'' generates the code ``arr.storeElement(0, arr.readElement(2)).''

As you can see, the proxies don't add any new functionality, they're just syntactic sugar, albeit very useful. With them we can treat our file arrays very much like any kind of array. There's one thing we cannot do:


  int* p = &arr[2];
  int& x = arr[3];
  *p=2;
  x=5;

With ordinary arrays, the above would be legal and have well defined semantics, assigning arr[2] the value 2, and arr[3] the value 5. With our file array we cannot do this, but unfortunately the compiler does not prevent it (a decent compiler will warn that we're binding a constant or pointer to a temporary.) We'll mend that hole next month (think about how) and also add iterators, which will allow us to use the file arrays almost exactly like real ones.

In memory data formatting

One often faced problem is that of converting strings representing some data to that data, or vice versa. With the aid of ``istrstream'', ``ostrstream'' and ``strstream'', this is easy. For example, say we have a string containing digits, and want those digits as an integer, the thing to do is to create an ``istrstream'' object from the string. An example will explain:


  char* s = "23542";
  istrstream is(s);
  int x;
  is >> x;

After executing this snippet, ``x'' will have the value 23542. ``istrstream'' isn't much more exciting than that. ``ostrstream'' on the other hand is more exciting. There are two alternative uses for ``ostrstream.'' One where you have an array you want to store data in, and one where you want the ``ostrstream'' to create it for you, as needed (usually because you have no idea what size the buffer must have.) The former usage is like this:


  char buffer[24];
  ostrstream os(buffer, sizeof(buffer));
  double x=23.34;
  os << "x=" << x << ends;

The variable ``buffer'' will contain the string ``x=23.34'' after this snippet. The stream manipulator ``ends'' zero terminates the buffer. Zero termination is not done by default, since the stream cannot know where to put it, and besides you might not always want it.

The other variant, where you don't know how large a buffer you will need, is generally more useful (I think.)


  ostrstream os;
  double x=23.34, y=34.45;
  os << x << '*' << y << '=' << x*y << ends;
  const char* p = os.str();
  const size_t length=os.pcount();

  // work with p and length.
  os.freeze(0); // release the memory.

I think the example pretty much shows what this kind of usage does. The member function ``str'' returns a pointer to the internal buffer (which is then frozen, that is, the stream guarantees that it will not deallocate the buffer, nor overwrite it. Attempts to alter the stream while frozen, will fail.) ``pcount'' returns the number of characters stored in the buffer. Last ``freeze'' can either freeze the buffer, or ``unfreeze'' it. The latter is done by giving it a parameter with the value 0. I find this interface to be unfortunate. It's so easy to forget to release the buffer (by simply forgetting to call ``os.freeze(0)'') and that leads to a memory leak.

``strstream'' finally, is just like ``fstream'' the combined read/write stream.

The string streams can be found in the header <strstream.h> (or for some compilers <strstrea.h>.)

Standards update

With the C++ standard, a lot of things have changed regarding streams. As I mentioned already last month, the headers are actually <iostream> and <fstream>, and the names std::istream, std::ostream, etc. The streams are templatized too, which both makes life easier and not. The underlying type for std::ostream is:


  std::basic_ostream<class charT,
                     class traits=std::char_traits<charT> >

``charT'' is the basic type for the stream. For ``ostream'' this is ``char'' (ostream is actually a typedef.) There's another typedef, ``std::wostream'', where the underlying type is ``wchar_t'', which on most systems probably will be 16-bit Unicode. The class template ``char_traits'' is a traits class which holds the type used for EOF, the value of EOF, and some other house keeping things.

Why the standard has removed the file stream open modes ios::create and ios::nocreate is beyond me, as they're extremely useful.

Casting is ugly, and it's hard to see in large code blocks. There are four new cast operators, that are highly visible, in the standard. They're (in approximate order of increasing danger,) dynamic_cast<T>, static_cast<T>, const_cast<T> and reinterpret_cast<T>. In the binary streaming seen in this article, reinterpret_cast<T> would be used, as a way of saying, ``Yeah, I know I'm violating type safety, but hey, I know what I'm doing, OK?'' The good thing about it is that it's so visible that anyone doubting it can easily spot the dangerous lines and have a careful look. The syntax is: os.write(reinterpret_cast<const char*>(&variable), sizeof(variable));

Finally, the generally useful strstreams has been replaced by ``std::istringstream'', ``std::ostringstream'' and ``std::stringstream'' (plus wide variants, std::wistringstream, etc.) defined in the header <sstream>. They do not operate on ``char*'', but on strings (there is a string class, or again, rather a string class template, where the most important template parameter is the underlying character.) ``std::ostringstream'' does not suffer from the freeze problem that ``ostrstream'' does.

Recap

The news this month were:

  • streams dealing with files, or in-memory formatting, are used just the same way as the familiar ``cout'' and ``cin,'' which saves both learning and coding (the already written ``operator<<'' and ``operator>>'' can be used for all kinds of streams already.)
  • streams can be used for binary, unformatted I/O too. This normally doesn't make sense for ``cout'' and ``cin'' or in-memory formatting (as the name implies,) but it's often useful when dealing with files.
  • It is possible to move around in streams, at least file streams and in-memory formatting streams. It's generally not possible to move around in ``cin'' and ``cout.''
  • proxy classes can be used to differentiate read and write operations for ``operator[]'' (the construction can of course be used elsewhere too, but it's most useful in this case.)
  • friends break encapsulation in a way that, when done right, strengthens encapsulation.
  • there's a difference between logical const and bitwise const, but the C++ compiler doesn't know and always assumes bitwise const.
  • truly simple smart pointers can save some memory management house keeping, and also be used as a work around for compilers lacking ``mutable'' (i.e. the way of declaring a variable as non-const for const members, in other words, how to differentiate between logical and bitwise const.)
  • streams can be used also for in-memory formatting of data.

Exercises

  • Improve the file array such that it accepts a ``stream&'' instead of a file name, and allows for several arrays in the same file.
  • Improve the proxy such that ``int& x=arr[2]'' and ``int* p=&arr[1]'' becomes illegal.
  • Add a constructor to the array that accepts only a ``size_t'' describing the size of the array, which creates a temporary file and removes it in its destructor.
  • What happens if we instantiate ``FileArray'' with a user defined type? Is it always desireable? If not, what is desireable? If you cannot define what's desireable, how can instantiation with user defined types be banned?
  • How can you, using the stream interface, calculate the size of a file?

Coming up

Next month will be devoted to improving the ``FileArray.'' We'll have iterators, allow arbitrary types, add error handling and more. I assume I won't need to tell you that it'll be possible to use the ``FileArray,'' just as ordinary arrays with generic programming, i.e. we can have the exact same source code for dealing with both!