18. Portable Code
18.1. Data Abstraction
- Port. Rec. 1
- Avoid the direct use of
pre-defined data types in declarations.
An excellent way of transforming your world to a "vale of tears"
is to directly use the pre-defined data types in declarations. If it
is later necessary, due to portability problems, to change the return
type of a function, it may be necessary to make change at a large number
of places in the code. One way to avoid this is to declare a new type
name using classes or typedef s to represent the types of
variables used. In this way, changes can be more easily made. This may
be used to give data a physical unit, such as kilogram or meter. Such
code is more easily reviewed. (For example, when the code is functioning
poorly, it may be noticed that a variable representing meters has been
assigned to a variable representing kilograms). It should be noted that
a typedef does not create a new type, only an alternative
name for a type. This means that if you have declared typedef
int Error , a variable of the type Error may be used
anywhere that an int may be used.
See also chapter 12, Rec. 49!
Example 67: Type declarations using typedef
// Instead of:
long int time;
short int mouseX;
char* menuName;
// Use (for example):
typedef long int TimeStamp;
typedef short int Coordinate;
class String { /* ... */ };
// and:
TimeStamp time;
Coordinate mouseX;
String menuName;
18.2. Sizes of Types
- Port. Rec. 2
- Do not assume that an
int and a long have the same size.
- Port. Rec. 3
- Do not assume that an
int is 32 bits long (it may be only 16 bits long).
- Port. Rec. 4
- Do not assume that a
char is signed or unsigned .
- Port. Rec. 5
- Always set
char
to unsigned if 8-bit ASCII is used.
In the definition of the C++ language, it has not yet been decided if
a char is signed or unsigned . This
decision has instead been left to each compiler manufacturer. If this is
forgotten and this characteristic is exploited in one way or another, some
difficult bugs may appear in the program when another compiler is used.
If 8-bit ASCII is used (as is quite likely in the future)
and comparisons are made of two characters, it is important that
unsigned char is used.
18.3. Type Conversions
- Port. Rec. 6
- Be careful not to make type
conversions from a "shorter" type to a "longer" one.
- Port. Rec. 7
- Do not assume that pointers
and integers have the same size.
- Port. Rec. 8
- Use explicit type conversions
for arithmetic using signed and unsigned values.
A processor architecture often forbids data of a given size to be
allocated at an arbitrary address. For example, a word must begin on an
"even" address for MC680x0. If there is a pointer to a char
which is located at an "odd" address, a type conversion from this
char pointer to an int pointer will cause the
program to crash when the int pointer is used, since this
violates the processor's rules for alignment of data.
18.4. Data Representation
- Port. Rec. 9
- Do not assume that you know
how an instance of a data type is represented in memory.
- Port. Rec. 10
- Do not assume that
long s, float s, double s or
long double s may begin at arbitrary addresses.
The representation of data types in memory is highly
machine-dependent. By allocating data members to certain addresses,
a processor may execute code more efficiently. Because of this, the
data structure that represents a class will sometime include holes and
be stored differently in different process architectures. Code which
depends on a specific representation is, of course, not portable.
See 18.3 for explanation of Port. Rec. 10.
18.5. Underflow/Overflow
- Port. Rec. 11
- Do not depend on underflow
or overflow functioning in any special way.
18.6. Order of Execution
- Port. Rec. 12
- Do not assume that the
operands in an expression are evaluated in a definite order.
- Port. Rec. 13
- Do not assume that you know
how the invocation mechanism for a function is implemented.
- Port. Rec. 14
- Do not assume that an object
is initialized in any special order in constructors.
- Port. Rec. 15
- Do not assume that static
objects are initialized in any special order.
If a value is modified twice in the same expression, the result of the
expression is undefined except when the order of evaluation is guaranteed
for the operators that are used.
The order of initialization for static objects may present
problems. A static object may not be used in a constructor, if it
is not initialized until after the constructor is run. At present,
the order of initialization for static objects, which are defined
in different compilation units, is not defined. This can lead to
errors that are difficult to locate
(see Example 69).
There are special techniques for avoiding this.
See Example 29!
Example 68: Do not depend on the order of initialization in constructors.
#include <iostream.h>
class X
{
public:
X(int y);
private:
int i;
int j;
};
inline X::X(int y) : j(y), i(j) // No! j may not be initialized before i !!
{
cout << "i:" << i << " & " << "j:" << j << endl;
}
main()
{
X x(7); // Rather unexpected output: i:0 & j:7
}
Example 69: Initialization of static objects
// Foo.hh
#include <iostream.h>
#include <string.h>
static unsigned int const Size = 1024;
class Foo
{
public:
Foo( char* cp ); // Constructor
// ...
private:
char buffer[Size];
static unsigned counter; // Number of constructed Foo:s
};
extern Foo foo_1;
extern Foo foo_2;
// Foo1.cc
#include "Foo.hh"
unsigned Foo::counter = 0;
Foo foo_1 = "one";
//Foo2.cc
#include "Foo.hh"
Foo foo_2 = "two";
Foo::Foo( char* cp ) // Irrational constructor
{
strncpy( buffer, cp, sizeof(buffer) );
foos[counter] = this;
switch ( counter++ )
{
case 0:
case 1:
cout << ::foo_1.buffer << "," << ::foo_2.buffer << endl;
break;
default:
cout << "Hello, world" << endl;
}
}
// If a program using Foo.hh is linked with Foo1.o and Foo2.o, either
// ,two or one, is written on standard output depending on
// one,two one,two the order of the files given to the linker.
18.7. Temporary Objects
- Port. Rec. 16
- Do not write code which is
dependent on the lifetime of a temporary object.
Temporary objects are often created in C++, such as when functions
return a value. Difficult errors may arise when there are pointers in
temporary objects. Since the language does not define the life expectancy
of temporary objects, it is never certain that pointers to them are
valid when they are used.
One way of avoiding this problem is to make sure that temporary objects
are not created. This method, however, is limited by the expressive
power of the language and is not generally recommended.
The C++ standard may someday provide an solution to this problem. In
any case, it is a subject for lively discussions in the standardization
committee.
Example 70: Difficult error in a string class which lacks output operator
class String
{
public:
operator const char*() const; // Conversion operator to const char*
friend String operator+( const String& left, const String& right );
// ...
};
String a = "This may go to ";
String b = "h***!";
// The addition of a and b generates a new temporary String object.
// After it is converted to a char* by the conversion operator, it is
// no longer needed and may be deallocated. This means that characters
// which are already deallocated are printed to cout -> DANGEROUS!!
cout << a + b;
18.8. Pointer Arithmetic
- Port. Rec. 17
- Avoid using shift operations
instead of arithmetic operations.
- Port. Rec. 18
- Avoid pointer arithmetic.
Pointer arithmetic can be portable. The operators ==
and != are defined for all pointers of the same type,
while the use of the operators < , > ,
<= , >= are portable only if they are
used between pointers which point into the same array.
|