Advertisement

Demystifying constructors

January 31, 2011

Dan_Saks-January 31, 2011

Even the experienced C++ programmer can be confused about what exactly constructors do and when they get called.

One of the easiest ways to misuse a structure object in C is to fail to initialize it properly. In C++, a class can have special member functions, called constructors, that provide guaranteed initialization for objects of that class type. The guarantee isn't absolute—you can subvert it using a cast. Nonetheless, using constructors can reduce the incidence of uninitialized objects.

While most C++ programmers use constructors frequently, I keep running into C++ programmers, even experienced ones, who seem to misunderstand how constructors really work. They're surprised, and somewhat dismayed, when a seemingly simple statement generates a flurry of constructor calls that they didn't expect.

Initialization is rarely optional. When it doesn't get done, subsequent operations often fail. However, initialization can be a problem when it happens at unexpected times, especially when the affected code is time-critical.

This month, I'll start to take some of the mystery out of when constructors execute and what it is that they actually do. As I often do, I'll explain the behavior of C++ by showing equivalent code in C. If you're a C programmer who doesn't use C++, I think you'll still find these insights helpful. C code that mimics the discipline imposed by C++ is often better code.

Shallow parts vs. deep parts
I'll begin by introducing a little terminology that should simplify the remaining discussion.

Consider an abstract type that implements a ring buffer of characters. A ring buffer is a first-in-first-out data structure. Data can be inserted at the buffer's back end and removed from the front end. The C++ definition for a ring buffer class might look, in part, like:

class ring_buffer
    {
    ~~~
private:
    char *base;
    size_t size;
    size_t head, tail;
    };

Member base represents an array that holds the buffered characters. Member size represents the number of elements in that array. Members head and tail are the indices of the elements at the buffer's front and back ends, respectively.

As I explained in a prior column, a class without base classes and virtual functions, and with all data members having the same accessibility (all public or all private), has essentially the same storage layout as a structure containing the same data members in the same order.1 Thus, for example, the ring_buffer class above has the same storage layout as a C structure defined as:

typedef struct ring_buffer ring_buffer;
struct ring_buffer
    {
    char *base;
    size_t size;
    size_t head, tail;
    };

In truth, member base stores a pointer to the initial element of the array, not the array itself. The array is part of ring_buffer 's implementation, but it's not one of the data members. The array occupies storage allocated separately from the ring_buffer object.

The ring_buffer is an example of a class with deep structure. A class has deep structure if it has at least one data member that refers to separately-allocated resources managed by the class. Classes with members that are pointers to dynamically-allocated memory are the most common classes with deep structure. However, a class with a member of any type that designates a separately-allocated managed resource, such as an integer designating a file, also has deep structure.

Obviously, not all classes have deep structure. For example, a class representing complex numbers typically has just two data members of some floating-point type, as in:

class complex
    {
    ~~~
private:
    double real, imaginary;
    };

Nothing in this class refers to resources beyond the data members. Such classes have shallow structure.

The shallow part of an object is the storage that contains the object's data members, as well as its base class sub-objects, vptr and padding, if any. (I briefly described base class sub-objects and vptrs in an earlier column.)1 The sizeof operator applied to a class object (or the class itself) yields the number of bytes in the shallow part of the object (or class).

The deep part of an object is any storage used to represent the object's state beyond the shallow part. Objects with shallow structure have no deep part.

< Previous
Page 1 of 4
Next >

Loading comments...