Subtyping
Now suppose you want to create a type of
ifstream object that not only opens a file but
also keeps track of the name of the file. You can use composition and embed both
an ifstream and a string into the new class:
//: C14:FName1.cpp
// An fstream with a file name
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
class FName1 {
ifstream file;
string fileName;
bool named;
public:
FName1() : named(false) {}
FName1(const string& fname)
: fileName(fname), file(fname.c_str()) {
assure(file, fileName);
named = true;
}
string name() const { return fileName; }
void name(const string& newName) {
if(named) return; // Don't overwrite
fileName = newName;
named = true;
}
operator ifstream&() { return file; }
};
int main() {
FName1 file("FName1.cpp");
cout << file.name() << endl;
// Error: close() not a member:
//! file.close();
} ///:~
There’s a problem here, however. An
attempt is made to allow the use of the FName1 object anywhere an
ifstream object is used by including an automatic type conversion
operator from FName1 to an ifstream&. But in main, the
line
file.close();
will not compile because automatic type
conversion happens only in function calls, not during member selection. So this
approach won’t work.
A second approach is to add the
definition of close( ) to FName1:
void close() { file.close(); }
This will work if there are only a few
functions you want to bring through from the ifstream class. In that case
you’re only using part of the class, and composition
is appropriate.
But what if you want everything in the
class to come through? This is called subtyping because you’re
making a new type from an existing type, and you want your new type to have
exactly the same interface as the existing type (plus any other member functions
you want to add), so you can use it everywhere you’d use the existing
type. This is where inheritance is essential. You can see that subtyping solves
the problem in the preceding example perfectly:
//: C14:FName2.cpp
// Subtyping solves the problem
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
class FName2 : public ifstream {
string fileName;
bool named;
public:
FName2() : named(false) {}
FName2(const string& fname)
: ifstream(fname.c_str()), fileName(fname) {
assure(*this, fileName);
named = true;
}
string name() const { return fileName; }
void name(const string& newName) {
if(named) return; // Don't overwrite
fileName = newName;
named = true;
}
};
int main() {
FName2 file("FName2.cpp");
assure(file, "FName2.cpp");
cout << "name: " << file.name() << endl;
string s;
getline(file, s); // These work too!
file.seekg(-200, ios::end);
file.close();
} ///:~
Now any member function available for an
ifstream object is available for an FName2 object. You can also
see that non-member functions like getline( ) that expect an
ifstream can also work with an FName2. That’s because
an FName2 is a type of ifstream; it doesn’t simply
contain one. This is a very important issue that will be explored at the end of
this chapter and in the next
one.