Chapter Seven

The class interface

The class interface is the most important part of a class. Sophisticated algorithms will not help if the class interface is wrong. Different aspects of the class interface are discussed in this chapter.

inline functions

argument passing

constness

operator and function overloading

conversion operator functions

Inline functions

Inline functions can improve the performance of your program. This chapter will discuss which functions that should be specified as inline, and which should not.

RULES
AND
RECOMMENDATIONS

Rec 7.1 Make simple functions inline.

Rule 7.2 Do not declare virtual member functions as inline .

Rec 14.1 , the danger of having too many inline functions.

Rule 14.2 , how to avoid making a virtual destructor inline.

EXAMPLE 2.5 , how to temporarily disable inlining.

Rec 7.1 Make simple functions inline.

It is possible to improve performance and make programs smaller by declaring functions inline . The opposite is also true if you use inlining in the wrong places.

Fewer machine instructions are executed when an inline function is called, since there is no need to prepare a stack frame for the function call. As long as the program does not grow so that the code will reside on different pages in memory, this is likely to improve performance. Too large executables should be avoided and that is why it is difficult to give an exact advice on when to use inline functions.

It may come as a surprise that inline expansion could decrease the overall size of the program, but if the overhead of a function call is larger than the total size of the inline-expanded code this is actually true.

If you have member functions whose sole purpose is to give access to data members, those member functions are likely candidates for inlining. This is a consequence of the rule that a class should not have any public or protected data members. Since member functions should be used instead, it is likely that you want to make them inline for the reasons explained above.

It can be hard to know exactly when inlining is appropriate, so our advice is to be cautious. Consider inlining only when you know that the code generated for the function is small.

A class with inline member functions

 
class Point
{
   public:
      Point(double x, double y);
      // ...
      // accessors
      double x() const;
      double y() const;

      // modifiers
      void   x(double x);
      void   y(double y);

   private:
      double xM;
      double yM;
};

inline
double Point::x() const
{
   return xM;
}

// ...

Point operator+(const Point& p1, const Point& p2)
{
  return Point(p1.x() + p2.x(), p1.y() + p2.y());
}

A negative effect of making a member function inline is that all client code must be recompiled each time the member function changes. This is especially annoying in larger projects with many unstable classes that are used in many places. If this is your situation, consider having all member functions as non-inline. By using inline definition files, you can do that without much effort.

Rule 7.2 Do not declare virtual member functions as inline.

Virtual member functions could often be simple enough for inlining, but they should unfortunately not be declared inline . If a class with virtual member functions is used, some compilers will require that all virtual member functions have implementations that are linked with the program. The reason is that the address of a virtual member function is needed when a function call is dynamically bound. Most compilers generate a table with the address of all virtual member functions, also called the virtual table.

Since inline functions are inline-expanded, they do not have an implementation by default. However if we make an inline function virtual, it must have a definition. Such a definition will then be generated by the compiler and since the inline function is defined in a header file, there is no obvious place to put it. A good place could be in the same object file that contains the definition of the virtual table for the class. What makes things complicated is the fact that the compiler does not always have an obvious place for the virtual table either.

The virtual table needs to be allocated in one of the object modules. Some compilers allocate it in the object module that contains the definition of the first virtual function of the class. If the first virtual function is inline, the virtual table + code for all virtual member functions that are inline could be generated in each object module that uses the class.

All this may seem complicated and it is. This may not be a problem in the future, but with the compilers of today you should avoid having virtual functions that are inline.

Argument passing and return values

Calling member functions is the normal way to make things happen in a C++ program, but ordinary functions are also used. Your code will be easier to understand if function parameters and return values are declared in a consistent way. The performance of your code can also be improved.

RULES
AND
RECOMMENDATIONS

Rec 7.3 Pass arguments of built-in types by value, unless the function should modify them.

Rec 7.4 Only use a parameter of pointer type if the function stores the address, or passes it to a function that does.

Rec 7.5 Pass arguments of class types by reference or pointer.

Rule 7.6 Pass arguments of class types by reference or pointer, if the class is meant as a public base class.

Rule 7.7 The copy assignment operator should return a non-const reference to the object assigned to.

Rule 5.12 , how to implement copy assignment operator.

Rule 7.8 - Rule 7.9 , constness of pointer or reference argument.

Rec 10.2 , validity of pointers and references returned from member functions.

Rec 15.9 , passing integers.

Rec 7.3 Pass arguments of built-in types by value, unless the function should modify them.

Arguments to functions can be passed in 3 ways: by value, by pointer and by reference.

Different types of function parameters

 
void     valueFunc(T  t);    // By value
void   pointerFunc(T* tp);   // By pointer
void referenceFunc(T& tr);   // By reference

Passing arguments by value means that the function parameters are copies of the arguments. If the parameters are pointers or references, the function has access to the arguments. But remember that if an argument is a temporary created by an implicit type cast, the object used to create that temporary will not by modified.

A good rule of thumb is to pass built-in types like char , int and double by value, since it is cheap to copy such variables. This recommendation is also valid for some objects of classes that are cheap to copy, such as simple aggregates of a very small number of built-in types, for example a class that represents complex numbers which often just consists of two double s as data members.

If a function needs access to an argument, then you must pass also built-in types by reference or pointer. This should otherwise be avoided.

Passing parameters by value

 
void func(char c);                // OK
void func(int i);                 // OK
void func(double d);              // OK
void func(complex<float> c);      // OK

Rec 7.4 Only use a parameter of pointer type if the function stores the address, or passes it to a function that does.

Reference and pointer parameters are similar in that both allow a function to modify the arguments. We only recommend pointer parameters if a function stores the pointer value, or if it passes it to another function that does.

Some programmers argue that the code is easier to understand if pointer arguments are used when the function modifies an object, since then you must take the addresses of objects when such functions are called. This would make it obvious, from reading the client code, when a function modifies an argument.

Unfortunately, the implementation of a function will often be more difficult to read if pointer parameters are dereferenced inside expressions. Local references make it easier to understand such complicated expressions. What is not good with this solution is that one more local variable is needed. This makes the function slightly more complex.

Pointer parameters also force the implementation to consider how null-pointers are handled, since dereferencing a 0 -pointer is a fatal error that certainly will crash your program. References cannot be null, which relieves the implementation of the problem of checking if it is null or not.

The implementation of a function taking a pointer as parameter might pass it to some other function, which in its case also might consider the possibility of being passed a null-pointer. It is easy to see that all this easily cascades to endless tests of pointer values.

Therefore we recommend pointers only as a way of showing to the user that the address of the argument is stored by the function for later use, or is passed to a function that does so. Functions with pointer parameters must therefore be treated specially since the client must not delete objects whose addresses are passed to such a function. You should be suspicious if the address of a local object is passed to a function. One benefit with following this recommendation to avoid pointer parameters is that dangling pointers to local objects are easier to detect.

Unless you are careful, pointer parameters may end up being used everywhere within a system with the motivation that "I use a pointer in my interface because internally I have to call that other interface, which takes a pointer as argument". The use of pointer parameters can this way easily spread over a complete program system.

Pointer and reference arguments

EmcMathVector represents a 2-dimensional vector.

 
class EmcMathVector
{
   public:
      EmcMathVector(double x, double y);
      EmcMathVector& operator*=(double factor);

      double x() const;
      double y() const;
      void   x(double x);
      void   y(double y);
      // ...
   private:
      double xM;
      double yM;
};

EmcMathVector::EmcMathVector(double x, double y) 
: xM(x), yM(y)
{
   // empty
}

EmcMathVector& EmcMathVector::operator*=(double factor)
{
   xM *= factor;
   yM *= factor;

   return *this;
}

The question is how we implement a function that modifies the state of a EmcMathVector -object. We could either pass a pointer or reference.

 
EmcMathVector v(1.2, 3.4);

// Not recommended
magnify(&v, 4.0);       // passing pointer

// Recommended
magnify(v,  4.0);       // passing reference

By looking at the implementation, we can see that the implementation of the function taking a pointer will be slightly more complex.

 
// Pointer argument

void magnify(EmcMathVector* v, double factor)
// Not recommended to pass pointer
{
   if (v)             // Pointers might be 0
   {
      *v *= factor;   // scalar multiplication of vector
   }
   // Handle null pointers here in some way: 
   // assert or exception
}

// Reference argument

