13. Type Conversions
- Rule 43
- Never use explicit
type conversions (casts).
- 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.
[See Example 53!]
Another unpredictable effect of implicit type conversions is that
temporary objects are created during the conversion.
[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).
[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.
Example 49: Constructors with a single argument that may imply dangerous type conversions
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 ) );
Example 50: A use of implicit type conversion
// String.hh
class String
{
public:
String( char* cp ); // Constructor
operator const char* () const; // Conversion operator to const char*
// ...
};
void foo( const String& aString );
void bar( const char* someChars );
// main.cc
main()
{
foo( "hello" ); // Implicit type conversion char* -> String
String peter = "pan";
bar( peter ); // Implicit type conversion String -> const char*
}
Example 51: When implicit type conversion gives unpleasant results
// 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 !
Example 52: Conversion of derived class pointer to a virtual base class pointer is irreversible
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
}
Example 53: Addition which leads to a compile-time error
// String.hh
class String
{
public:
String( char* cp ); // Constructor
operator const char* () const; // Conversion operator to const char*
// ...
};
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()
{
foo( "hello" ); // Error ambiguous type conversion !
String peter = "pan";
bar( peter ); // Implicit type conversion String -> const char*
}
Example 54: For more efficient execution, remove const-ness when storing intermediate results
// 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);
}
((Vector*)this)->lengthCache = sqrt(quadLength); // No! Cast away const
}
return lengthCache;
}
void
Vector::set( int nr, int value )
{
if ( nr >= size ) error( "Out Of Bounds");
array[nr]=value;
hasChanged = 1;
}
Example 55: Alternative to removing const-ness for more efficient execution
// This is code is safer than Example 54 but could be inefficient
#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 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;
}
// Continue on next page !
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;
}
|