Saturday 21 December 2013

Art of C++ Class Design

Art of C++ Class Design
==================

C++ is multi paradigm programming language. It supports object oriented programming,
dynamic type programming(using template), functional programming, system/embedded
programming. Class is the fundamental part of C++ and of course any object oriented
programming.

If we take any software engineering principle, it talks about
some level of indirection to design a software with loosely coupled system and highly
cohesive data management. For developing any large-scale system, we need to be focus
for code flexibility(easy to add extra functionality) and easy of maintenance(provide
good readability).

Correctness of class design is more important in language like C++, So we need to take
special attention while managing memory, low-level functions and data. Comprehensive
thinking of fundamentals of C++ class member functions are mandatory for correctness
of C++ class design. Such operations are initialisation, copying object, assignment and
cleanup. These operations are vital for any non-trivial C++ class. In this article we
'll see non-exhaustive list of C++ class design techniques. By giving proper consideration
to these techniques, we could ensure the correctness of C++ class implementation. However,
this article is limited to non-exhaustive list of such techniques to focus only the core
principles. So the examples are, only to the point intended for the design explained here.

1. Designing your own Initialisation, Copy and assignment operations
==================================================

Compilers are very smart. Compilers put stuff/functions automatically for our class
if we are not providing/designing them. But Its not always right to believe compiler
synthesised functions will suite for our class design.
For a class X{ }; following are minimal list of functions synthesised automatically.

X();                                    // default constructor
X(const X&)                      // copy constructor
X& operator=(const X&)   // assignment operator
~X()                                   // destructor

If programmer provide implementation for any of the functions above, then compiler
uses the programmer's definition. But how to decide whether we need our own implementation
or not?

In general, if you have a class, and you need to support assignment operation of objects of
class while initialisation(For Class X; X a=b; X a(b);), or if you need to pass object
as call by value parameter to the function(foo(a);), then we need copy operation.

Also we must implement our own operator=() function when our class contain pointer to object,
or the class destructor has delete-expression. Then we need custom assignment operation.

The above reason enable us to define our own copy and assignment operations. Since
compiler synthesised version perform plain copy of memory address. Which leads to double
deletion problem when the objects goes out of scope or when deleted. This is fatal.
Here is the customised copy and assignment operation shown below to avoid double deletion
problem where class allocate and de-allocate resource.

class String
    {
     public:
           String();              

           String(const String & s)                                            // copy constructor
           {
           str = new char[s.length() + 1];
           strcpy(str, s.str);
           }

           String & operator=(const String & s )                      // assignment operator
           {
              if(this == &s) return;                                             // check for self assignment
           
              char * temp = new char[s.length() + 1];                // exceptional safe allocation
              strcpy(temp, s.str);                                               // exceptional safe copy
              delete [] str;                                                         // delete old data
              str = temp;                                                           // *this, having new data

              return *this;                                         // result is reference to the object, for l-value support
           }

           ~String()
            { delete [] str; }  // destructor takes no arg, and de-allocate the resource


           // basic setter, getter, length, concat functions are assumed as added

     private:
           char *str;
   };


We 'll see default constructor design now. Suppose if a class has any constructors with
arguments, then compiler suppress the default constructor synthesising, so if we need to declare
objects of that class without explicitly initialisation, then we must explicitly write a
constructor that takes no arguments. Example:

class Point
    {
      public:
                Point(int x, int y):p1(x),p2(y){}  // initialisation list
      private:
                int p1, p2;
    };

Point op;      // error : no default constructor(no way to initialise object 'op')

Point ob[10];  // error : same above

Conclusion:
In general, While designing C++ class, design your initialisation and assignment
differently. Because these are two different operations for different purpose.
Assignment occurs when we assign. All other copying performs initialisation.
Like, initialisation during declaration, function return, argument passing and
catch exceptions.

2. Designing destructor for a class
=========================

Not all class with constructor need a destructor. If a class does not allocate resource
dynamically, such class member datas are automatically de-allocated by default
destructor, so we do not need to write our own destructor(though we define our own
construct). In other way, If any class resources are not freeing the allocation automatically
then such class should have explicit destructor. Only some class need a virtual destructor.
For Example:

class A { String s1; };

class B:A { String s2; };

int main()
    {
       A* ap = new B();
       delete ap;    // calls the wrong destructor until A has virtual destructor

       return 0;
    }


Conclusion:
If the class has new-expression in the constructor then that class should have
delete-expression in its destructor. And, virtual functions of any kind are useful only
in the presence of inheritance. When your are executing a delete-expression, if the
actual intention of delete points to its derived class destructor, then, that base class's
destructor needs to be virtual; Otherwise, not.

3. Control operations in class design
===========================

By making copy constructor and operator= as private, we can force the user/client code to
not copy objects of that class. If we don't use these member functions from any other
members, then its enough to define them as below.

class NoCopy
      {
       private:
               NoCopy(const NoCopy &);
               NoCopy& operator=(const NoCopy&);
      };

int main()
     {
            NoCopy obj1;            // OK
            NoCopy obj2(obj1);  // error : copy constructor private
            NoCopy obj3 = obj1; // error : copy constructor private
            obj1 = obj2;               // error : assignment operator private

       return 0;
     }

By declaring destructor private we can prevent stack allocation.

class OnlyHeap
     {
         private:
                ~OnlyHeap(){ }
         public:
                static void deleteHeap(OnlyHeap *d){ delete d; }

     };

int main()
     {
            OnlyHeap *obj1 = new OnlyHeap();  // OK
            delete obj1                       // error : calling private destructor
            OnlyHeap obj2;  // error : during end of scope, call's private destructor
            OnlyHeap::deleteHeap(obj1);       // OK

       return 0;
     }


By declaring operator new private we can prevent heap allocation.

class OnlyStack
     {
          private:
                 class temp{};
                 void * operator new(size_t, temp);

     };

int main()
      {
             OnlyStack obj1;                   // OK
             OnlyStack obj2 = new OnlyStack(); // error : no new operator to call

        return 0;
      }

Conclusion:
private access specifier gives great control over how class's object should behave
during object creation/resource allocation and member operations. This is useful
idiom to suppress class operations. Use it wisely.

2 comments: