Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Why iostreams?

You may wonder what’s wrong with the good old C library. And why not “wrap” the C library in a class and be done with it? Indeed, there are situations when this is the perfect thing to do, when you want to make a C library a bit safer and easier to use. For example, suppose you want to make sure a stdio file is always safely opened and properly closed, without relying on the user to remember to call the close( ) function:

//: C18:FileClass.h
// Stdio files wrapped
#ifndef FILECLAS_H
#define FILECLAS_H
#include <cstdio>

class FileClass {
  std::FILE* f;
public:
  FileClass(const char* fname, const char* mode="r");
  ~FileClass();
  std::FILE* fp();
};
#endif // FILECLAS_H ///:~

In C when you perform file I/O, you work with a naked pointer to a FILE struct, but this class wraps around the pointer and guarantees it is properly initialized and cleaned up using the constructor and destructor. The second constructor argument is the file mode, which defaults to “r” for “read.”

To fetch the value of the pointer to use in the file I/O functions, you use the fp( ) access function. Here are the member function definitions:

//: C18:FileClass.cpp {O}
// Implementation
#include "FileClass.h"
#include <cstdlib>
using namespace std;

FileClass::FileClass(const char* fname, const char* mode){
  f = fopen(fname, mode);
  if(f == NULL) {
    printf("%s: file not found\n", fname);
    exit(1);
  }
}

FileClass::~FileClass() { fclose(f); }
FILE* FileClass::fp() { return f; } ///:~

The constructor calls fopen( ),as you would normally do, but it also checks to ensure the result isn’t zero, which indicates a failure upon opening the file. If there’s a failure, the name of the file is printed and exit( ) is called.

The destructor closes the file, and the access function fp( )returns f. Here’s a simple example using class FileClass :

//: C18:FileClassTest.cpp
//{L} FileClass
// Testing class File
#include "FileClass.h"
#include "../require.h"
using namespace std;

int main(int argc, char* argv[]) {
  requireArgs(argc, 1);
  FileClass f(argv[1]); // Opens and tests
  const int bsize = 100;
  char buf[bsize];
  while(fgets(buf, bsize, f.fp()))
    puts(buf);
} // File automatically closed by destructor
///:~

You create the FileClass object and use it in normal C file I/O function calls by calling fp( ). When you’re done with it, just forget about it, and the file is closed by the destructor at the end of the scope.

True wrapping

Even though the FILE pointer is private, it isn’t particularly safe because fp( ) retrieves it. The only effect seems to be guaranteed initialization and cleanup, so why not make it public, or use a struct instead? Notice that while you can get a copy of f using fp( ), you cannot assign to f – that’s completely under the control of the class. Of course, after capturing the pointer returned by fp( ), the client programmer can still assign to the structure elements, so the safety is in guaranteeing a valid FILE pointer rather than proper contents of the structure.

If you want complete safety, you have to prevent the user from direct access to the FILE pointer. This means some version of all the normal file I/O functions will have to show up as class members, so everything you can do with the C approach is available in the C++ class:

//: C18:Fullwrap.h
// Completely hidden file IO
#ifndef FULLWRAP_H
#define FULLWRAP_H

class File {
  std::FILE* f;
  std::FILE* F(); // Produces checked pointer to f
public:
  File(); // Create object but don't open file
  File(const char* path,
       const char* mode = "r");
  ~File();
  int open(const char* path,
           const char* mode = "r");
  int reopen(const char* path,
             const char* mode);
  int getc();
  int ungetc(int c);
  int putc(int c);
  int puts(const char* s);
  char* gets(char* s, int n);
  int printf(const char* format, ...);
  size_t read(void* ptr, size_t size,
              size_t n);
  size_t write(const void* ptr,
                size_t size, size_t n);
  int eof();
  int close();
  int flush();
  int seek(long offset, int whence);
  int getpos(fpos_t* pos);
  int setpos(const fpos_t* pos);
  long tell();
  void rewind();
  void setbuf(char* buf);
  int setvbuf(char* buf, int type, size_t sz);
  int error();
  void clearErr();
};
#endif // FULLWRAP_H ///:~

This class contains almost all the file I/O functions from cstdio. vfprintf( ) is missing; it is used to implement the printf( ) member function.

File has the same constructor as in the previous example, and it also has a default constructor. The default constructor is important if you want to create an array of File objects or use a File object as a member of another class where the initialization doesn’t happen in the constructor (but sometime after the enclosing object is created).

The default constructor sets the private FILE pointer f to zero. But now, before any reference to f, its value must be checked to ensure it isn’t zero. This is accomplished with the last member function in the class, F( ), which is private because it is intended to be used only by other member functions. (We don’t want to give the user direct access to the FILE structure in this class.) [50]

This is not a terrible solution by any means. It’s quite functional, and you could imagine making similar classes for standard (console) I/O and for in-core formatting (reading/writing a piece of memory rather than a file or the console).

The big stumbling block is the runtime interpreter used for the variable-argument list functions. This is the code that parses through your format string at runtime and grabs and interprets arguments from the variable argument list. It’s a problem for four reasons.

  1. Even if you use only a fraction of the functionality of the interpreter, the whole thing gets loaded. So if you say:

    printf("%c", 'x');

    you’ll get the whole package, including the parts that print out floating-point numbers and strings. There’s no option for reducing the amount of space used by the program.

  2. Because the interpretation happens at runtime there’s a performance overhead you can’t get rid of. It’s frustrating because all the information is there in the format string at compile time, but it’s not evaluated until runtime. However, if you could parse the arguments in the format string at compile time you could make hard function calls that have the potential to be much faster than a runtime interpreter (although the printf( ) family of functions is usually quite well optimized).
  3. A worse problem occurs because the evaluation of the format string doesn’t happen until runtime: there can be no compile-time error checkin
g. You’re probably very familiar with this problem if you’ve tried to find bugs that came from using the wrong number or type of arguments in a printf( ) statement. C++ makes a big deal out of compile-time error checking to find errors early and make your life easier. It seems a shame to throw it away for an I/O library, especially because I/O is used a lot.

  1. For C++, the most important problem is that the printf( ) family of functions is not particularly extensibl
e. They’re really designed to handle the four basic data types in C (char, int, float, double and their variations). You might think that every time you add a new class, you could add an overloaded printf( ) and scanf( ) function (and their variants for files and strings) but remember, overloaded functions must have different types in their argument lists and the printf( ) family hides its type information in the format string and in the variable argument list. For a language like C++, whose goal is to be able to easily add new data types, this is an ungainly restriction.


[50] The implementation and test files for FULLWRAP are available in the freely distributed source code for this book. See preface for details.

Contents | Prev | Next


Contact: webmaster@codeguru.com
CodeGuru - the website for developers.