Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
As we stated earlier, C++ provides only implementation
inheritance, meaning that you always inherit everything from your base
classes. This can be good because it frees you from having to implement everything
in the derived class, as we had to do with the interface inheritance examples
earlier. A common use of multiple inheritance involves using mixin classes, which are classes that exist to add capabilities to other classes through
inheritance. Mixin classes are not intended to be instantiated by themselves.
As an example, suppose we are clients of a class that
supports access to a database. In this scenario, you only have a header file
available part of the point here is that you don t have access to the source
code for the implementation. For illustration, assume the following
implementation of a Database class:
//: C09:Database.h
// A prototypical resource class.
#ifndef DATABASE_H
#define DATABASE_H
#include <iostream>
#include <stdexcept>
#include <string>
struct DatabaseError : std::runtime_error {
DatabaseError(const std::string& msg)
: std::runtime_error(msg) {}
};
class Database {
std::string dbid;
public:
Database(const std::string& dbStr) : dbid(dbStr)
{}
virtual ~Database() {}
void open() throw(DatabaseError) {
std::cout << "Connected to "
<< dbid << std::endl;
}
void close() {
std::cout << dbid << "
closed" << std::endl;
}
// Other database functions...
};
#endif // DATABASE_H ///:~
We re leaving out actual database functionality (storing,
retrieving, and so on), but that s not important here. Using this class
requires a database connection string and that you call Database::open( )
to connect and Database::close( ) to disconnect:
//: C09:UseDatabase.cpp
#include "Database.h"
int main() {
Database db("MyDatabase");
db.open();
// Use other db functions...
db.close();
}
/* Output:
connected to MyDatabase
MyDatabase closed
*/ ///:~
In a typical client-server situation, a client will have
multiple objects sharing a connection to a database. It is important that the
database eventually be closed, but only after access to it is no longer
required. It is common to encapsulate this behavior through a class that tracks
the number of client entities using the database connection and to
automatically terminate the connection when that count goes to zero. To add
reference counting to the Database class, we use multiple inheritance to
mix a class named Countable into the Database class to create a
new class, DBConnection. Here s the Countable mixin class:
//: C09:Countable.h
// A "mixin" class.
#ifndef COUNTABLE_H
#define COUNTABLE_H
#include <cassert>
class Countable {
long count;
protected:
Countable() { count = 0; }
virtual ~Countable() { assert(count == 0); }
public:
long attach() { return ++count; }
long detach() {
return (--count > 0) ? count : (delete this, 0);
}
long refCount() const { return count; }
};
#endif // COUNTABLE_H ///:~
It is evident that this is not a standalone class because
its constructor is protected; it requires a friend or a derived class to
use it. It is important that the destructor is virtual, because it is called
only from the delete this statement in detach( ), and we
want derived objects to be properly destroyed.
The DBConnection class inherits both Database
and Countable and provides a static create( ) function that
initializes its Countable subobject. This is an example of the Factory
Method design pattern, discussed in the next chapter:
//: C09:DBConnection.h
// Uses a "mixin" class.
#ifndef DBCONNECTION_H
#define DBCONNECTION_H
#include <cassert>
#include <string>
#include "Countable.h"
#include "Database.h"
using std::string;
class DBConnection : public Database, public Countable
{
DBConnection(const DBConnection&); // Disallow
copy
DBConnection& operator=(const DBConnection&);
protected:
DBConnection(const string& dbStr)
throw(DatabaseError)
: Database(dbStr) { open(); }
~DBConnection() { close(); }
public:
static DBConnection*
create(const string& dbStr) throw(DatabaseError)
{
DBConnection* con = new DBConnection(dbStr);
con->attach();
assert(con->refCount() == 1);
return con;
}
// Other added functionality as desired...
};
#endif // DBCONNECTION_H ///:~
We now have a reference-counted database connection without
modifying the Database class, and we can safely assume that it will not
be surreptitiously terminated. The opening and closing is done using the Resource Acquisition Is Initialization (RAII) idiom mentioned in Chapter 1 via the DBConnection
constructor and destructor. This makes the DBConnection easy to use:
//: C09:UseDatabase2.cpp
// Tests the Countable "mixin" class.
#include <cassert>
#include "DBConnection.h"
class DBClient {
DBConnection* db;
public:
DBClient(DBConnection* dbCon) {
db = dbCon;
db->attach();
}
~DBClient() { db->detach(); }
// Other database requests using db
};
int main() {
DBConnection* db =
DBConnection::create("MyDatabase");
assert(db->refCount() == 1);
DBClient c1(db);
assert(db->refCount() == 2);
DBClient c2(db);
assert(db->refCount() == 3);
// Use database, then release attach from original
create
db->detach();
assert(db->refCount() == 2);
} ///:~
The call to DBConnection::create( ) calls attach( ),
so when we re finished, we must explicitly call detach( ) to
release the original hold on the connection. Note that the DBClient
class also uses RAII to manage its use of the connection. When the program
terminates, the destructors for the two DBClient objects will decrement
the reference count (by calling detach( ), which DBConnection
inherited from Countable), and the database connection will be closed (because
of Countable s virtual destructor) when the count reaches zero after the
object c1 is destroyed.
A template approach is commonly used for mixin inheritance,
allowing the user to specify at compile time which flavor of mixin is desired.
This way you can use different reference-counting approaches without explicitly
defining DBConnection twice. Here s how it s done:
//: C09:DBConnection2.h
// A parameterized mixin.
#ifndef DBCONNECTION2_H
#define DBCONNECTION2_H
#include <cassert>
#include <string>
#include "Database.h"
using std::string;
template<class Counter>
class DBConnection : public Database, public Counter {
DBConnection(const DBConnection&); // Disallow
copy
DBConnection& operator=(const DBConnection&);
protected:
DBConnection(const string& dbStr)
throw(DatabaseError)
: Database(dbStr) { open(); }
~DBConnection() { close(); }
public:
static DBConnection* create(const string& dbStr)
throw(DatabaseError) {
DBConnection* con = new DBConnection(dbStr);
con->attach();
assert(con->refCount() == 1);
return con;
}
// Other added functionality as desired...
};
#endif // DBCONNECTION2_H ///:~
The only change here is the template prefix to the class
definition (and renaming Countable to Counter for clarity). We
could also make the database class a template parameter (had we multiple
database access classes to choose from), but it is not a mixin since it is a
standalone class. The following example uses the original Countable as
the Counter mixin type, but we could use any type that implements the
appropriate interface (attach( ), detach( ), and so
on):
//: C09:UseDatabase3.cpp
// Tests a parameterized "mixin" class.
#include <cassert>
#include "Countable.h"
#include "DBConnection2.h"
class DBClient {
DBConnection<Countable>* db;
public:
DBClient(DBConnection<Countable>* dbCon) {
db = dbCon;
db->attach();
}
~DBClient() { db->detach(); }
};
int main() {
DBConnection<Countable>* db =
DBConnection<Countable>::create("MyDatabase");
assert(db->refCount() == 1);
DBClient c1(db);
assert(db->refCount() == 2);
DBClient c2(db);
assert(db->refCount() == 3);
db->detach();
assert(db->refCount() == 2);
} ///:~
The general pattern for multiple parameterized mixins is
simply
template<class Mixin1, class Mixin2, , class
MixinK>
class Subject : public Mixin1,
public Mixin2,
public MixinK { };
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |