FAQs in section [15]:
[20.1] What is a "virtual member function"?
From an OO perspective, it is the single most important
feature of C++:
A virtual function allows derived classes to replace
the implementation provided by the base class. The compiler makes
sure the replacement is always called whenever the object in
question is actually of the derived class, even if the object is
accessed by a base pointer rather than a derived pointer. This
allows algorithms in the base class to be replaced in the derived
class, even if users don't know about the derived class.
The derived class can either fully replace ("override")
the base class member function, or the derived class can
partially replace ("augment") the base class member
function. The latter is accomplished by having the derived class
member function call the base class member function, if desired.
[15.2] How can C++ achieve dynamic binding yet also static
typing?
When you have a pointer to an object, the object may actually
be of a class that is derived from the class of the pointer (e.g.,
a Vehicle* that is actually pointing to a Car
object). Thus there are two types: the (static) type of the
pointer (Vehicle, in this case), and the (dynamic) type
of the pointed-to object (Car, in this case).
Static typing means that the legality of a member
function invocation is checked at the earliest possible moment:
by the compiler at compile time. The compiler uses the static
type of the pointer to determine whether the member function
invocation is legal. If the type of the pointer can handle the
member function, certainly the pointed-to object can handle it as
well. E.g., if Vehicle has a certain member function,
certainly Car also has that member function since Car
is a kind-of Vehicle.
Dynamic binding means that the address of the code in
a member function invocation is determined at the last possible
moment: based on the dynamic type of the object at run time. It
is called "dynamic binding" because the binding to the
code that actually gets called is accomplished dynamically (at
run time). Dynamic binding is a result of virtual
functions.
[15.3] What's the difference between how virtual and
non-virtual member functions are called?
Non-virtual member functions are resolved statically.
That is, the member function is selected statically (at compile-time)
based on the type of the pointer (or reference) to the object.
In contrast, virtual member functions are resolved
dynamically (at run-time). That is, the member function is
selected dynamically (at run-time) based on the type of the
object, not the type of the pointer/reference to that object.
This is called "dynamic binding." Most compilers use
some variant of the following technique: if the object has one or
more virtual functions, the compiler puts a hidden
pointer in the object called a "virtual-pointer" or
"v-pointer." This v-pointer points to a global table
called the "virtual-table" or "v-table."
The compiler creates a v-table for each class that has at
least one virtual function. For example, if class Circle
has virtual functions for draw() and move()
and resize(), there would be exactly one v-table
associated with class Circle, even if there were a
gazillion Circle objects, and the v-pointer of each of
those Circle objects would point to the Circle
v-table. The v-table itself has pointers to each of the virtual
functions in the class. For example, the Circle v-table
would have three pointers: a pointer to Circle::draw(),
a pointer to Circle::move(), and a pointer to Circle::resize().
During a dispatch of a virtual function, the run-time
system follows the object's v-pointer to the class's v-table,
then follows the appropriate slot in the v-table to the method
code.
The space-cost overhead of the above technique is nominal: an
extra pointer per object (but only for objects that will need to
do dynamic binding), plus an extra pointer per method (but only
for virtual methods). The time-cost overhead is also fairly
nominal: compared to a normal function call, a virtual
function call requires two extra fetches (one to get the value of
the v-pointer, a second to get the address of the method). None
of this runtime activity happens with non-virtual
functions, since the compiler resolves non-virtual
functions exclusively at compile-time based on the type of the
pointer.
Note: the above discussion is simplified considerably,
since it doesn't account for extra structural things like
multiple inheritance, virtual
inheritance, RTTI, etc., nor does it account for space/speed
issues such as page faults, calling a function via a pointer-to-function,
etc. If you want to know about those other things, please ask comp.lang.c++;
PLEASE DO NOT SEND E-MAIL TO ME!
[15.4] When should my destructor be virtual?
[Recently changed "explicit destructor"
to "explicitly defined destructor" to avoid confusion
with the explicit keyword used with
constructors (on 7/99). Click
here to go to the next FAQ in the "chain" of recent
changes.]
When you may delete a derived object via a base
pointer.
virtual functions bind to the code associated with
the class of the object, rather than with the class of the
pointer/reference. When you say delete basePtr, and
the base class has a virtual destructor, the destructor
that gets invoked is the one associated with the type of the
object *basePtr, rather than the one associated with the
type of the pointer. This is generally A Good Thing.
TECHNO-GEEK WARNING; PUT YOUR PROPELLER HAT ON.
Technically speaking, you need a base class's destructor to be virtual
if and only if you intend to allow someone to invoke an object's
destructor via a base class pointer (this is normally done
implicitly via delete), and the object being destructed
is of a derived class that has a non-trivial destructor. A class
has a non-trivial destructor if it either has an explicitly
defined destructor, or if it has a member object or a base class
that has a non-trivial destructor (note that this is a recursive
definition (e.g., a class has a non-trivial destructor if it has
a member object (which has a base class (which has a member
object (which has a base class (which has an explicitly defined
destructor)))))).
END TECHNO-GEEK WARNING; REMOVE YOUR PROPELLER HAT
If you had a hard grokking the previous rule, try this (over)simplified
one on for size: A class should have a virtual
destructor unless that class has no virtual
functions. Rationale: if you have any virtual functions
at all, you're probably going to be doing "stuff" to
derived objects via a base pointer, and some of the "stuff"
you may do may include invoking a destructor (normally done
implicitly via delete). Plus once you've put the first virtual
function into a class, you've already paid all the per-object
space cost that you'll ever pay (one pointer per object; note
that this is theoretically compiler-specific; in practice
everyone does it pretty much the same way), so making the
destructor virtual won't generally cost you anything extra.
[15.5] What is a "virtual constructor"?
An idiom that allows you to do something that C++ doesn't
directly support.
You can get the effect of a virtual constructor by a virtual
clone() member function (for copy constructing), or a virtual
create() member function (for the default constructor).
class Shape {
public:
virtual ~Shape() { } // A virtual destructor
virtual void draw() = 0; // A pure virtual function
virtual void move() = 0;
// ...
virtual Shape* clone() const = 0; // Uses the copy constructor
virtual Shape* create() const = 0; // Uses the default constructor
};
class Circle : public Shape {
public:
Circle* clone() const { return new Circle(*this); }
Circle* create() const { return new Circle(); }
// ...
};
In the clone() member function, the new Circle(*this)
code calls Circle's copy constructor to copy the state
of this into the newly created Circle object.
In the create() member function, the new Circle()
code calls Circle's default
constructor.
Users use these as if they were "virtual
constructors":
void userCode(Shape& s)
{
Shape* s2 = s.clone();
Shape* s3 = s.create();
// ...
delete s2; // You probably need a virtual destructor here
delete s3;
}
This function will work correctly regardless of whether the Shape
is a Circle, Square, or some other kind-of Shape
that doesn't even exist yet.
|