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 typedefs 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 longs, floats, doubles or long doubles 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.