Chapter Thirteen

Parts of C++ to avoid

There are parts of C++ that should be avoided. C++ comes with many new standard library classes and templates that in many cases replace functions inherited from the C standard library. Also certain parts of the language that are inherited from C are no longer needed. Either better language constructs exists or there are classes or templates to use instead.

Library functions to avoid

C++ has inherited all parts of the library defined by the C standard. Some of the functions provided by the C standard library are not well-suited for C++ programming and should not be used.

RULES
AND
RECOMMENDATIONS:

Rec 13.1 Use new and delete instead of malloc , calloc , realloc and free .

Rule 13.2 Use the iostream library instead of C-style I/O.

Rule 13.3 Do not use setjmp( ) and longjmp() .

Rec 13.4 Use overloaded functions and chained function calls instead of functions with an unspecified number of arguments.

Rec 7.15 , Rule 7.16 , how to overload functions and operators.

Rule 8.1 - Rule 8.2 , how to use new and delete .

Rec 12.2 , exception handling can be used instead of setjmp and longjmp .

Rec 13.1 Use new and delete instead of malloc, calloc, realloc and free.

You should avoid all memory-handling functions from the standard C-library (such as malloc , calloc , realloc and free ) since they do not call constructors for new objects or destructors for deleted objects.

It is also dangerous to mix C and C++ allocation of memory, such as:

Complete avoidance of C memory handling is therefore recommended.

Rule 13.2 Use the iostream library instead of C-style I/O.

For similar reasons the iostream library is better to use than the stdio library. Functions in the stdio library cannot be used for user-defined objects.

C-style I/O is not adequate for objects

 
EmcString s;
cin >> s;                // Yes: this works

scanf("%??", s);         // NO: this does not work

It is not possible to extend the set of formats understood by scanf .

If optimal efficiency is required, the stdio library is sometimes better than the iostream library. This is not a universal truth, however, so you should do performance benchmarks before you start to use the stdio library. If you use it, localize the code so that it is easy to replace.

Rule 13.3 Do not use setjmp() and longjmp().

The normal way to leave a function is by using a return statement which gives control back to the calling function. If a serious error has been encountered, this can be an unwise thing to do. The calling function could perhaps recover from the failure, and when the program crashes it is difficult to find out what went wrong. The correct thing to do in C++ is to throw an exception. The library functions setjmp() and longjmp() can be used to simulate exception handling. Unfortunately the behavior of these functions is very platform-dependent. Even worse is the fact that destructors are not called for bypassed objects when longjmp() is called. You should therefore avoid them altogether.

Rec 13.4 Use overloaded functions and chained function calls instead of functions with an unspecified number of arguments.

Functions with unspecified number of arguments should be avoided since they are a common cause of bugs that are hard to find. For example, the compiler is not able to check that an argument is of the type expected by the function. Such checks must instead be done by the function in run-time.

In most cases it is in C++ possible to use overloaded functions or operators instead, and to chain the function calls by returning references to operate upon. Such solutions are more type safe.

Passing objects to printf()

The function printf() should not be given an object as argument even if the object is of a class that can be implicitly converted to a type that printf() knows how to handle.

 
class DangerousString
{
   public:
      DangerousString(const char* cp);
      operator const char*() const; // Conversion operator
      // ...
};

DangerousString hello = "Hello World!";
cout << hello << endl;            // Works perfectly
printf("%s\n", hello);            // Garbage is printed 

In this case operator const char*() will be called when the string is passed to cout , but this will not happen for the string when it is passed to printf() . When a string object is passed as argument to printf() , no implicit conversion takes place and the bit pattern for the object will be printed as a string.

Overloading of operator<<

 
class EmcString
{
   public:
      EmcString(const char* cp);
      // ...
};

ostream&  
operator<<(ostream& os, const EmcString& s);

EmcString s = "Hello World!";
cout << s << endl;           // uses overloaded operator 

Language constructs to avoid

A few parts of the C++ language should be avoided since they are too error prone compared to the potential benefit of using them.

RULES
AND
RECOMMENDATIONS

Rule 13.5 Do not use macros instead of constants, enums, functions or type definitions.

Rec 13.6 Use an array class instead of built-in arrays.

Rec 13.7 Do not use unions.

Rule 2.3 , macros should be used in include guards.

Rec 10.3 , polymorphism and inheritance can often replace selection statements and unions.

Rec 15.14 , macros can be used for writing forward-compatible code.

Style 1.6 - Style 1.7 , how include guards are written.

Rule 13.5 Do not use macros instead of constants, enums, functions or type definitions.

In C, macros are often used for defining constants. In C++, a better alternative is to use enum values or const declared variables. Macros do not obey the normal scope rules for the language, and this is a common source of errors. The compiler can seldom give meaningful error messages if the error is caused by a macro replacement.

Macros do not obey scope rules

 
#define SIZE 1024              // Not recommended
const size_t SIZE = 1024;      // Compilation error

Macro names should be all uppercase letters to help avoid unexpected macro replacements by the preprocessor. This is one reason to why you should not have normal identifiers in all uppercase letters.

Constants defined by the language obey the scope rules of the language and can for example be enclosed inside a class.

Recommended way to define constants

You can often define constants within a class.

 
class X
{
   public:
      // ...
   private:
      static const size_t maxBuf = 1024;
      enum Color {green, yellow, red};
};

// Definition of static const member
const size_t X::maxBuf;

Using an enum instead of static const int

Older compilers will not allow you to define ordinary constants inside a class. A common trick is to use an anonymous enum instead.

 
class X
{
      // ...
   private:
      enum { maxBuf = 1024 };
      enum Color {green, yellow, red};
};

Another advantage of using constants instead of macros is that most debuggers only see the code as it looks like after preprocessing, when all macro definitions have been substituted for their calls. It is possible to print the value of a constant, but not a macro value. Constants therefore make it easier to debug a program.

Macros are often used in C as a way to avoid the function-call overhead for time-critical functions.

Function-like macro, SQUARE

 
// Not recommended to have function-like macro
#define SQUARE(x) x*x

There are many problems with function-like macros. Since the arguments are pure textual replacements, the consequences of using complex expressions as arguments are often surprising.

 
int i = SQUARE(3 + 4);
// Wrong result: i = (3 + 4 * 3 + 4) == 19, not 49

It is common to add parentheses to the definition to avoid some bugs.

 
// Parentheses to avoid precedence bugs
#define SQUARE(x) ((x)*(x))  

But there are some bugs for which there is no good solution. If an argument is used more than once and an expression is passed as argument, the expression will be evaluated more than once.

 
int a = 2; int b = SQUARE(a++); 
// Unknown result: b = 4 or 6 depending on when the value
// of postfix ++ is evaluated.

Inline functions in C++ are often a better choice, since they allow you to avoid the function call overhead and you still have something that behaves as a function.

Inline function, square

 
inline int square(int x) // Recommended
{
   return x * x;
};

int c = 2;
int d = square(c++);     // d = (2 * 2) == 4

Another advantage of inline functions compared to macros is that they are type-safe, which means that the compiler will give meaningful error messages when a function is used with the wrong type of arguments.

Function-like macros are not type safe

 
int i = SQUARE("hello");   // Error: Illegal operands

Macros are also sometimes used to introduce synonyms for a type. A better solution is to use a typedef.

How to define synonyms for a type

 
#define Velocity int     // Not recommended
typedef int Velocity;    // Recommended

Macros should only be used as include guards and for very special purposes such as forward-compatibility macro packages (exceptions, templates and run-time type identification).

Rec 13.6 Use an array class instead of built-in arrays.

There are many potential bugs involved in using pointers to access built-in arrays. For example, when traversing an array, it is common to access too few or too many elements. Memory management can also be a big problem. It is almost always better to use an array template instead, and fortunately the standard library for C++ provides such a class.

There are a few other problems with the built-in arrays. They are of a fixed size which means that the whole array must be copied if you need to increase its size. If the size changes often this can be bad for the performance of the program. It is in most cases better to use a class that handles growth in an efficient way.

Another problem is that there is no bounds checking, which means that you can access a memory area outside the array if you are not careful.

When accessing an array, the index is simply used to find the address of an element in the array. An array is treated as a pointer to the first element and the index is the offset to the element.

The fact that an array is treated as a pointer when passed to functions is a common source for errors. It is especially dangerous to have arrays of objects. Since the size of derived class objects in most cases are different from the size of base class objects, the offset between elements in an array of base class objects will be different than the offset between elements in an array of derived class objects. C++ allows a derived class pointer to be assigned to a base class pointer, with the consequence that a compiler cannot prevent you from passing an array of derived class objects to a function that expects a pointer to an array of base class objects. When accessing elements in the array, you will get pointers within objects rather than pointers to objects. This is yet another reason to avoid the built-in arrays.

Passing array to function

 
// Fruit is a base class

void printFruits(Fruit* fruits, size_t size) 
// Not recommended to pass arrays to functions
{
   for (size_t i = 0; i < size; i++)
   {
      cout << fruits[i] << endl;
   }
}

If we have an array of objects of the derived class Apple , the following code may crash.

 
// Apple is derived from Fruit

const size_t numberOfApples = 3;

Apple apples[numberOfApples];

printFruits(apples, numberOfApples); // Might crash!

Rec 13.7 Do not use unions.

Unions may seem quite easy to use, since they look like classes with the exception that they only store one of its data members at a time. The similarity between classes and unions are, however, treacherous. A union cannot have virtual member functions, base classes, static data members or data members of any type that has a non-trivial default constructor, copy constructor, destructor or copy assignment operator. This can make unions very hard to use.

Unions can be an indication of a non-object oriented design that is hard to extend. Since a union could store different types of data, the programmer needs a way to tell what is actually stored. If the set of different types of data changes, each piece of code that accesses the object must be rewritten. This disadvantage can be made less serious by putting all access to the union inside a class, instead of used directly in many different places in the code.

The usual alternative to unions is inheritance and dynamic binding. The advantage of having a derived class representing each type of value stored is that the set of derived classes can be extended without rewriting any code. Since code with unions is only slightly more efficient, but much more difficult to maintain, you should avoid them unless you have a very good reason.