Rule 44 Do not write code which depends on functions that use implicit type conversions.
Rule 45 Never convert pointers to objects of a derived class to pointers to objects of a virtual base class.
Rule 46 Never convert a const to a non-const.
A type conversion may either be explicit or implicit, depending on whether it is ordered by the programmer or by the compiler. Explicit type conversions (casts) are used when a programmer want to get around the compiler's typing system; for success in this endeavour, the programmer must use them correctly. Problems which the compiler avoids may arise, such as if the processor demands that data of a given type be located at certain addresses or if data is truncated because a data type does not have the same size as the original type on a given platform. Explicit type conversions between objects of different types lead, at best, to code that is difficult to read.
Explicit type conversions (casts) can be motivated if a base class pointer to a derived class pointer is needed. This happens when, for example, a heterogeneous container class is used to implement a container class to store pointers to derived class objects. This new class can be made "type-safe" if the programmer excludes other objects than derived class pointers from being stored. In order for this implementation to work, it is necessary that the base class pointers are converted to derived class pointers when they are removed from the heterogeneous container class.
The above reason for using explicit casts will hopefully disappear when templates are introduced into C++.
It is sometimes said that explicit casts are to object-oriented programming, what the goto statement was to structured programming.
There are two kinds of implicit type conversions: either there is a conversion function from one type to another, written by the programmer, or the compiler does it according to the language standard. Both cases can lead to problems.
C++ is lenient concerning the variables that may be used as arguments to functions. If there is no function which exactly matches the types of the arguments, the compiler attempts to convert types to find a match. The disadvantage in this is that if more than one matching function is found, a compilation error will be the result. Even worse is that existing code which the compiler has allowed in other contexts, may contain errors when a new implicit type conversion is added to the code. Suddenly, there may be more than one matching function. [FN: See Example 53.]
Another unpredictable effect of implicit type conversions is that temporary objects are created during the conversion. [FN: See Example 51.] This object is then the argument to the function; not the original object. The language definition forbids the assignment of temporary objects to non-constant references, but most compilers still permit this. In most cases, this can mean that the program does not work properly. Be careful with constructors that use only one argument, since this introduces a new type conversion which the compiler can unexpectedly use when it seems reasonable in a given situation.
Virtual base classes give rise to other type conversion problems. It is possible to convert a pointer, to an instance of a class which has a virtual base class, to a pointer to an object of that virtual base class. The opposite conversion is not allowed, i.e. the type conversion is not reversible. For this reason, we do not recommend the conversion of a derived class pointer to a virtual base class pointer.
In order to return a non-const temporary object, it sometimes happens that an explicit type conversion is used to convert const member data to non-const. This is bad practice that should be avoided, primarily because it should be possible for a compiler to allocate constants in ROM (Read Only Memory). [FN: See Example 54 and Example 55.]
Exception to Rule 43: An explicit type conversion (cast) is preferable to a doubtful implicit type conversion. Explicit type conversions may be used to convert a pointer to a base class to a pointer of a derived class within a type-safe container class that is implemented using a heterogeneous container class. Explicit type conversion must be used to convert an anonymous bit-stream to an object. This situation occurs when unpacking a message in a message buffer. Generally, explicit type conversions are needed for reading an external representation of an object.
Exception to Rule 44: At times it is desirable to have constructors that use only one argument. By performing an explicit type conversion, the correctness of the code does not depend on the addition. See the Exception to Rule 22!
Exception to Rule 45: If a virtual base class is to contain a pure virtual function which converts a virtual base class pointer to a derived class pointer, this can be made to work by defining the function in the derived class. Note that this implies that all derived classes must be known in the virtual base class. See Example 52!
Exception to Rule 46: No exceptions. Use pointers to data allocated outside the class, when necessary. See Example 54 and Example 55.
class String { public: String( int length ); // Allocation constructor // ... }; // Function that receives an object of type String as an argument void foo( const String& aString ); // Here we call this function with an int as argument int x = 100; foo( x ); // Implicit conversion: foo( String( x ) );
// String.hh class String { public: // Constructor String( char* cp ); // Conversion operator to const char* operator const char* () const; // ... }; void foo( const String& aString ); void bar( const char* someChars ); // main.cc main() { // Implicit type conversion char* -> String foo( "hello" ); String peter = "pan"; // Implicit type conversion String -> const char* bar( peter ); }
// This function looks bulletproof, but it isn't. // Newer versions of compilers should flag this as an error. void mySwap( int& x, int& y ) { int temp = x; x = y; y = temp; } int i = 10; unsigned int ui = 20; mySwap( i, ui ); // What really happens here is: // int T = int( ui ); // Implicit conversion // mySwap( i, T ); // ui is of course not // changed! // Fortunately, the compiler warns for this !
class VirtualBase { public: virtual class Derived* asDerived() = 0; }; class Derived : virtual public VirtualBase { public: virtual Derived* asDerived(); }; Derived* Derived::asDerived() { return this; } void main() { Derived d; Derived* dp = 0; VirtualBase* vp = (VirtualBase*)&d; dp = (Derived*)vp; // ERROR! Cast from virtual base class pointer dp = vp->asDerived(); // OK! Cast in function asDerived }
// String.hh class String { public: // Constructor String( char* cp ); // Conversion operator to const char* operator const char* () const; // ... }; void foo( const String& aString ); void bar( const char* someChars ); // Word.hh class Word { public: Word( char* cp ); // Constructor // ... }; // Function foo overloaded void foo( const Word& aWord ); // ERROR: foo( "hello" ) MATCHES BOTH: // void foo( const String& ); // AND void foo( const Word& ); //main.cc main() { // Error ambiguous type conversion ! foo( "hello" ); String peter = "pan"; // Implicit type conversion String -> const char* bar( peter ); }
// This is code is NOT recommended #include <math.h> class Vector { public: Vector(int, const int []); // Constructor double length() const; // length = sqrt(array[1]*array[1] + ... ) void set(int x, int value); // ... private: int size; int* array; double lengthCache; // to cache calculated length int hasChanged; // is it necessary to re-calculate length ? }; double Vector::length() const { if (hasChanged) // Do we need to re-calculate length { ((Vector*)this)->hasChanged=0; // No! Cast away const double quadLength = 0; for ( int i = 0; i < size; i++ ) { quadLength += pow(array[i],2); } // No! Cast away const ((Vector*)this)->lengthCache = sqrt(quadLength); } return lengthCache; } void Vector::set( int nr, int value ) { if ( nr >= size ) error( "Out Of Bounds"); array[nr]=value; hasChanged = 1; }
// This is code is safer than Example 54 but could be inefficient #include <math.h> class Vector { public: Vector(int, const int []); // Constructor // length = sqrt(array[1]*array[1] + ... ) double length() const; void set(int x, int value); // ... private: int size; int* array; double* lengthCache; // to cache length in int* hasChanged; // is it necessary to re-calculate length ? }; Vector::Vector(int sizeA, const int arrayA[]) : size(sizeA), array( new int[sizeA] ), hasChanged(new int(1)), lengthCache(new double) { for ( int i = 0; i < size; i++ ) { array[i] = arrayA[i]; } } Vector::~Vector() // Destructor { delete array; delete hasChanged; delete lengthCache; } double Vector::length() const { if (hasChanged) // Do we need to re-calculate length ? { *hasChanged=0; double quadLength = 0; for ( int i = 0; i < size; i++ ) { quadLength += pow(array[i],2); } *lengthCache = sqrt(quadLength); } return lengthCache; } void Vector::set( int nr, int value ) { if ( nr >= size ) error( "Out Of Bounds"); array[nr]=value; *hasChanged = 1; }
Next section: 14 Flow Control Structures