|














Am avut 371681 vizite de la lansarea siteului.

|
|
Inapoi
Inainte
Cuprins
virtual functions &
constructors
When an object containing virtual
functions is created, its VPTR must be initialized to
point to the proper VTABLE. This must be done before
there’s any possibility of calling a virtual function. As you might guess,
because the constructor has the job of bringing an object into existence, it is
also the constructor’s job to set up the VPTR. The compiler secretly
inserts code into the beginning of the constructor that initializes the VPTR.
And as described in Chapter 14, if you don’t explicitly create a
constructor for a class, the compiler will synthesize one for you. If the class
has virtual functions, the synthesized constructor will include the proper VPTR
initialization code. This has several implications.
The first concerns efficiency. The reason
for inline functions is
to reduce the calling overhead for small functions. If C++ didn’t provide
inline functions, the preprocessor might be used to create these
“macros.” However, the preprocessor has no concept of access or
classes, and therefore couldn’t be used to create member function macros.
In addition, with constructors that must have hidden code inserted by the
compiler, a preprocessor macro wouldn’t work at all.
You must be aware when hunting for
efficiency
holes that the compiler is inserting hidden code into your constructor function.
Not only must it initialize the VPTR, it must also check the value of
this (in case the operator new returns zero) and call
base-class constructors. Taken together, this code can impact what you thought
was a tiny inline function call. In particular, the size of the constructor may
overwhelm the savings you get from reduced function-call overhead. If you make a
lot of inline constructor calls, your code size can grow without any benefits in
speed.
Of course, you probably won’t make
all tiny constructors non-inline right away, because they’re much easier
to write as inlines. But when you’re tuning your code, remember to
consider removing the inline
constructors.
Order of constructor
calls
The second interesting facet of
constructors and virtual functions concerns the order of constructor calls and
the way virtual calls are made within constructors.
All
base-class constructors are always called in the constructor for an inherited
class. This makes sense because the constructor has a special job: to see that
the object is built properly. A derived class has access only to its own
members, and not those of the base class. Only the base-class constructor can
properly initialize its own elements. Therefore it’s essential that all
constructors get called; otherwise the entire object wouldn’t be
constructed properly. That’s why the compiler enforces a constructor call
for every portion of a derived class. It will call the default constructor if
you don’t explicitly call a base-class constructor in the constructor
initializer list. If there is no
default constructor, the
compiler will complain.
The order of the constructor calls is
important. When you inherit, you know all about the base class and can access
any public and protected members of the base class. This means you
must be able to assume that all the members of the base class are valid when
you’re in the derived class. In a normal member function, construction has
already taken place, so all the members of all parts of the object have been
built. Inside the constructor, however, you must be able to assume that all
members that you use have been built. The only way to guarantee this is for the
base-class constructor to be called first. Then when you’re in the
derived-class constructor, all the members you can access in the base class have
been initialized. “Knowing all members are valid” inside the
constructor is also the reason that, whenever possible, you should initialize
all member objects (that is, objects placed in the class using composition) in
the constructor initializer
list. If you follow this practice, you can assume that all base class members
and member objects of the current object have been
initialized.
Behavior of virtual functions inside
constructors
The hierarchy of constructor calls brings
up an interesting dilemma. What happens if you’re inside a constructor and
you call a virtual function? Inside an ordinary member function you can imagine
what will happen – the virtual call is resolved at runtime because the
object cannot know whether it belongs to the class the member function is in, or
some class derived from it. For consistency, you might think this is what should
happen inside constructors.
This is not the case. If you call a
virtual function inside a constructor, only the local version of the function is
used. That is, the virtual mechanism doesn’t work within the
constructor.
This behavior makes sense for two
reasons. Conceptually, the constructor’s job is to bring the object into
existence (which is hardly an ordinary feat). Inside any constructor, the object
may only be partially formed – you can only know that the base-class
objects have been initialized, but you cannot know which classes are inherited
from you. A virtual function call, however, reaches “forward” or
“outward” into the inheritance hierarchy. It calls a function in a
derived class. If you could do this inside a constructor, you’d be calling
a function that might manipulate members that hadn’t been initialized yet,
a sure recipe for disaster.
The second reason is a mechanical one.
When a constructor is called, one of the first things it does is initialize its
VPTR. However, it can only know that it is of the
“current” type – the type the constructor was written for. The
constructor code is completely ignorant of whether or not the object is in the
base of another class. When the compiler generates code for that constructor, it
generates code for a constructor of that class, not a base class and not a class
derived from it (because a class can’t know who inherits it). So the VPTR
it uses must be for the VTABLE of that class. The
VPTR remains initialized to that VTABLE for the rest of the object’s
lifetime unless this isn’t the last constructor call. If a
more-derived constructor is called afterwards, that constructor sets the VPTR to
its VTABLE, and so on, until the last constructor finishes. The state of
the VPTR is determined by the constructor that is called last. This is another
reason why the constructors are
called in order from base to most-derived.
But while all this series of constructor
calls is taking place, each constructor has set the VPTR to its own VTABLE. If
it uses the virtual mechanism for function calls, it will produce only a call
through its own VTABLE, not the most-derived VTABLE (as would be the case after
all the constructors were called). In addition, many compilers recognize
that a virtual function call is being made inside a constructor, and perform
early binding because they know that late-binding will produce a call only to
the local function. In either event, you won’t get the results you might
initially expect from a virtual function call inside a
constructor.
Destructors and virtual destructors
You cannot use the
virtual keyword with
constructors, but destructors
can and often must be virtual.
The constructor
has the special job of putting an object together piece-by-piece, first by
calling the base constructor, then the more derived constructors in order of
inheritance (it must also call member-object constructors along the way).
Similarly, the destructor has a special job: it must disassemble an object that
may belong to a hierarchy of classes. To do this, the compiler generates code
that calls all the destructors, but in the reverse order that they are
called by the constructor. That is, the destructor starts at the most-derived
class and works its way down to the base class. This is the safe and desirable
thing to do because the current destructor can always know that the base-class
members are alive and active. If you need to call a base-class member function
inside your destructor, it is safe to do so. Thus, the destructor can perform
its own cleanup, then call the next-down destructor, which will perform
its own cleanup, etc. Each destructor knows what
its class is derived from, but not what is derived from
it.
You should keep in mind that constructors
and destructors are the only places where this hierarchy of calls must happen
(and thus the proper hierarchy is automatically generated by the compiler). In
all other functions, only that function will be called (and not
base-class versions), whether it’s virtual or not. The only way for
base-class versions of the same function to be called in ordinary functions
(virtual or not) is if you explicitly call that
function.
Normally, the action of the destructor is
quite adequate. But what happens if you want to manipulate an object through a
pointer to its base class (that is, manipulate the object through its generic
interface)? This activity is a major objective in object-oriented programming.
The problem occurs when you want to delete a pointer of this type for an
object that has been created on the heap with new. If the pointer is to
the base class, the compiler can only know to call the base-class version of the
destructor during delete. Sound familiar? This is the same problem that
virtual functions were created to solve for the general case. Fortunately,
virtual functions work for destructors as they do for all other functions except
constructors.
//: C15:VirtualDestructors.cpp
// Behavior of virtual vs. non-virtual destructor
#include <iostream>
using namespace std;
class Base1 {
public:
~Base1() { cout << "~Base1()\n"; }
};
class Derived1 : public Base1 {
public:
~Derived1() { cout << "~Derived1()\n"; }
};
class Base2 {
public:
virtual ~Base2() { cout << "~Base2()\n"; }
};
class Derived2 : public Base2 {
public:
~Derived2() { cout << "~Derived2()\n"; }
};
int main() {
Base1* bp = new Derived1; // Upcast
delete bp;
Base2* b2p = new Derived2; // Upcast
delete b2p;
} ///:~
When you run the program, you’ll
see that delete bp only calls the base-class destructor, while delete
b2p calls the derived-class destructor followed by the base-class
destructor, which is the behavior we desire. Forgetting to make a destructor
virtual is an insidious bug because it often doesn’t directly
affect the behavior of your program, but it can quietly introduce a memory leak.
Also, the fact that some destruction is occurring can further mask the
problem.
Even though the destructor, like the
constructor, is an “exceptional” function, it is possible for the
destructor to be virtual because the object already knows what type it is
(whereas it doesn’t during construction). Once an object has been
constructed, its VPTR is initialized, so virtual function calls can take
place.
 |
| |
|