Channels ▼
RSS

C/C++

Designing C++ Classes

Source Code Accompanies This Article. Download It Now.


NOV92: DESIGNING C++ CLASSES

Steven is a software-design engineer at Microsoft and project leader for the Microsoft Foundation Classes C++ Application Framework for Windows. He can be contacted at One Microsoft Way, Redmond, WA 98052 or via stevesi@microsoft.com.


C++ is rapidly gaining acceptance as the language of choice among professional software developers. Unlike the transition we made from Pascal to C about ten years ago (which was essentially a syntactic change), the transition from C to C++ poses a unique set of challenges, one of which is the understanding of what the C++ compiler does behind your back. For instance, to maintain the consistent semantics of the C++ language, a number of functions are required for each C++ class. If you do not provide an implementation for each of these functions, a C++ compiler is required to generate default implementations. Unfortunately, most compilers generate these functions silently, and often the default behavior is not adequate for a user-defined class. This article details the four functions that C++ generates when your program does not provide definitions for them: default constructor, copy constructor (or copy initializer), destructor, and assignment operator.

As you know, a constructor for a class is a special function with the same name as its class that is used to initialize the member variables of a class, for example CMyClass::CMyClass(). A constructor is called whenever an object needs to be created (such as for a global variable, a local variable, a dynamically created variable, or an implicit or explicit temporary object, as well as when an object is part of another object via membership or inheritance). A constructor can have any number of formal parameters. The special case of no formal parameters (or any number of formals, each with a default value) is called the default constructor. If you do not supply any user-defined constructors, the compiler will generate a public default constructor for your class. This constructor does nothing, but it will invoke the constructors for any base classes and embedded objects. Usually, a compiler-generated default constructor is harmless. If you supply a constructor with arguments, and you wish consumers of your class to be able to create arrays of objects, then you'll need to supply a default constructor. Along with a constructor, your classes will also get a destructor, a special function with the same name as your class but prefaced with a tilde (~), for example CMyClass:: ~CMyClass(). As with the default constructor, the compiler will generate a default-destructor implementation that will invoke the destructors for any embedded objects and base classes. If you implement a destructor, you should reverse the effects of your constructor. The tricky part about destructors is that your destructor should almost always be made virtual. If you don't make your destructor virtual, then, as Example 1 illustrates, bad things will happen. In Example 1, the destructor is not virtual, and as a result, the wrong destructor is invoked when the object is deleted.

Example 1: If destructors aren't virtual, bad things will happen.

  class CBase {
  public:
      CBase (); // user-supplied constructor
      ~CBase (); // user-supplied destructor

     // other functions and variables
  };
  class CMyClass : public CBase {
  public:
      CMyClass (); // user-supplied constructor
      ~CMyClass(); // user-supplied destructor
     // other functions and variables
  };
  void main () {
      CBase* pBase = new CMyClass;
      delete pBase;
          // wrong destructor invoked because ~CBase () is not virtual
  }

The assignment operator CMyClass& CMyClass::operator=(const CMyClass& src) and copy constructor CMyClass::CMyClass(const CMyClass& src) are similar and often confused. To illustrate the difference between them, the code in Example 2 shows where each is called. Even though the declaration in line 4 looks like an assignment, it differs from the true assignment operation on line 3 because it is a variable initialization (mInit is being initialized to m0). The default implementation of a compiler-generated assignment operator or copy constructor is to perform a member-wise assignment or copy of the object. In the case of the assignment operator, the default implementation returns a non-const reference to the destination object so that assignments may be chained together. (You may return a const reference, or even have a void function if you desire.) If your class has any dynamically allocated data in it, then you must always implement both an assignment operator and a copy constructor. If you don't, then again, strange things will happen.

Example 2: Differentiating between the assignment operator and copy constructor.

  /* 1 */ extern CMyClass m0;
  /* 2 */ CMyClass m1;          // invokes default constructor
  /* 3 */ m1 = m0;              // invokes assignment operator
  /* 4 */ CMyClass mInit = m0;  // invokes copy constructor

For example, consider a string class that maintains a char* pointer to dynamically allocated information. If you assign one string object to another and rely on the default implementation of the assignment operator, the char* member variable will be copied to the destination. The result will be two objects with character points that point to the same string. Undoubtedly, one will be destroyed and free the string memory (in the destructor), while the other string will continue to live, causing memory-trashing bugs at run time! The correctly implemented string class will make a copy of the string data (by dynamic allocation and performing strcpy, for example) in a user-defined assignment operator.

One other thing to consider when writing these functions is to be sure to test for assigning to yourself, as in m1 = m1. Though most of us do not write code like that, it can often result from a complex expression.

Each of these four functions can be modified by the standard C++ access-protection keywords: public, protected, and private. (The generated functions are always public). Thus if you wish to prevent assigning one object to another, all you need to do is make the assignment operator and copy constructor private members. One useful trick, which we used in the Microsoft Foundation Classes (MFC, the C++ application framework for Windows) was to make the copy constructor and assignment operator private members in the common base class. This will always prevent the compiler from generating these functions in all derived classes. As a matter of fact, a compile-time error will be generated, which is always preferable to a runtime error.

This brings us to what is called the "canonical class form." In MFC, all our classes follow a standard template, unless we have a reason to limit the functionality of a class. If you follow this canonical class form, then classes you write will behave much like intrinsic types (int, char, and so on), since they can be created, assigned, and copied just like variables of intrinsic types.

But as stated, MFC implements a private assignment operator and a private copy constructor in the CObject class; see Example 3. This means that derived classes can safely not supply implementations of these functions, especially since it often doesn't make sense to permit two objects to be equal (for example, a class that maintains a reference to some system-allocated resource, such as a file).

Example 3: A private assignment operator and a private copy constructor in the CObject class.

  class CAnyClass : public CObject {
      CAnyclass ();
      virtual ~CAnyClass ();
      CAnyClass (const CAnyClass& src);
      CAnyClass& operator=(const CAnyClass& src);
  };

C++ is a great engineering tool and most certainly makes it easier to write more type-safe and maintainable programs. Unlike its predecessor C, however, C++ does a number of things behind your back. If you don't take this into account when designing your classes, they'll be less useful and not as robust as they could be.


Copyright © 1992, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video