void magnify(EmcMathVector& v, double factor)
// Recommended to pass reference
{
   v *= factor;        // scalar multiplication of vector
} 
 

Rec 7.5 Pass arguments of class types by reference or pointer.

Arguments of class type are often costly to copy, so we recommend that you pass a reference (or in some cases a pointer), preferably declared const, to such objects. Const access guarantees that the function will not change the argument, and by passing a reference, the argument is not copied.

 
void func(const EmcString& s);       // const reference

Small objects are sometimes more efficient to pass by value, but the default is to assume that arguments of class types are passed as const references. It is a good idea to always read the documentation for the class to make sure whether an object should be passed by value or by const reference.

Template parameters are a problem here, since when declaring template functions you can in many cases not know if a user will pass a built-in or a class type. The thing to do then is to select a way of passing parameters by looking at how costly copying of a template parameter is expected to be. If you anticipate cheap copying, then you should pass parameters by value. Otherwise use references.

Passing arguments of unknown type

A simplified version of the vector class in the standard library is a good example of what assumptions about instantiation-bound types can be made. InputIterator is an argument to a member template and is expected to behave as a pointer. Since pointers should be cheap to copy, InputIterator parameters are passed by value. T is the type of the object stored in the vector, and since the class should work even when T is expensive to copy, T parameters are passed as references. T pointers, on the other hand, are passed by value.

 
template <class T>
class vector
{
   public:
      template <class InputIterator>
           vector(InputIterator first, 
                  InputIterator last);
      T*   begin();
      T&   operator[](size_t n);
      void push_back(const T& x);
      T*   insert(T* position, const T& x = T());
      // ...
};

Footnote:

Member templates are a recent addition to the language. They are motivated by the fact that it is impossible to create smart pointer templates that smoothly replace the ordinary pointers without this new language feature, but there are other uses for them as well. With member template constructors, it is possible to allow a template instantiation to provide a conversion from an otherwise unrelated type to itself. Remember that two template instantiations are different types.

You can instantiate vector<T>::vector with any type that behaves as an
InputIterator . This means that it is up to the client to decide if built-in arrays or iterator classes are used to initialize the vector<T> object. Without member templates, it would have been necessary to make a decision when designing the template.

Rule 7.6 Pass arguments of class types by reference or pointer, if the class is meant as a public base class.

If a class is meant to be a public base class, then you should always pass such objects by pointer or reference. This will as previously described in almost all cases give you better performance, but there are other reasons as well. If a function takes a reference or a pointer to a base class, objects of derived classes can also be used as arguments, since C++ allows a pointer of reference to a public base class to be bound to a derived class object. This is what most often is called polymorphism.

You should never attempt to pass objects of these types by value, since what happens in such cases is that you will encounter what is usually called slicing. You can avoid that problem by only having abstract base classes, or by making the copy constructor private or protected. Since an object of an abstract base class cannot be copied and thus created, the compiler will catch errors of this kind.

Passing base class reference

 
// basic_ostream<charT, traits> is a public base class

template <class charT, class traits = file_traits<charT> >
class basic_ofstream 
   : public basic_ostream<charT, traits>
{
   public:
      explicit basic_ofstream(const char* s,
                              openmode mode = out | trunc);
      // ...
};

typedef basic_ostream<char> ostream;
typedef basic_ofstream<char> ofstream;

ostream& operator<<(ostream& o, const EmcMathVector& v)
{
   o << v.x() << ", " << v.y();
   return o;
}

int main()
{
   ofstream out("hello.txt");
   EmcMathVector v(1.2, 5.5);
   
   out << v << endl;
   // operator<<(ostream&, const EmcMathVector&) called
   return 0;
}

In this case an ofstream object is passed to the operator<< taking a reference to its base class ostream .

Passing base class object by value

It is not possible to pass an object of the class ostream by value, since an ostream object cannot be copied.

 
void uselessPrint(ostream o, const EmcMathVector& v)
// NO: Compile error
{
   o << v.x() << ", " << v.y();
} 
 

Rule 7.7 The copy assignment operator should return a non-const reference to the object assigned to.

The return value from the copy assignment operator should always be a non-const reference to the object assigned to. There are many reasons to this. One is that this is the return value of a compiler generated copy assignment operator. It could be confusing if hand-written copy assignment operators had a different signature than the compiler generated ones. Another reason is that all classes with copy semantics in the standard library have copy assignment operators with non-const return values.

Return value from assignment operators

The following expression is legal when using an int* to access an int array.

 
int*  array = new char[3];
// ... set values
int* arrayPointer;
// assign to first element
*(arrayPointer = array) = 42

If we instead use a smart pointer class to access the array, we want to keep this behavior for objects of that class.

 
EmcAutoArrayPtr<int> smartArrayPointer;
// assign to first element
*(arrayPointer = array) = 42

This requires that the smart pointer class provides the copy assignment operator and that it returns a non-const reference to this .

Const Correctness

Being "const correct" is important when writing code in C++. It is about correctly declaring function parameters, return values, variables and member functions as const or not.

RULES
AND
RECOMMENDATIONS

Rule 7.8 A pointer or reference parameter should be declared const if the function does not change the object bound to it.

Rule 7.9 The copy constructor and copy assignment operator should always have a const reference as parameter.

Rule 7.10 Only use const char -pointers to access string literals.

Rule 7.11 A member function that does not change the state of the program should be declared const .

Rule 7.12 A member function that gives non-const access to the representation of an object must not be declared const .

Rec 7.13 Do not let const member functions change the state of the program.

Rule 5.12 , how to implement copy assignment operator.

Rule 7.7 , return value of copy assignment operator.

Rule 7.8 A pointer or reference parameter should be declared const if the function does not change the object bound to it.

Functions often have const reference or const pointer parameters to indicate that an argument is not modified by the function. A good thing with const declared parameters is that the compiler will actually give you an error if you modify such a parameter by mistake, thus helping you to avoid bugs in the implementation.

const -declared parameter

 
// operator<< does not modify the EmcString parameter
ostream& operator<<(ostream& out, const EmcString& s);

When an argument is passed by value, it is used to initialize a function parameter that will be a copy of the argument. The caller is therefore immune to changes made to that parameter by the called function. If you declare the parameter as const in these circumstances you will just be preventing any change to the parameter taking place in the body of the function. This would be of little help, since not being able to change a parameter passed by value only puts unnecessary constraints upon the programmer implementing the function. If a parameter passed by value is declared const , the value must be copied to a local variable if the value is to be modified by the function.

By not declaring the parameter const , it is possible to use the argument value without first copying the value.

Using parameter as a local variable

 
template <class T>
T arraySum(const EmcArray<T>& array,
           size_t first,
           size_t last)
{
   assert(last <= array.length());

   T sum = 0;

   for( ;first < last; first++) 
   {                            
      // It is possible to update first since
      // it has not been declared const.
      sum += array[first];
   }

   return sum;
}

Rule 7.9 The copy constructor and copy assignment operator should always have a const reference as parameter.

Two particularly important examples of const parameters are the copy constructors and the copy assignment operators, which should always have a const reference as parameter. In almost all cases it is evident that they should not change the object copied from. Being sloppy in this respect can have drastic consequences, since it will force derived classes and containing classes to also take non-const references as parameters.

If a class inherits another class and provides a copy constructor, this only works if that class has a copy constructor that accepts a const reference parameter. If not, the compiler will report an error, since a const object is passed to a copy constructor taking a non-const parameter. The same problem applies to the case when such a class is used as a data member.

If a class does not allow constant objects to be copied, then it cannot be used in many situations where the programmer expects these properties to hold. It could be when the class is used as a template argument, base class or data member.

There are a few rare exceptions to this rule, such as when the copy is destructive; the new object takes over the state of the old object. This is for example the case if a resource or token, such as a message, is passed from an old object to a new object when the old object is copied.

Copyable type parameter

The following template assumes that the type argument T is copyable.

 
// Interface

// T is Copyable
template<class T>
class EmcStack
{
   public:
      // ...
      void push(const T& t);
      // ...
   private:
      size_t  allocatedM;
      size_t  topM;
      T*      repM;
};

// Implementation

// EmcAutoArrayPtr manages arrays of objects

template <class T>
void EmcStack<T>::push(const T& t)
{
   if (topM == allocatedM) // allocate more memory
   {
      size_t newSize = 2 * allocatedM;
      EmcAutoArrayPtr<T> newRep(new T[newSize]);
      for(size_t i = 0; i < topM; i++)
      {
         newRep[i] = repM[i];
      }
      repM = newRep.release();
      allocatedM = newSize;
   }

   // Only works if T is of a type that allows copying 
   // of constants.
   repM[topM] = t;
   topM++;
}

