Saturday, May 31, 2008

C++ #23

124. A Pure Virtual Destructor
Unlike ordinary member functions, a virtual destructor is not overridden when redefined in a derived class. Rather, it is extended: the lower-most destructor first invokes the destructor of its base class and only then, it is executed. Consequently, when you try to declare a pure virtual destructor, you may encounter compilation errors, or worse: a runtime crash. However, there's no need to despair--you can enjoy both worlds by declaring a pure virtual destructor without a risk. The abstract class should contain a declaration (without a definition) of a pure virtual destructor:

//Interface.h file
class Interface {
public:
virtual ~Interface() = 0; //pure virtual destructor declaration
};
Somewhere outside the class declaration, the pure virtual destructor has to be defined like this:

//Interface.cpp file
Interface::~Interface()
{} //definition of a pure virtual destructor; should always be empty
125. The Type-name Keyword
A type-name is merely a placeholder that has to be substituted with a proper type. For example:

template<class T> T min( const T& first, const T& second); //T is a type-name, not a type
In some circumstances, an ambiguity between a type and a type-name may result:

int N;
template < class T > T func(){
T::A * N; // ambiguous: a multiplication statement or a pointer declaration?
//_
};
If T::A is a type-name, then N is a pointer; on the other hand, if T::A is a type, then "T::A * N" is an expression statement in which T::A is multiplied by a global int N.
By default, the compiler assumes that an expression like T::A is a type. The type-name keyword instructs the compiler to supersede this default interpretation and resolve the ambiguity in favor of a type-name rather than a type. In other words, the seemingly ambiguous statement above is actually resolved as a multiplication expression (the result of which is discarded). In order to declare a pointer, the type-name keyword is required:

int N;
template < class T > T func(){
typename T::A * N; // N is a now pointer since T::A is a type-name
//_
};
126. How is Virtual Inheritance Implemented?
When multiple inheritance is used, it is sometimes necessary to use virtual inheritance. A good example for this is the standard iostream class hierarchy:

//Note: this is a simplified description of iostream classes

class ostream: virtual public ios { /*..*/ }
class istream: virtual public ios { /*..*/ }

class iostream : public istream, public ostream { /*..*/ } //a single ios inherited
How does C++ ensure that only a single instance of a virtual member exists, regardless of the number of classes derived from it? C++ uses an additional level of indirection to access a virtual class, usually by means of a pointer. In other words, each object in the iostream hierarchy has a pointer to the shared instance of the ios object. The additional level of indirection has a slight performance overhead, but it's a small price to pay.
127. Phases of Construction
The construction of an object consists of several phases, including constructing its base and embedded objects, assigning a this pointer, creating the virtual table, and invoking the constructor's body. The construction of an object declared as const and/or volatile has an additional phase that turns an object into a const/volatile one. Therefore, you should not assume any "constness" of an object unless it has been fully constructed. And of course, a constructor of a const/volatile object can modify its object
128. Memory Layout of a Multiply Inherited Object is Unspecified
When multiple inheritance is used, the memory layout of such an object is implementation-dependent. The compiler can rearrange the order of the inherited subobjects to improve memory alignment. In addition, a virtual base can be moved to a different memory location. Therefore, when using multiple inheritance, you should not assume anything about the underlying memory layout of an object.
129. Perform Cross Casts Properly
A cross cast converts a multiply-inherited object to one of its secondary base classes. To demonstrate what a cross cast does, consider this class hierarchy:

struct A
{
int i;
virtual ~A () {}
};
struct B
{
bool b;
};

struct D: public A, public B
{
int k;
D() { b = true; i = k = 0; }
};

A *pa = new D;
B *pb = dynamic_cast<B*> pa; //cross cast; convert to the second base class
The static type of pa is "pointer to A", whereas its dynamic type is "pointer to D". A simple static_cast cannot convert a "pointer to A" into a "pointer to B" because A and B are unrelated (your compiler issues an error message in this case). To perform the cross cast properly, the value of pb has to be calculated at run time. After all, the cross cast can appear in a source file that doesn't even know that class D exists. To get the cross cast done properly, a dynamic cast is required, as shown in the example.
130. Perform Safe Downcasts
A downcast is a cast from a base to a derived object. Before the introduction of RTTI to the language, downcasts were regarded as bad programming practice--they were unsafe and some even considered the reliance on the dynamic type of an object to be a violation of object-oriented principles. You can perform safe downcasts from a virtual base to its derived object using dynamic_cast.

struct V
{
virtual ~V (){} //ensure polymorphism
};
struct A: virtual V {};
struct B: virtual V {};
struct D: A, B {};

#include <iostream>
using namespace std;
int main()
{
V *pv = new D;
A* pa = dynamic_cast<A*> (pv); // downcast
cout<< "pv: "<< pv << " pa: " << pa <<endl; // pv and pa have different addresses
return 0;
}
V is a virtual base for classes A and B. D is multiply-inherited from A and B. Inside main(), pv is declared as a "pointer to V" and its dynamic type is "pointer to D". The dynamic type of pv is needed in order to properly downcast it to a pointer to A. Using a static_cast<> in this case would be rejected by the compiler. As the output of the program shows, pv and pa indeed point to different memory addresses.
131. What is the Role of an Implicitly-Declared Constructor?
If there is no user-declared constructor in a class, and the class does not contain const or reference data members, the implementation implicitly declares a default constructor for it. An implicitly-declared default constructor is an inline public member of its class. It performs the initialization operations that are needed by the implementation to create an object instance. Note, however, that these operations do not involve initialization of user-declared data members or allocation of memory from the free store. For example:

class C
{
private:
int n;
char *p;
public:
virtual ~C() {}
};

void f()
{
C obj; // 1 implicitly-defined constructor is invoked
}
The programmer did not declare a constructor in class C. Therefore, an implicit default constructor was declared and defined by the implementation in order to create an instance of class C. The synthesized constructor does not initialize the data members n and p, nor does it allocate memory for the latter. These data members have an indeterminate value after obj has been constructed.
132. The Exception Specification of an Implicitly-Declared Default Constructor
An implicitly-declared default constructor has an exception specification. The exception specification contains all the exceptions of every other special member functions (for example, the constructors of base class and embedded objects) that the constructor invokes directly. To demonstrate that, consider this class hierarchy:

struct A
{
A(); //can throw any type of exception
};

struct B
{
B() throw(); //empty exception specification; not allowed to throw any exceptions
};
Here, the classes, C and D, have no user-declared constructors and consequently, the implementation implicitly declares constructors for them:

struct C : public B
{
//implicitly-declared constructor
// public: inline C::C() throw;
}

struct D: public A, public B
{
//implicitly-declared constructor
// public inline D::D();
};
The implicitly-declared constructor in class C is not allowed to throw any exception because it directly invokes the constructor of class B, which is not allowed to throw any exception either. On the other hand, the implicitly-declared constructor in class D is allowed to throw any type of exception because it directly invokes the constructors of the classes A and B. Since the constructor of class A is allowed to throw any type of exception, D's implicitly-declared constructor has a matching exception specification. In other words, an implicitly-declared constructor allows all exceptions if any function it directly invokes allows all exceptions; it allows no exceptions if every function it directly invokes allows no exceptions.
133. Declare a Class Template as a Friend of Another Class Template
A class template can be a friend of another class template. For example:

template <class U> class D{/*…*/};
template <class T> class Vector
{
public:
//…
template <class U> friend class D;
};
In this example, every specialization of the class template D is a friend of every specialization of the class template Vector.

134. Class Types Used in Typeid Expressions Must Be Completely Defined
When you use operator typeid, the class type of its argument must be completely defined. This means that a forward declaration of the argument's base class is insufficient, as in this example:

class Base; //forward declaration of a base

void func(Base *pb)
{
if (typeid(*pb) != typeid(Derived)) //trouble
do_something();
}
In order to make this code work properly, the declarations of both Derived and Base have to be accessible from the scope of the typeid expression. You can make them accessible by #including their header file:

#include "Base.h"
#include "Derived.h"

void func(Base *pb)
{
if (typeid(*pb) != typeid(Derived)) //now fine
do_something();
}
135. Static Class Members may not be Initialized in a Constructor
A common mistake is to initialize static class members inside the constructor body or a member-initialization list like this:

class File
{
private:
static bool locked;
private:
File();
//…
};
File::File(): locked(false) {} //error, static initialization in a member initialization list
Although compilers flag these ill-formed initializations as errors, programmers often wonder why this is an error. Bear in mind that a constructor is called as many times as the number of objects created, whereas a static data member may be initialized only once because it is shared by all the class objects. Therefore, you should initialize static members outside the class, as in this example:

class File
{
private:
static bool locked;
private:
File() { /*..*/}
//…
};

File::locked = false; //correct initialization of a non-const static member
Alternatively, for const static members of an integral type, the Standard now allows in-class initialization:

class Screen
{
private:
const static int pixels = 768*1024; //in-class initialization of const static integral types
public:
Screen() {/*..*/}
//…
};

No comments: