C++ Programming Tips

Correctness, portability, and reuse of code

Efficiency and optimization

Potential pitfalls and other subtlies


Making the effort to develop good C++ programming habits will save you considerable time and heartache. Let me share some simple lessons I've learned.

C++ != (C+1)

It's dangerous to think of C++ as just a new and improved C. Granted, well-formed, valid C is almost always well-formed, valid C++, but there are instances in which C source will not compile—or worse, compile but not behave as expected. If there is a C way of doing things and a C++ way of doing things, it's usually best to choose the latter. The C++ way is generally safer, faster, and more elegant. To give but one example, string manipulation is considerably easier and less error prone with std::strings than with '\0'-terminated char*s.

Avoid the C Preprocessor

The only appropriate use of #define is in the “include-file-once” construction. Use const or enum to define constants and use inlined functions rather than preprocessor macros. The preprocessor does not understand C++ syntax. It performs a naive search and replace with no type checking and is thus unsafe.

#ifndef MISC_HEADER_HPP
#define MISC_HEADER_HPP
#include <cmath>
using std::sqrt;

const int MAX_VALUE = 127;
enum { RED, BLUE = 4, GREEN };

inline double hypot(double x, double y)
{
    return sqrt(x*x+y*y);
}
#endif // MISC_HEADER_HPP

Portability

Since I tend to develop on one platform (MacOS X desktop) and run on others (GNU/Linux, AIX, IRIX clusters), cross-platform compatibility is an issue that matters to me. There is nothing more irritating than having painstakingly tested and debugged a large project on one machine only to find that it won't even compile on another. Strict adherence to the C++ language standard can help to avoid this problem. Don't use platform-specific extensions and don't use a forgiving compiler as a crutch. It's a good idea to turn on all your compiler's warnings during development. I compile with gcc -ansi -pedantic -Wall.

It's also a good idea to use the predefined typedefs std::size_t and std::ptrdiff_t, since the underlying types for array indexing and pointer subtraction differ from platform to platform.

#include <cstddef>
using std::size_t;
using std::ptrdiff_t;

const size_t dim = 3;

main()
{
   size_t w = sizeof(int);
   int v[dim];                          // sizeof(v) == dim*w;
   ptrdiff_t p = &(v[0]) - &(v[2]);     // v[2] == *(v-p);
   exit(0);
}

Run-time and Compile-time Testing

C++ does not offer language-level support for contract programming—the programmer cannot specify pre- and post-conditions on functions and classes—but the same functionality can be acheived using the C assert macro. It is good practice to sprinkle assertions liberally throughout your code. In general, you should assert(expr) wherever you assume that expr is true. If expr is false, your program will abort and report the file and line number in which the assertion failed. This is an extremely useful way of uncovering logical errors in your code. Compiling with the -DNDEBUG option turns off assertion testing (by causing the assert macro to evaluate to the empty statement.)

#include <cassert>
#include <cmath>
#include "my_rand.hpp"

double my_rand(void);

int main()
{
   double p = my_rand();
   assert( 0.0 < p and p < 1.0 );
   double pp = sqrt(p*(1-p));
   exit(0);
}

It is better to catch errors at compile time when possible. C++'s relatively strong type checking helps to eliminate some kinds of errors that would only become apparent at runtime in other languages (e.g., Objective-C). We can use template specialization tricks to enforce other compile-time checks. For example, the following code will only compile if the (hypothetical) class quad_prec is specialized in std::numeric_limits.

#include <limits>
#include "quad_prec.hpp";

template <bool> class compiler_test;
template <> class compiler_test<true> {};

template <class T>
T foo(void) throw()
{
   compiler_test< std::numeric_limits<T>::is_specialized == true >();

   if (std::numeric_limits<T>::is_signed)
      return T(1);
   else
      return T(-1);
}

class quad_prec;

int main()
{
   quad_prec x = foo<quad_prec>();
   exit(0);
}

The const Keyword

The syntax for the const keyword is a little complicated, but it's worth mastering. You can catch many careless errors by marking const any variables you know to be constant. If you accidentally attempt to alter a variable so marked, the compiler will report an error. Note that const variables must be initialized when declared.

int main()
{
   const double pi = 3.14;
   
   pi = 22.0/7.0;       // illegal!
   const double gamma;  // illegal!

   exit(0);
}

In the case of pointers, const may indicate, depending on its position, either that the pointer is constant or that the data it points to is constant.

int main()
{
   int v[4] = { 0, 1, 2, 3 };
   const int* p1 = v;        // p1 is a pointer to const int
   int* const p2 = v;        // p2 is a const pointer to int
   const int* const p3 = v;  // p3 is a const pointer to const int

   *p2 = *(++p1) = *p3;  // all perfectly legal
   
   *p1 = -1;   // illegal!
   ++p2;       // illegal!
   *p3 = -2;   // illegal!
   ++p3;       // illegal!
   
   exit(0);
}

Within a class definition, the syntax foo const {} indicates that the member function foo does not make changes to the internal data of the class. A class instantiation marked const can only have its const member functions invoked.

#include <cstddef>
using std::size_t;

template <size_t N>
class array
{
   // DATA
private:
   double data[N];
   
   // METHODS
public:
   array() {}
   array(const array& a)
   {
      for (size_t n = 0; n < N; ++n)
         data[n] = a[n];
   }
   double& operator[](size_t n) { return data[n]; }
   const double& operator[](size_t n) const { return data[n]; }
   size_t size() const { return N; }
   void cycle() 
   {
      const x = data[N-1];
      for (size_t n = N-1; n; --n)
         data[n] = data[n-1];
      data[0] = x;
   }
   double sum() const
   {
      double tmp = data[0];
      for (size_t n = 1; n < N; ++n)
         tmp += data[n];
      return tmp;
   }
};

int main()
{
    array<3> a;
    a[0] = 3.4;
    a[1] = 9.9;
    a[2] = -4.1;
    a.cycle();
    
    const array<3> b(a);   // b = ( 9.9, -4.1, 3.4 );
    
    a[0] = b[2];  // legal
    b[2] = a[0];  // illegal!
    
    double total = b.sum(); // legal
    size_t L = b.size();    // legal
    b.cycle();              // illegal!
    
    exit(0);
}

Namespaces

Avoid polluting the global name space. If you are writing code that may be reused in another project, you don't want the names of your functions and classes to clash with those from other libraries. Yes, a function named MY_VERY_OWN_SPECIAL_PREFIX_foo is likely to play well with others, but a verbose naming convention is cumbersome and still does not guarantee uniqueness. C++ provides a straightforward solution. Definitions enclosed by a namespace block can be accessed using the scope (::) operator.

#include <vector>

namespace mylib {
class vector;
} // namespace mlib

int main()
{
   std::vector x;
   mylib::vector y;
   exit(0);
}

Anonymous namespaces provide an elegant way to specify file scope. In C, this is accomplished using the static keyword, but since static has so many different meanings (depending on the context), it's best to use it sparingly. (I prefer to reserve it for indicating that a variable is shared by all instantiations of a particular class.) In the following example, including foo.hpp makes only the function foo available. The helper functions remain hidden.

#ifndef MYLIB_FOO_HPP
#define MYLIB_FOO_HPP

namespace {

double helperFunc1(void);
double helperFunc2(void);
double helperFunc3(void);

} // namespace anonymous

namespace mylib {

double foo(int i)
{
   switch(i)
   {
      case 1:
      return helperFunc1();
      case 2:
      return helperFunc2();
      case 3:
      return helperFunc3();
   }
}

} // namespace mylib

#endif // MYLIB_FOO_HPP

Generic Programming

The Standard Template Library (STL) is now part of the C++ standard. It provides a set of templated container classes, iterators (pointer generalizations), and generic algorithms. With the STL you can finally stop reinventing the wheel. There is really no good reason to write your own array, linked list, or hash table. And no good reason to write your own search or sorting function. There are robust, well-tested, best-in-class implementations already provided for you. A very useful STL programmer's guide is maintained by SGI. The following code reads in a sequence of integers from stdin and sends them back sorted to stdout.

#include <vector>
using std::vector;

#include <algorithm>
using std::copy;
using std::sort;

#include <iterator>
using std::istream_iterator;
using std::ostream_iterator;
using std::back_insert_iterator;

#include <iostream>
using std::cin;
using std::cout;

int main()
{
   vector<int> v;
   copy(istream_iterator<int>(cin),istream_iterator<int>(),back_inserter(v));
   sort(v.begin(),v.end());
   copy(v.begin(),v.end(),ostream_iterator<int>(cout," "));
   exit(0);
}

Exceptions

In C the usual way to indicate failure in a function is to return some special value or to set a global variable such as errno. This is inadequate for the following reasons. First, there is not always an appropriate return value that can be interpreted unambiguously as an error [e.g., float f = atof("?4.56");]. Second, a global variable offers no assurances about which function flagged the error condition and when.

C++ offers vastly improved support for exception handling using a catch/throw syntax.

#include <cmath>
#include <iostream>

double foo(double x) throw (int)
{
   if (x > 1.0) throw 1;
   return sqrt(1.0-x);
}

int main()
{
   try { y = foo(5.3); } 
   catch (int)
   {
      cerr << "!!! Exception thrown !!!";
      exit(1);
   }
   exit(0);
}

Exceptions are always passed by value, but the pass by reference syntax can be used to indicated that all derived classes should be caught as well.

#include <iostream>
#include <exception>

struct my_exception : std::exception
{
   char const* what() const throw() { return "my exception"; }
};

int main()
{
   try 
   { 
      throw my_exception; 
   }
   catch (const std::exception &e)
   {
      cerr << "!!!" << e.what() << "!!!";
      exit(1);
   }
   exit(0);
}

Some sensible advice on exception handling can be found here.

Dynamic Storage

Beware the new new. Its behaviour after failing to allocate memory has changed. It used to return a NULL pointer. The C++ Standards Committee, however, has decided that new should instead throw a std::bad_alloc exception.

#include <iostream>
#include <exception>

class large_object;

int main()
{
   try { large_object ptr_lo = new large_object; }
   catch (std::bad_alloc)
   {
      std::cerr << "Out of memory" << std::endl;
      exit(1);
   }
   
   // manipulations on ptr_lo;
   
   delete ptr_lo;
}

You can still request that new behave in the old way by passing a std::nothrow argument.

#include <iostream>
#include <new>

class large_object;

int main()
{
   large_object ptr_lo = new (std::nothrow) large_object;
   if (prt_lo == NULL)
   {
      std::cerr << "Out of memory" << std::endl;
      exit(1);
   }
   
   // manipulations on ptr_lo;
   
   delete ptr_lo;
}

In many cases, it's really best to do away with explicit calls to new and delete. Dynamic storage of arbitrary type can be provided by the auto_ptr class. The first advantage of an auto_ptr over a conventional C pointer is that freeing of storage is handled automatically: a delete occurs when auto_ptr's destructor is called. This means that memory is freed as soon as an auto_ptr goes out of scope. The second advantage is that auto_ptr is exception-safe. If an exception occurs, auto_ptr's destuctor is called as the stack unwinds.

#include <memory>

void foo1() 
{
   T* ptr_T = new T;
   
   // manipulations on *ptr_T
   // if an exception is thrown in here, we're in trouble

   delete p; // memory leaks if we forget the corresponding delete
}

void foo2() 
{
   auto_ptr ptr_T( new T ); 
   
   // manipulations on *ptr_T
}

Avoid hidden temporary objects

One of the potential inefficiencies of C++ is that its pass-by-value semantics sometimes leads to the creation of temporary objects that haven't explicity been declared by the programmer. This behaviour is innocent enough for built-in datatypes but not for large class objects; in the latter case creation of a temporary object requires invoking the class's (expensive) copy contructor.

For function arguments, the solution is to pass class objects by reference.

#include <vector>
using std::vector;

#include <algorithm>
using std::find;

bool contains_value1(vector<int> v, int i)
{
   // v is a local variable with data 
   // copied from the function argument
   
   vector<int>::const_iterator result;
   result = find(v.begin(), v.end(), i);
   assert(result == v.end() || *result == i);
   return result != v.end();
}

bool contains_value2(const vector<int> &v, int i)
{
   // v is a local reference to the function argument.
   // The compiler can optimize away the indirection.
   
   vector<int>::const_iterator result;
   result = find(v.begin(), v.end(), i);
   return result != v.end();
}

inline bool contains_value3(const vector<int>& v, int i)
{
   return find(v.begin(), v.end(), i) != v.end();
}

Overloaded binary operators are often a source of hidden temporaries. Code of the form A = B+C is inefficient in that the right hand side first generates a temporary object—the output of operator+(B,C)—which is only then assigned to A. There are two simple workarounds: either assign B and then add C in place or use the copy constructor to put the value of B+C directly into A.

class matrix;
matrix matrix::operator+(const matrix&, const matrix&);
matrix& matrix::operator+=(const matrix&);
bool matrix::operator==(const matrix&,const matrix&);

int main()
{
   matrix A1, A2, B, C;
   
   A1 = B + C;      // inefficient

   A2 = B;          // no temporaries generated
   A2 += C;
   
   matrix A3(B+C);  // compiler can optimize away the temporary
                    // by constructing B+C directly into A3
   
   assert(A1 == A2 and A2 == A3);
   
   exit(0);
}

Similar considerations lead to the so-called return value optimization.

#include <complex>
using std::complex;

complex<double> linear_combination1(double a1, complex<double> z1,
                                    double a2, complex<double> z2)
{
    complex<double> z;
    z = a1*z1 + a2*z2;
    return z;
}

complex<double> linear_combination2(double a1, complex<double> z1,
                                    double a2, complex<double> z2)
{
    complex<double> z(a1*z1 + a2*z2);
    return z;
}

complex<double> linear_combination3(double a1, complex<double> z1,
                                    double a2, complex<double> z2)
{
    return complex<double>(a1*z1 + a2*z2);  // compiler can eliminate temporary
}

Note that pre- and postfix operators have different return types. Using ++object rather than object++ prevents the creation of temporaries.

object& operator++(int);  // prefix increment
object operator++();      // postfile increment

Loop Unrolling

It is typical for a program to spend the bulk of its time in some tight inner loop—in which case, careful optimization of the loop can yield significant performance improvements. One common situation is when the number of operations inside the loop is so small that the compare and increment overhead of the loop counter is an appreciable fraction of the total computational work.

It was once considered good practice to manually “unroll” such loops:

#include <vector>
using std::vector;

double sum1(const vector<double> &v)
{
   double tmp = 0.0;
   for (vector<double>::size_type i = 0; < v.size(); ++i)
      tmp += v[i];
   return tmp;
}

double sum2(const vector<double> &v)
{
   assert(v.size()%4 == 0);
   double tmp = 0.0;
   for (vector<double>::size_type i = 0; < v.size()/4; ++i)
   {
      tmp += v[i];
      tmp += v[i+1];
      tmp += v[i+2];
      tmp += v[i+3];
   }
   return tmp;
}

Most modern compilers can now do this for you. On gcc, one simply writes g++ -funroll-loops -O3.

The problem with unrolling by hand is that the optimal degree of unrolling is highly architecture-dependent. Tuning the code for one machine may actually degrade its performance on another. That is to say, manual loop unrolling trades portability for performance.

Order of variable declarations

When a member initialization list is specified in a constructor, the initialization of class members is carried out in the order of declaration. Somewhat counterintuitively, the order of the initialization list carries no meaning. Thus the class ordered_tuple defined below has undefined behaviour:

class ordered_tuple
{
public:
   int i3;
   int i2;
   int i1;
   ordered__tuple(int i) : i1(i), i2(i1+1), i3(i2+1) {}

};

The compiler will not complain about such errors, so they can be hard to detect.