[View INPRISE Home Page][View Product List][Search This Web Site][View Available Downloads][Join Inprise Membership][Enter Discussion Area][Send Email To Webmaster]
INPRISE Online And ZD Journals Present:

C++Builder Developer's Journal


Concrete data types
By Jody Hagins


A concrete data type (CDT) is a user-defined type that behaves just like a built-in type. In other words, a concrete data type can be created, deleted, assigned, copied, and passed as a parameter just like any C++ built-in type. This is a powerful feature that allows you to correctly make certain assumptions about how a specific class will behave. In this article, we'll learn how to build concrete data types, and, along the way, pick up on some subtleties of the C++ language itself.

A simple CDT

The simplest CDT you can create is
class Nada { };
Now, I admit that on the surface this class isn't very interesting. However, looks can be deceiving. This class may look like nothing, but it is, in fact, a complete and correct CDT. When the compiler is through with this class, it will look something like this:
class Nada
{
public:
  Nada() { }
  ~Nada() { }
  Nada(Nada const &) { }
  Nada & operator = (Nada const &)
    { return *this; }
  Nada * operator & () { return this; }
  Nada const * operator & () const
    { return this; }
};
For the purists among us, I should mention that the compiler generates only those methods that are actually called by the program. In other words, if you never create an instance of class Nada, the compiler won't generate any code. As another example, if your code doesn't call the copy constructor, the compiler won't generate one. A special note should also be made of the destructor. The compiler won't really generate a destructor for this class. In fact, all of the following conditions must be true for the compiler to generate a destructor for any class:
  • The class itself doesn't declare a destructor.
  • The class is derived from another class.
  • The base class has a destructor.
Also note that the compiler-generated destructor will be public, and will take on the virtual characteristic of the base class destructor. The Nada class may not be very interesting, but it gives us a unique look at the minimal requirements for a class to be a CDT. For a class to be considered a CDT, it must have correct public implementations of the following:
  • Default constructor
  • Copy constructor
  • Assignment operator
  • Destructor
For a given class, the compiler-generated code for any of the above may constitute a correct implementation. Then again, the class most likely will need to specifically define each of the above. We'll examine these requirements in more detail as we take the MyClass example from the "Constructing constructors" article, and make it into a CDT.

The constructors

The "Constructing constructors" article explains the ins and outs of constructors, so we won't dwell on them too much here. However, it's worth pointing out that a CDT copy constructor must construct the instance in such a way that it remains valid even if the object being copied is deleted. The MyClass constructors are fine the way they are, so they don't need to be modified in our CDT conversion process.

The destructor

We previously discovered that, in some cases, the compiler doesn't even create a constructor for a class. In these cases, how does the object clean up its member variables? When the compiler calls an object's destructor, the first thing it does is execute the code inside the destructor's code block. Then it automatically cleans up the member variables by destructing them--in the reverse order in which they were created. Next, it walks up the inheritance hierarchy, doing the same thing for each base class. If you don't declare a destructor, the compiler does the exact same thing, it just has no destructor code block to execute. Thus, you can rest safely, knowing that your member variables and base classes will be destructed, even if there is no explicit destructor. Symmetry is important in class design. In almost all cases, the destructor should perform the inverse operation of the constructor. Specifically, it should clean up any dynamic memory, close any open files, etc. So, in our attempt to convert MyClass into a CDT, we need to add the following code:
~MyClass() { free(data); }

The assignment operator

The assignment operator (also known as operator=) is often misunderstood. It's not the same as the copy constructor. Remember, the copy constructor actually constructs a brand new object instance, using another object instance to initialize itself. The assignment operator, however, assigns the value of one object instance to another, already existing instance. This is a subtle difference, but one which is very important.

As an example, consider a first attempt at writing a MyClass assignment operator:

void operator=(const MyClass& mc)
{
  x = mc.x;
  y = mc.y;
  data = strdup(mc.data);
}
This is a typical implementation, and it makes sense. In fact, it's identical to the copy constructor. After all, you're copying the data. However, the assignment operator only applies to existing objects. From our previous constructors, we can see that all existing objects have a data pointer that points to allocated memory. In this code, however, we're overwriting our data pointer, which causes a memory leak. What we really want to do is free this memory, and then assign it.

Our next attempt would look like this:


void operator=(const MyClass& mc)
{
  free(data);
  x = mc.x;
  y = mc.y;
  data = strdup(mc.data);
}
Now, that's more like it. We've eliminated the memory leak. Unfortunately, as is often the case, fixing one problem has now caused another. Consider the case where a MyClass object is assigned to itself. The data pointer for the instance being assigned is freed. However, this pointer is the same pointer as that for the instance that it's being copied from (since they're the same instance). Thus, the strdup function will now receive a pointer to memory that has already been freed. This is obviously not good news, and brings us to another attempt, in which we check to see if we're assigning an object to itself:

void operator=(const MyClass& mc)
{
  if (this != &mc)
  {
    free(data);
    x = mc.x;
    y = mc.y;
    data = strdup(mc.data);
  }
}
Ahhh. Now, surely, all is well with the world. We've eliminated all the bugs from the assignment operator. While this may be true, we still don't have a CDT. Remember the definition of a CDT? It must be able to be used just as any built-in data type. Consider the following code:

int i, j, k;
i = j = k;
MyClass x, y, z;
x = y = z;
The i = j = k line compiles (and rightly so). However, the x = y = z line doesn't, because the assignment operator returns a void. Thus, for a class to be a CDT, the assignment operator must return a reference (MyClass&) to the object being assigned. Why a reference? Why not an object, or a const reference? The answer to this question is hidden in another example of what's permitted for built-in types. Consider the following code:

int i = 1;
int j = 2;
int k = 3;
(i = j) = k;

First off, let me say that this won't even compile with a C compiler, but it's perfectly legal C++. What are the values of i, j, and k after the assignment? Let's break it down. First, the parenthesized expression is evaluated, which sets i to the value of j. So, i and j are both currently equal to 2. Now the return value of that expression is set equal to the value of k. However, what's the return value of the parenthesized expression? In C, the return value would be 2, which is not an l-value (thus the compiler error). In C++, the return value is i, with i treated as an l-value. Thus, this line of code leaves i and k both equal to 3, and j equal to 2.

So, if the assignment operator of a class returns an object, or a constant reference to an object, you can't write code like this. Now, some would argue that you shouldn't write code like this in the first place, but that's beside the point. The fact of the matter is that if you want your classes to be CDTs, then you must return a reference to the assignee. The final version of the assignment operator is as follows:


MyClass& operator=(const MyClass& mc)
{
  if (this != &mc)
  {
    free(data);
    x = mc.x;
    y = mc.y;
    data = strdup(mc.data);
  }
  return *this;
}
Thus, in general (there are exceptions), the assignment operator first does the work of the destructor, and then does the work of the copy constructor.

Derivation and CDTs

If you want your derived classes to behave as CDTs, then you must ensure that the big four are implemented correctly. The implementation of the derived class is identical to what we learned about any CDT. However, when you derive a class from a base class, there are several issues you must remember. First, there are some gotchas related to writing constructors for derived classes. Those can be found in the article, "Constructing constructors". Next, we must concern ourselves with the destructor. Since the compiler takes care of calling the base class destructor, we really have nothing special to worry about. However, as a note, any class that's intended to be used as a base class should have a virtual destructor. Note that if the base class has a virtual destructor, and the derived class doesn't declare a destructor, the compiler will generate a virtual destructor for the derived class. Finally, for the assignment operator, we must address a similar issue as we saw for the copy constructor. If the derived class doesn't declare an assignment operator, the base class assignment operator will be called, followed by a member-by-member assignment of the members in the derived class.

However, if the derived class does declare an assignment operator, the base class assignment operator won't be called at all, unless the derived class assignment operator specifically calls it. Thus, a derived assignment operator would look like this:


Derived& operator=(const Derived & d)
{
  if (this != &d)
  {
    MyClass::operator=(d);
    i = d.i;
  }
  return *this;
}

Conclusion

Concrete data types are very powerful, in that, once built, they can be used just like built-in types, without special consideration to how and when they can be allocated, and in what manner they can be used. Granted, there are some special considerations to be taken, but once the implementation is sound, the user of the classes needn't be concerned with any hidden subtleties. We encourage you to create concrete data types out of all your classes.


Back to Top
Back to 1999 Index of Articles

Copyright © 1999, Ziff-Davis Inc. All rights reserved. ZD Journals and the ZD Journals logo are trademarks of Ziff-Davis Inc. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis is prohibited.
Receive FREE Weekly C++ Builder Developer's Journal Tips in email.
Trademarks & Copyright © 1998 INPRISE Corporation.