Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
You might wonder what s wrong with the good old C library.
Why not wrap the C library in a class and be done with it? Sometimes this is a
fine solution. For example, suppose you want to make sure that the file
represented by a stdio FILE pointer is always safely opened and
properly closed without having to rely on the user to remember to call the close( )
function. The following program is such an attempt:
//: C04:FileClass.h
// stdio files wrapped.
#ifndef FILECLASS_H
#define FILECLASS_H
#include <cstdio>
#include <stdexcept>
class FileClass {
std::FILE* f;
public:
struct FileClassError : std::runtime_error {
FileClassError(const char* msg)
: std::runtime_error(msg) {}
};
FileClass(const char* fname, const char* mode =
"r");
~FileClass();
std::FILE* fp();
};
#endif // FILECLASS_H ///:~
When you perform file I/O in C, 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:
//: C04:FileClass.cpp {O}
// FileClass Implementation.
#include "FileClass.h"
#include <cstdlib>
#include <cstdio>
using namespace std;
FileClass::FileClass(const char* fname, const char*
mode) {
if((f = fopen(fname, mode)) == 0)
throw FileClassError("Error opening
file");
}
FileClass::~FileClass() { fclose(f); }
FILE* FileClass::fp() { return
f; } ///:~
The constructor calls fopen( ), as you would
normally do, but it also ensures that the result isn t zero, which indicates a
failure upon opening the file. If the file does not open as expected, an exception
is thrown.
The destructor closes the file, and the access function fp( )
returns f. Here s a simple example using FileClass:
//: C04:FileClassTest.cpp
//{L} FileClass
#include <cstdlib>
#include <iostream>
#include "FileClass.h"
using namespace std;
int main() {
try {
FileClass f("FileClassTest.cpp");
const int BSIZE = 100;
char buf[BSIZE];
while(fgets(buf, BSIZE, f.fp()))
fputs(buf, stdout);
} catch(FileClass::FileClassError& e) {
cout << e.what() << endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
} // 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; the file is closed by the destructor at the end of
its scope.
Even though the FILE pointer is private, it isn t
particularly safe because fp( ) retrieves it. Since the only effect
seems to be guaranteed initialization and cleanup, 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. After capturing the pointer returned by fp( ),
the client programmer can still assign to the structure elements or even close
it, so the safety is in guaranteeing a valid FILE pointer rather than
proper contents of the structure.
If you want complete safety, you must prevent the user from
directly accessing the FILE pointer. Some version of all the normal file
I/O functions must show up as class members so that everything you can do with
the C approach is available in the C++ class:
//: C04:Fullwrap.h
// Completely hidden file IO.
#ifndef FULLWRAP_H
#define FULLWRAP_H
#include <cstddef>
#include <cstdio>
#undef getc
#undef putc
#undef ungetc
using std::size_t;
using std::fpos_t;
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 implements 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 some time 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 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 underlying FILE
structure in this class.)
This approach 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 stumbling block is the runtime interpreter used for the
variable argument list functions. This is the code that parses 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 into your executable. So if you say printf("%c",
'x');, you ll get the whole package, including the parts that print
floating-point numbers and strings. There s no standard option for reducing the
amount of space used by the program.
2. Because the interpretation happens at runtime, you can t get rid
of a performance overhead. 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 direct 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. Because the format string is not evaluated until runtime, there
can be no compile-time error checking. You re probably 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 type safety away for an I/O library, especially since
I/O is used a lot.
4. For C++, the most crucial problem is that the printf( )
family of functions is not particularly extensible. They re really designed to
handle only the basic data types in C (char, int, float, double,
wchar_t, char*, wchar_t*, and void*) and their
variations. You might think that every time you add a new class, you could add
overloaded printf( ) and scanf( ) functions (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 such as C++, whose goal is to be able to easily add new
data types, this is an unacceptable restriction.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |