Follow Techotopia on Twitter

On-line Guides
All Guides
eBook Store
iOS / Android
Linux for Beginners
Office Productivity
Linux Installation
Linux Security
Linux Utilities
Linux Virtualization
Linux Kernel
System/Network Admin
Programming
Scripting Languages
Development Tools
Web Development
GUI Toolkits/Desktop
Databases
Mail Systems
openSolaris
Eclipse Documentation
Techotopia.com
Virtuatopia.com
Answertopia.com

How To Guides
Virtualization
General System Admin
Linux Security
Linux Filesystems
Web Servers
Graphics & Desktop
PC Hardware
Windows
Problem Solutions
Privacy Policy

  




 

 

Thinking in C++ Vol 2 - Practical Programming
Prev Home Next

Why iostreams?

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

 
 
   Reproduced courtesy of Bruce Eckel, MindView, Inc. Design by Interspire