Выбрать главу

#include <sstream>

#include <string>

using namespace std;

class Able {

  int myData;

public:

  Able(int x) {

    myData = x;

  }

  void print(ostream& os) const {

    os << myData;

  }

  int toInt() const {

    return myData;

  }

  string toString() const {

    ostringstream os;

    os << myData;

    return os.str();

  }

};

template<class Printable>

void testPrintable(const Printable& p) {

  p.print(cout);

  cout << endl;

}

template<class Intable>

void testIntable(const Intable& n) {

  int i = n.toInt() + 1;

  cout << i << endl;

}

template<class Stringable>

void testStringable(const Stringable& s) {

  string buf = s.toString() + "th";

  cout << buf << endl;

}

int main() {

  Able a(7);

  testPrintable(a);

  testIntable(a);

  testStringable(a);

} ///:~

The names Printable, Intable, and Stringable are now just template parameters that assume the existence of the operations indicated in their respective contexts. Some people are more comfortable with the first version, because the type names guarantee by inheritance that the expected interfaces are implemented. Others are content with the fact that if the operations required by the test functions are not satisfied by their template type arguments, the error is still caught at compile time. The latter approach is technically a "weaker" form of type checking than the former (inheritance) approach, but the effect on the programmer (and the program) is the same. This is one form of weak typing that is acceptable to many of today’s C++ programmers.

Implementation inheritance

As we stated earlier, C++ provides only implementation inheritance, meaning that you inherit everything from all your base classes. This can be a good thing, of course, 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 not intended to be instantiated independently, but exist to add capabilities to other classes through inheritance.

As an example, suppose we are clients of a class that supports access to a database. We will likely only have a header file available (which is part of the point we are about to make), but for illustration, assume the following, simple implementation of a Database class:

//: C09:Database.h

// A prototypical resource class

#ifndef DATABASE_H

#define DATABASE_H

#include <iostream>

#include <stdexcept>

#include <string>

using std::cout;

using std::string;

using std::runtime_error;

struct DatabaseError : runtime_error {

  DatabaseError(const string& msg) : runtime_error(msg)

  {}

};

class Database {

public:

  Database(const string& dbStr) : dbid(dbStr) {}

  virtual ~Database(){}

  void open() throw(DatabaseError) {

    cout << "connected to " << dbid << '\n';

  }

  void close() {

    cout << dbid << " closed\n";

  }

  //Other database functions...

private:

  string dbid;

};

#endif ///:~

We’re leaving out actual database functionality (storing, retrieving, and so on), but that’s actually 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 create a mixin class named Countable and mix it into the Database class by creating a new class, DBConnection, through multiple inheritance. Here’s the Countable mixin class:

//: C09:Countable.h

// A "mixin" class

#ifndef COUNTABLE_H

#define COUNTABLE_H

#include <cassert>

class Countable {

public:

  long attach() { return ++count; }

  long detach() {

    return (--count > 0) ? count : (delete this, 0);

  }

  long refCount() const { return count; }

protected:

  Countable() { count = 0; }

  virtual ~Countable() { assert(count == 0); }

private:

  long count;

};

#endif ///:~

It is evident that this is not a standalone class because its constructor is protected; it therefore requires a friend or a derived class to use it. It is important that the destructor is virtual, of course, because it is called only from the delete this statement in detach( ), and we of course want derived objects to be completely destroyed.[107] The DBConnection class derives from 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 "Countable.h"

#include "Database.h"

#include <cassert>

#include <string>

using std::string;

class DBConnection : public Database, public Countable {

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...

protected:

  DBConnection(const string& dbStr) throw(DatabaseError)

  : Database(dbStr) {

вернуться

107

Even more importantly, we don’t want undefined behavior. It is an error for a base class not to have a virtual destructor.