Rule 7.10 Only use const char-pointers to access string literals.

Constness is not always as enforced by the language. A very simple example is string literals that are non-const. It is best to always access such strings through const char -pointers, so that they cannot be modified. What is not commonly known is that according to the language definition they are of non-const type.

When using a const char* instead, the compiler will prevent you from modifying the string literal through the pointer.

Unfortunately this does not guard you from direct assignment to the pointer itself. It is therefore better to either const declare the pointer, or use array notation, since it is not possible to assign to a built-in array.

Accessing string literals

 
// NOT RECOMMENDED
char*             message1   = "Calling Orson";  

// Better
const char*       message2   = "Ice Hockey";     

// Even better
const char* const message3   = "Terminator";     

// Best
const char        message4[] = "I like candy";   

Rule 7.11 A member function that does not change the state of the program should be declared const.

You should declare all member functions that do not modify the state of the program as const . Declaring a member function as const has two important implications:

Only const member functions can be called for const objects.

A const member function will not change data members.

It is a common error to forget to const declare member functions that should be const. If you forget this, then it will be difficult to pass const references or pointers to objects of that class as arguments to functions. It would also be difficult to use const references or pointers returned from functions.

Please note that it is possible for a const member function to change static data members, global data, as well as the objects that pointer data members are pointing at. It is even possible to modify the object operated upon if a non-const pointer or reference to that object exists.

Implications of const

UselessString is a class that has not declared any const member functions.

 
class UselessString
{
   public:
      UselessString();
      UselessString(char* cp);
      UselessString(UselessString& u);

      ~UselessString();

      UselessString& operator=(UselessString& u);

      char*  cStr();
      size_t length();
      char&  operator[](size_t index);
      char&  at(size_t index);

      friend ostream& operator<<(ostream& o,
                                 UselessString& u);

   private:
      // ...
};

A consequence is that the following code, that you would expect to be legal, will not compile:

 
void print(const UselessString& s)
{
   // Should be possible o print a const object
   cout << s << endl;  // Will not compile
}

Accessing objects inside const member function

 
class Silly
{
   public:
      explicit Silly(int val);
      void me(Silly& s) const;      // Odd function
   private:
      int  valM;
};

Silly::Silly(int val) : valM(val)
{
   // ...
}

The odd thing about the declaration of the function me() is that it takes a non-const parameter, which indicates that it might be changed by the function, while the function itself is declared as const . If we look at its implementation we can easily see its peculiarity.

 
void Silly::me(Silly& s) const
{
   // valM = 42;    // Error: cannot modify valM
   s.valM = 42;     // OK but odd:  s is not const
}

If you call the const member function me() with the object operated upon as argument, the object will be modified by the member function call despite the member function's constness.

 
Silly s(7);
s.me(s);     // s.valM == 42, not 7

Rule 7.12 A member function that gives non-const access to the representation of an object must not be declared const.

A member function that gives non-const access to the representation of an object must not be declared const , since the object has no control over possible modifications through such pointers or references. The solution is to properly overload member functions with respect to constness.

Accessing characters in a string

The following piece of code allows a string to be modified by using the indexing operator to access individual characters.

 
EmcString name = "John Bauer";
name[0]  = 'B';             // OK

The implementation returns a reference to a character that is part of the representation for the string and that can be assigned to. Here, the indexing operator indirectly modifies the object.

The EmcString class has overloaded operator[] with respect to constness to prevent const objects to be indirectly modified this way.

 
class EmcString
{
   public:
      EmcString(const char* cp);
      size_t length() const;
      // ...
      // Non-const version
      char& operator[](size_t index); 
      // Const version
      char  operator[](size_t index) const;
      // ...
   private:
      size_t lengthM;  // Length of string
      char*  cpM;      // A pointer to the characters
};

The string is represented by two data members cpM , the character array, and lengthM , the length of the string.

The implementation of the indexing operators are straightforward. They just return a reference to the character specified by the index parameter, as long as the index is within bounds.

 
char& EmcString::operator[](size_t index)
{
   assert(index < lengthM);
   return cpM[index];
}

The compiler would not complain if this indexing operator is declared const , since it is not the pointer cpM that is modified, only what it points at. By doing that, one operator member function would have been enough, which would be a benefit for the person maintaining the class. since the fewer member functions the class has, the easier it is to maintain.

From the user's perspective it would be wrong to const declare the indexing operator returning a reference, since that would open up the possibility that a constant string could change value. Here, the compiler's interpretation of const would not be the same as the programmer's.

 
const EmcString pioneer = "Roald Amundsen";
// pioneer[0]  = 'M';  Should NOT be legal!!

We want to allow each individual character of a const declared string to be accessed, but not modified. The correct way to do that is to overload the indexing operator with respect to constness. The const member function does not return a reference so the string cannot be modified through assignment to the return value.

 
const EmcString s = "hello";

size_t length = s.length();

for (size_t j = 0; j < length; j++)
{
   // OK: Read only
   cout << "char " << j << ": " << s[j] << endl;  
}

Rec 7.13 Do not let const member functions change the state of the program.

A const member function promises, unless cheating, not to change any of the data members of the object. Usually this is not enough as a promise. A const member function should be possible to call any number of times without affecting the state of the complete program. It is therefore also important that a const member function refrains from changing static data members, global data, or other objects which the object has a pointer or reference to. Objects often put some parts of their representation in separate objects and instead have data members that are pointers to these objects. As a complicating factor, it may also be the case that the value of a data member is not part of the state of the object. It could be a value, such as the determinant for a matrix, that was very costly to calculate and therefore cached in an internal data member for efficiency reasons.

If const member functions fulfil their promise not to change the state of the program, then that make them very useful for example as a reliable tool in assertions that checks if the program is in a consistent state. Assertions should be possible to switch off without changing the behavior of the program, which makes it obvious that const member functions must behave as promised.

There are many subtleties involved in this issue. What if there is a log attached to the program, that is used when the program is debugged? Writing to such a log does in some ways affect the state of the program, since it will affect output buffers and the number of open files. The only possible thing to do is to appeal to good engineering judgement.

Overloading and default arguments

Overloading and default arguments in C++ are two straightforward but powerful extensions to C. By avoiding a few pitfalls they can greatly reduce the complexity of a system.

RULES
AND
RECOMMENDATIONS

Rule 7.14 All variants of an overloaded member function should be used for the same purpose and have similar behavior.

Rec 7.15 If you overload one out of a closely-related set of operators, then you should overload the whole set and preserve the same invariants that exist for built-in types.

Rule 7.16 If, in a derived class, you need to override one out of a set of the base class' overloaded virtual member functions, then you should override the whole set, or use using-declarations to bring all of the functions in the base class into the scope of the derived class.

Rule 7.17 Supply default arguments with the function's declaration in the header file, not with the function's definition in the implementation file.

Rec 13.4 , overloaded functions replace functions with an unspecified number of arguments.

Rec 10.6 - Rec 10.7 , specifying behavior of member functions.

Rule 7.14 All variants of an overloaded member function should be used for the same purpose and have similar behavior.

Different member functions can be used for essentially the same purpose. By giving all member functions the same name, this fact can be made explicit to the user of a class. This is called function name overloading.

Using function name overloading for any other purpose than to group closely related member functions is not recommended and would be very confusing.

Overloaded member functions

When working with strings, we sometimes want to know how many occurrences of a character or a substring it contains. The string class EmcString overloads the name contains for both these operations.

 
EmcString cosmonaut("Juri Gagarin");

char c = 'a';
bool cValue = cosmonaut.contains(c);  
// cValue == true

EmcString uri("uri");
bool uriValue = cosmonaut.contains(uri);
// uriValue == true

By giving the member functions the same name, the code will be more readable since only one name, contains, must be remembered by the programmer.

Different versions of contains should also have the same behavior.

Rec 7.15 If you overload one out of a closely-related set of operators, then you should overload the whole set and preserve the same invariants that exist for built-in types.

If used correctly, operator overloading can improve the readability of the code. This is the case for classes that represent mathematical quantities such as complex numbers and for classes that replace arrays or pointers.

C++ programmers expect that all operators in a set of closely related operators are available.

For example, if a class provides == for comparing two objects of the class, it should also provide != .

In general, many relationships between operators can be described as a set of invariants.

For example, if a and b are int s and if a != b is true, this implies that !(a == b) is also true. The same property should hold if a and b are objects of a class.

The general recommendation is that if you overload operators, provide all operators in a closely related set of operators and preserve the invariants that are valid for built-in types.

Operator overloading

If a class provides copy assignment and operator==() , two objects are expected to be equal after assigning one of them to the other.

 
Int x = 42;
Int y = 0;
x = y;
// x == y should be true

If a class provides the comparison operators, < , <= , > and >= , we expect that an object can either be lesser, greater or equal to another object. For example, if we have a function max that returns the largest of two operands, it should not matter what operator is used in the implementation.

 
Int max(Int x, Int y)
{
   if (x > y) // could use: < instead
   {
      // We also expect that:
      // y < x
      return x;
   }
   else
   {
      // We also expect that:
      // x <= y
      return y;
   }
}

It can be useful to preserve an invariant by using an operator member function in the implementation of another closely related operator member function. You could say the invariant is the implementation, since it defines how to implement an operator function in terms of another overloaded operator function.

Implementation of closely related operators

EmcString overloads operator ==() and operator !=() . The implementation of operator!=() compares two strings and returns true if they are not equal.

 
bool EmcString::operator!=(const EmcString& s) const
{
   if (lengthM != s.lengthM)  
   // Different lengths means that strings are different
   {
      return true;
   }
   else
   {
      return (strcmp(cpM, s.cpM) != 0);
   }
}

To check if two strings are equal, we can simply negate the result of operator !=() . By doing that, less code is needed to implement operator ==() .

 
bool EmcString::operator==(const EmcString& s) const
{
   return !(*this != s);  // operator!= used here
}

Rule 7.16 If, in a derived class, you need to override one out of a set of the base class' overloaded virtual member functions, then you should override the whole set, or use using-declarations to bring all of the functions in the base class into the scope of the derived class.

Mixing overloading and inheritance can be tricky. A problem is that if you in a derived class override only one of the overloaded virtual functions in the base class, then the functions not overridden will be hidden for all users of the derived class.

Both virtual and non-virtual member functions can be hidden. A hidden member function can only be called when the object is accessed through a base class pointer or reference, but not directly.

Hidden member functions will make the code more difficult to understand. The same expression could mean different things depending on how the object is accessed. Implicit conversions must be taken in consideration and the programmer must be of aware of what versions of the overloaded function that are hidden for both base classes and the actual class.

Hiding member functions

 
class Base
{
   public:
      // ...
      void f(char);
      void f(int);
      virtual void v(char);
      virtual void v(int);
};

Derived inherits Base and provides some of the overloaded functions.

 
// NOT RECOMMENDED

class Derived : public Base
{
   public:
      Derived();
      // ...
      void f(int);
      virtual void v(char);
};

Different member functions will be called depending on how Derived is accessed. For example, if v uses f for its implementation, the result could be surprising.

 
void Derived::v(char c)
{
   f(c);      // calls Derived::f(int), not Base::f(char)
   v((int)c); // recursive call to Derived::v(char)
}

If the object is accessed within the scope of Base or through a Base pointer or reference, the result of overload resolution will be different.

 
Derived d;
Base& bref = d;
char c = 'a';

bref.f(c);      // calls Base::f(char)
bref.v(c);      // calls Derived::v(char)
bref.v((int)c); // calls Base::v(int)

It is not always wrong to hide member functions. A good example is a non-virtual comparison member function that takes a reference to another object as argument. It can be difficult to compare objects of different types. You will need run-time type checking or define the comparison entirely in terms of virtual functions. If you in a derived class know how to compare two objects of that class efficiently, you may want to hide the more general comparison function so that it is only used when operating on base class pointers or references.

If the member function would have been declared virtual , the derived class could instead have replaced it with a more efficient version.

A virtual member function should be overridden to replace the base class implementation, not to hide any names in the base class. The natural thing is to always make all inherited virtual member functions that are accessible in the base class also accessible in the derived class. It would be very strange if different virtual member functions are called depending on how the object is accessed.

If your compiler does not implement namespaces, you will have to reimplement the member function.

