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 <cstdlib>
#include "FileClass.h"
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:Fctest.cpp
//{L} FileClass
// Testing class File
#include "../require.h"
#include "FileClass.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
stdio.h.
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.)
[44] 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 run-time interpreter
used
for the variable-argument list functions. This is the code that parses through
your format string at run-time and grabs and interprets arguments from the
variable argument list. It’s a problem for four reasons.
- 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.
- Because
the interpretation happens at run-time 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
run-time. 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 run-time interpreter (although the
printf( )
family of functions is usually quite well optimized).
- A
worse problem occurs because the evaluation of the format string doesn’t
happen until run-time: 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.
- 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.
[44]
The implementation and test files for FULLWRAP are available in the freely
distributed source code for this book. See preface for details.
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru