Читаем Thinking In C++. Volume 2: Practical Programming полностью

  Bottom(int i, int j, int k, int m)

  : Top(i), Left(0, j), Right(0, k) { w = m; }

  friend ostream&

  operator<<(ostream& os, const Bottom& b) {

    return os << b.x << ',' << b.y << ',' << b.z

      << ',' << b.w;

  }

};

int main() {

  Bottom b(1, 2, 3, 4);

  cout << sizeof b << endl;

  cout << b << endl;

  cout << static_cast(&b) << endl;

  Top* p = static_cast(&b);

  cout << *p << endl;

  cout << static_cast(p) << endl;

  cout << dynamic_cast(p) << endl;

} ///:~

Each virtual base of a given type refers to the same object, no matter where it appears in the hierarchy.[111] This means that when a Bottom object is instantiated, the object layout may look something like this:

The Left and Right subobjects each have a pointer (or some conceptual equivalent) to the shared Top subobject, and all references to that subobject in Left and Right member functions will go through those these pointers.[112] In this case, there is no ambiguity when upcasting from a Bottom to a Top object, since there is only one Top object to convert to.

The output of the previous program is as follows:

36

1,2,3,4

1245032

1

1245060

1245032

The addresses printed suggest that this particular implementation does indeed store the Top subobject at the end of the complete object (although it’s not really important where it goes). The result of a dynamic_cast to void* always resolves to the address of the complete object.

We made the Top destructor virtual so we could apply the dynamic_cast operator. If you remove that virtual destructor (and the dynamic_cast statement so the program will compile), the size of Bottom decreases to 24 bytes. That seems to be a decrease equivalent to the size of three pointers. What gives?

It’s important not to take these numbers too literally. Other compilers we use manage only to increase the size by four bytes when the virtual constructor is added. Not being compiler writers, we can’t tell you their secrets. We can tell you, however, that with multiple inheritance, a derived object must behave as if it has multiple VPTRs, one for each of its direct base classes that also have virtual functions. It’s as simple as that. Compilers can make whatever optimizations its authors can invent, but the behavior must be the same.

Certainly the strangest thing in the previous code is the initializer for Top in the Bottom constructor. Normally one doesn’t worry about initializing subobjects beyond direct base classes, since all classes take care of initializing their own bases. There are, however, multiple paths from Bottom to Top, so relying on the intermediate classes Left and Right to pass along the necessary initialization data results in an ambiguity (whose responsibility is it?)! For this reason, it is always the responsibility of the most derived class to initialize a virtual base. But what about the expressions in the Left and Right constructors that also initialize Top? They are certainly necessary when creating standalone Left or Right objects, but must be ignored when a Bottom object is created (hence the zeros in their initializers in the Bottom constructor—any values in those slots are ignored when the Left and Right constructors execute in the context of a Bottom object). The compiler takes care of all this for you, but it’s important to understand where the responsibility lies. Always make sure that all concrete (nonabstract) classes in a multiple inheritance hierarchy are aware of any virtual bases and initialize them appropriately.

These rules of responsibility apply not only to initialization but to all operations that span the class hierarchy. Consider the stream inserter in the previous code. We made the data protected so we could "cheat" and access inherited data in operator<<(ostream&, const Bottom&). It usually makes more sense to assign the work of printing each subobject to its corresponding class and have the derived class call its base class functions as needed. What would happen if we tried that with operator<<( ), as the following code illustrates?

//: C09:VirtualBase2.cpp

// Shows how not to implement operator<<

#include

using namespace std;

class Top {

  int x;

public:

  Top(int n) { x = n; }

  friend ostream&

  operator<<(ostream& os, const Top& t) {

    return os << t.x;

  }

};

class Left : virtual public Top {

  int y;

public:

  Left(int m, int n) : Top(m) { y = n; }

  friend ostream&

  operator<<(ostream& os, const Left& l) {

    return os << static_cast(l) << ',' << l.y;

  }

};

class Right : virtual public Top {

  int z;

public:

  Right(int m, int n) : Top(m) { z = n; }

  friend ostream&

  operator<<(ostream& os, const Right& r) {

    return os << static_cast(r) << ',' << r.z;

  }

};

class Bottom : public Left, public Right {

  int w;

public:

  Bottom(int i, int j, int k, int m)

  : Top(i), Left(0, j), Right(0, k) { w = m; }

  friend ostream&

Перейти на страницу:

Похожие книги

3ds Max 2008
3ds Max 2008

Одни уверены, что нет лучшего способа обучения 3ds Мах, чем прочитать хорошую книгу. Другие склоняются к тому, что эффективнее учиться у преподавателя, который показывает, что и как нужно делать. Данное издание объединяет оба подхода. Его цель – сделать освоение 3ds Мах 2008 максимально быстрым и результативным. Часто после изучения книги у читателя возникают вопросы, почему не получился тот или иной пример. Видеокурс – это гарантия, что такие вопросы не возникнут: ведь автор не только рассказывает, но и показывает, как нужно работать в 3ds Мах.В отличие от большинства интерактивных курсов, где работа в 3ds Мах иллюстрируется на кубиках-шариках, данный видеокурс полностью практический. Все приемы работы с инструментами 3ds Мах 2008 показаны на конкретных примерах, благодаря чему после просмотра курса читатель сможет самостоятельно выполнять даже сложные проекты.

Владимир Антонович Верстак , Владимир Верстак

Программирование, программы, базы данных / Программное обеспечение / Книги по IT