Inheriting overloaded virtual member functions

Suppose the template EmcBoundedCollection<T> inherits from EmcCollection<T> . Objects of the same derived class are possible to compare more efficiently than if the objects are of different classes. This is the reason to why the member function isEqual is overloaded in the derived class, but to avoid surprises the base class version is also made accessible.

 
// Stores any number of values

template <class T>
class EmcCollection
{
   public:
      // ...
      virtual bool isEqual(const EmcCollection<T>&) const;
      bool operator==(const EmcCollection<T>&) const;
};

In a derived class it is OK to hide the non-virtual operator==() , but not
isEqual( ).

 
// Stores a limited number of values

template <class T>
class EmcBoundedCollection : public EmcCollection<T>
{
   public:
      // ...
      using EmcCollection<T>::isEqual;
      virtual bool 
         isEqual(const EmcBoundedCollection<T>&) const;
      bool 
         operator==(const EmcBoundedCollection<T>&) const;
};

Rule 7.17 Supply default arguments with the function's declaration in the header file, not with the function's definition in the implementation file.

Default arguments are a surprisingly complex area of C++. For example, it is possible to redeclare a function several times with different default arguments. We firmly believe that it is best to use default arguments only with the declaration of a function in the header file, not to make functions simpler to call in the implementation file. Such tricks tend to make the code more difficult to understand.

Adding default arguments

 
void f(int x, int y = 2);

// 50 lines of declarations later

void f(int x = 1, int y); // NOT RECOMMENDED

If you call f without specifying any arguments, the default arguments will be used.

 
f();     // calls f(1,2)

Default arguments for member function

 
// operator() returns 0 if a generated internal 
// random double between [0,1) is > limit. 
// Else return 1.

class RanDraw
{
   public:
      enum RanType {Fast, Good};
      RanDraw( double limit, int seed, RanType t = Good );
      // Default argument for t in class definition

      // ...
};

RanDraw::RanDraw(double limit, int seed, RanType t)
// No default arguments outside class definition
// ...
{
   // ...
} 

Conversion functions

It can be difficult to understand C++-code that uses implicit type conversions between otherwise unrelated types. Your classes can be designed to prevent such code by removing one-argument constructors and conversion functions.

RULES
AND
RECOMMENDATIONS

Rec 7.18 One-argument constructors should be declared explicit .

Rec 7.19 Do not use conversion functions.

Rec 6.1 - Rec 6.3 , a more general discussion about conversions.

Rec 15.14 , if your compiler does not support explicit.

Rec 7.18 One-argument constructors should be declared explicit.

Implicit type conversions are bad since the behavior of existing code can change when new such conversions are added, and it is difficult to know what function that is called when looking at the code.

If an object of a type is passed as argument to a function, it is natural to expect to find a function taking that type as parameter.

If implicit type conversions are used, it is no longer that easy. A programmer must also check all implicit type conversions for the argument type in order to find out which function that actually is called. This search can be quite difficult to do manually since some conversions might be defined by an otherwise unrelated class.

A good way to improve the situation is to avoid implicit type conversions and to prevent the client from depending on them.

By default, all one argument constructors can be used for implicit type conversions. All one-argument constructors should therefore be declared as explicit to prevent them from being called implicitly. The keyword " explicit " is a recent addition to the C++-language and may not yet be supported by your compiler.

One-argument constructor

 
class Other
{
   public:
      explicit Other(const Any& a); 
      // No implicit conversion from Any
      // ...
};

Since the class Other declares the constructor as explicit , the type must be specified when using an Any object instead of a Other object.

 
void foo(const Other& o);

Any any;
// foo(any);         // Would not compile
foo(Other(any));     // OK

Rec 7.19 Do not use conversion functions.

Conversion functions introduce an implicit conversion from a class to another type. You should avoid them and instead use ordinary functions to get a value of another type.

How to avoid conversion operator function

Our string class EmcString provides a member function cStr() for the purpose of returning the string representation as a const char *.

 
class EmcString
{
   public:
      // ...
      const char* cStr() const; 
      // conversion to const char*
      // ...
};

void log(const char* cp);

EmcString magicPlace("Ngoro-Ngoro crater at dusk");

log(magicPlace.cStr()); 
// Explicit conversion from String to const char*