A C++ Standard At Last

Dr. Dobb's Journal February 1998

By Al Stevens

Al is a DDJ contributing editor and can be contacted at astevens@ddj.com.

On November 14, 1997, the combined ANSI X3J16 and ISO WG21 C++ committee approved their latest draft and submitted it for official sanction. When you read this, you will have available to you at long last an approved standard definition of the C++ programming language. The target is still and stable. You can download a copy of the specification document in Adobe PDF and Postscript formats from ftp://research.att.com/dist/c++std. At least, that's where you get the December draft, which is what has been released to the public at the time I am writing this.

It has taken eight years to reach this point. When someone asks why it took so long, committee members are quick to point out that it took the C committee almost as long -- 1983 to 1990. Which only generates sympathy for the C committee members; they had no one's example to follow and no precedent to blame for lengthy deliberations. Imagine how long it would have taken the C++ committee if a significant part of their work was not already done for them by the C committee. Others might ask why things take so long. I don't. Given the international implications of the effort, the number of people and special interests involved, the relatively few times they were able to meet as a body, their self-imposed charter to invent as well as codify language and library, and the fact that the committee consists mostly of unpaid volunteers who also have to hold down jobs, I ask how they possibly did it in only eight years.

But, at long last, we have a standard. What are the consequences of this historic event? Well, it depends on who you are.

If you are a compiler or library developer, you now have a stable target. You can begin to implement development environments that comply. You will learn of ambiguities in the specification and develop your own interpretations. You will deal with those issues related to the language and its library that are designated as being implementation dependent, in other words, issues that are left for you to resolve. You have to determine appropriate compiler and run-time behavior for those code idioms where the specification fails or declines to define behavior. You are finally required to build in all the features you opposed because they looked too hard to support based on how your particular product is structured. You no longer have that convenient excuse for ill-behaving compilers and libraries: "The standard is not approved; anything might be correct." You no longer have sanctimonious justification to release those never-ending new versions to capture more and more developer revenue: "The committee has approved some more changes; we have to comply."

If you are but a mere programmer caught up in all this fuss, you must decide now whether to upgrade to the next version of your compiler, bound to be different from the last and guaranteed, because of committee repentance of past language transgressions, to break existing code. You must set about to learn all that's new and improved in Standard C++ as opposed to traditional C++. You can wait and see, or wade in and see, plus, plus.

If you are a computer programming author like me, you get to sign yet another book contract and collect yet another royalty advance to write yet another edition of your epic and deathless C++ tome.

C++ References

C++ programmers have two kinds of references to worry about: the ones on their résumés and the ones in their programs. I can't help you much with your resumé, but I can comment somewhat on the enigmatic C++ reference. Experienced C++ programmers understand the behavior of references, but, as with much of C++, there are options, surprises, and restrictions -- and there are tricks for sidestepping the restrictions.

A reference behaves like an alias of another object. You initialize a reference to refer to the other object when you declare the reference. After that, all operations on the reference actually affect the object to which the reference refers.

These discussions describe reference behavior as I interpret it in the December '96 C++ specification and as I observe it in the three compilers I use -- Borland C++ 5.2, Microsoft Visual C++ 5.0, and GNU C++ 4.7.2.

Reference Surprises

References are to new C++ programmers what pointers were to new C programmers. Hard to understand at first. Eventually reference behavior becomes second nature, but there are subtle traits -- surprises, actually -- in that behavior that can trap the unsuspecting and unwary. These characteristics are well documented but not particularly intuitive. Here are some of them, in no particular order.

Assignment versus Initialization

Why is it that you can initialize a reference but you cannot assign to it? Other languages do not have that restriction. Algol68 and Java, for example, allow you to change what a reference variable refers to by assigning another object or another reference to it. In The Design and Evolution of C++ (Addison-Wesley, 1994), Bjarne Stroustrup says:

I had in the past been bitten by Algol68 references where r1=r2 can either assign through r1 to the object referred to or assign a new reference value to r1 (rebinding r1) depending on the type of r2. I wanted to avoid such problems in C++.

Consequently, r1=r2 in C++ specifically assigns the object referred to by r2 to the object referred to by r1. After the assignment, each reference still refers to its original object, and the two objects are equal -- or, more precisely, as equal as the class's assignment operator function permits.

Reference versus Pointer

The reference and the pointer are closely related. A pointer can do anything a reference can do and more. One justification for choosing a reference might be found in that "and more" phrase. Inasmuch as you cannot do pointer arithmetic on a reference or initialize it with a null value, the reference can make your programs less vulnerable to pointer-related bugs.

It seems that whenever a Java author tries to justify Java over C++, the author emphasizes that most C++ program bugs result from pointers that point somewhere they should not. Java does not have pointers, you see, so Java programmers avoid such bugs. Java does employ null references, but dereferencing a method or member of one throws a run-time NullPointerException object.

From that Java argument you could extrapolate that an advantage of C++ references is that they do not contain certain C++ pointer pitfalls. That conclusion might be valid, but you could not prove it from my experience. Most of my bugs are not the result of errant pointers, nor were they when I programmed in C, but I do not claim that my experience is typical.

Programs use pointers and references to pass and return objects by reference rather than by value. This usage allows cooperating functions to work on common objects rather than copies and avoids the overhead required to build large argument objects on the stack.

I often use a reference to trim down the dereferencing notation of an object that is retrieved with a complex expression involving pointers, arrays, and class members. Listing Three demonstrates that usage. This usage is for notational convenience. Depending on the compiler's optimizations, it might also improve the program's efficiency.

There is no real performance advantage gained by using a reference or a pointer in any of these idioms. So which one should you use? Here are some of my guidelines. Don't go checking all the code I've published, though. I apply these guidelines loosely.

About that last guideline. I've written a lot of text-parsing algorithms for programming language translation and scripting applications. When the parse involves a recursive descent, I find pointer arithmetic to be essential and pointer-to-pointer idioms to be particularly expressive. Certainly, you can write such algorithms without pointers. But, for me, this is a case where pointers work better.

References are commonly used to implement overloaded operator functions, which, according to Stroustrup, was the reason he came up with the reference concept in the first place. If you overload an arithmetic operator for class objects, and the size of the object makes it undesirable to pass copies as arguments to the operator function, you must use references for parameters as Listing Four demonstrates. You cannot use pointers as arguments to overloaded arithmetic operator functions because that would seem to the compiler to be changing the behavior of pointer arithmetic, which you cannot do.

Overloaded Assignment and Copy Construction

Anyone who does conscientious class design knows this guideline: If a program can declare multiple objects of a class and if the class contains pointer data members that point to objects constructed on the heap, the class needs a copy constructor and an overloaded assignment operator function.

Here's why. (If you already know why, you can skip this discussion and proceed to "Hacking: Assigning Reference Data Members.") If a class does not have an overloaded assignment operator function, the compiler generates default member-for-member assignment behavior when the program assigns an object of the class to another object of the same class. If a class does not have a copy constructor, the compiler generates default member-for-member copy construction when a new object of the class is constructed from an existing object of the same class. Examples of those contexts are: when an object of the class is the only initializer argument for the constructor of a new object of the class; when an object of the class is passed as an argument to a function's parameter of the same class; when an object of the class is returned from a function.

If the class includes a pointer data member that points to a heap object, and the program assigns an object of the class to another object of the class, the assigning object's copy of the pointer overwrites that of the object being assigned to. The original heap data member in the object being assigned to is still allocated, but its pointer is overwritten; consequently, the program cannot delete the pointer and return its memory to the heap. A memory leak results.

In such a situation, or in the case where the program uses the default copy constructor to construct an object from an existing object, whichever of the two objects is destroyed first deletes the heap data member of the assigning object, usually from within the class destructor. When the other object is subsequently destroyed, the class destructor tries to delete the same heap data member by deleting the same pointer value, which now points either to free memory or to memory that has been otherwise allocated since the first object was destroyed. Undefined behavior results. Best case, you get an immediate crash that you can debug. Worst case, you get an insidious, lurking bug that does not show its effects until later.

Hacking: Assigning Reference Data Members

What if the class has a reference data member? References must be initialized when they are declared and cannot be modified after that. Reference data members must be initialized when the class is constructed and only from an entry in the constructor's parameter initialization list. If the class includes a reference data member, the class's copy constructor initializes it with a reference to the object of the same data member in the initializing class object. But how does an overloaded assignment operator function change an existing data member reference in an existing object to refer to whatever the same data member reference refers to in the assigning object? According to the rules for references, such an operation is not possible because you cannot assign a new value to an existing reference.

If you establish and follow a guideline that uses reference data members only to refer to global, one-of-a-kind objects, the restriction I just described is not a problem; the two objects probably already refer to the same global object and the overloaded assignment operator function can ignore the reference data member. But if objects are permitted to use the same reference data member to refer to different other objects, the problem is real. The first and most obvious solution is to change such reference data members to pointers, but then you give up the advantages of reference data members, whatever you perceive those advantages to be. Sometimes sacrifice is the only choice. Other times, you might not have the option of changing the data representation of a publicly used class. You can use inheritance to derive a class that overloads assignment, or you can add an overloaded assignment function to a class's declaration in its header file and provide the function (not a good way to modify the behavior of a published class), but the class's other member functions might be in a library and not available to you.

Let's look at the innards of a typical overloaded assignment operator function and see if there is anything that we can do about this situation. First, the operator function ensures that the program is not assigning an object to itself. Without this test, some of what follows might have ill effects. Second, if the class is derived from a base class, the function calls the base class's assignment operator function, which might be overloaded or which might invoke the base class's default assignment operation. This action, the call to the base class's assignment function, is not automatically generated by the compiler, whereas the equivalent behavior in a copy constructor, the call to the base class constructor, is automatically generated. Third, the overloaded assignment operator function destroys any nonstatic dynamically allocated resources owned by the object being assigned to. Fourth, the function allocates new heap memory for the appropriate pointer data members in the object being assigned to and copies the data values from the assigning object's heap memory. Finally, the function copies all the other data members by using the assignment operator.

The next step in addressing this problem is to understand how references are implemented. Remembering that the first C++ implementations translated C++ code to C code, we can guess that a reference is really a pointer with some restrictions enforced by the compiler -- no null value, no assignment, no comparisons, no address-of, and no arithmetic -- and with data member operator rather than points-to operator semantics for dereferencing the object pointed to. There is no guarantee that all compilers will implement references as pointers, but all the compilers I have looked at do that. Having written source level debuggers that permit evaluation of expressions for variable examination, I cannot imagine another sensible way to implement references.

It would follow that if reference data members are actually memory address variables stored within an object's memory space, there must be some way to write a copy of such a variable to another object's memory. C++ being a flexible language, several tricks come to mind.

You might try to use the reference's proximity to other data members to infer the reference's address and then use the memmove function as Listing Five does. This approach works with my compilers but might not be portable. Pointer sizes, padding, the placement of data members with respect to one another, where the hidden vptr variable is, and so on, are implementation-specific details that can vary among platforms.

You might try to put the reference data member in an anonymous union along with a void pointer and use the pointer identifier to make the assignment. Listing Six demonstrates this approach, which works with two of my compilers but is incorrect C++. The standard specification says, "If a union contains a static data member, or a member of reference type [italics added], the program is ill-formed." That rule was voted into the draft standard in 1995, but none of my compilers prohibit the usage perhaps out of respect for existing code, an issue that did not seem to concern the committee as much as it should have. The maverick hack described here does not work with the Borland compiler but not because Borland C++ forbids references in unions; that compiler reports an error when a union, struct, or class has a reference data member and no constructor to initialize the reference.

There is another hack that works only if the class is not in a hierarchy. You can use memmove to imitate the default assignment function. Listing Seven is an example of that approach, which works with all three of my compilers. This particular approach would be unreliable in a derived class because the memmove operation would overwrite whatever the base class assignment function did and subvert that function's good intentions.

Don't spend a lot of time trying to hypothesize a situation wherein you need hacks like these. I have on rare occasions, but you probably never will. If it should happen to come up, these are the only options I could find.

DDJ

Listing One

#include <iostream.h>int main()
{
    int* ip = 0;    // null pointer
    // ...
    int& ir = *ip;  // refers to nonexisting int
    cout << ir;     // error! dereferences nonexisting int
    return 0;
}

Back to Article

Listing Two

#include <iostream.h>int main()
{
    int* ip = new int;  // allocate heap int
    int& ir = *ip;      // reference to heap int
    ir = 123;
    // ...
    delete ip;          // destroy heap int
    cout << ir;         // error: dereference destroyed heap int
    return 0;
}

Back to Article

Listing Three

#include <iostream.h>struct foo {
    struct bar {
        int ifb[10];
    } foobar[2];
};
void dofoo(foo*);
int main()
{
    foo f = {{{9,8,7,6,5,4,3,2,1,0},{0,1,2,3,4,5,6,7,8,9}}};
   dofoo(&f);
    return 0;
}
void dofoo(foo* fp)
{
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 10; j++)    {
            // reference to int dereferenced by complex notation
            int& ir = fp->foobar[i].ifb[j];
            // use the reference for less code clutter
            ir += 20;
            cout << ir << ' ';
            ir -= 10;
            cout << ir << ' ';
        }
    }
}

Back to Article

Listing Four

#include <iostream.h>class Date {
    int day, month, year;
public:
    Date(int d, int m, int y) : day(d), month(m), year(y)
        { }
    friend int operator-(const Date& d1, const Date& d2);
};
int operator-(const Date& d1, const Date& d2)
{
    int diff;
    // ... overloaded - code here
    return diff;
}
int main()
{
    Date dt1(6,24,1940);
    Date dt2(11,17,1941);
    cout << (dt1-dt2);
    return 0;
}

Back to Article

Listing Five

#include <iostream.h>#include <string.h>
class foo {
    int ix;
    int& ry;
    char* pt;
public:
    foo(const char* t, int x, int& y);
    virtual ~foo();
    friend ostream& operator<<(ostream& os, const foo& f);
    foo& operator=(const foo&);
};
foo::foo(const char* t, int x, int& y) : ix(x), ry(y)
{
    pt = new char[strlen(t)+1]; 
    strcpy(pt, t);
}
foo::~foo()
{
    delete [] pt;
}
foo& foo::operator=(const foo& fr)
{
    if (&fr != this)    {
        ix = fr.ix;
        delete [] pt;
        pt = new char[strlen(fr.pt)+1]; 
        strcpy(pt, fr.pt);
        // --- hack the assignment of the reference member
        char* pc1 = (char*) &ix + sizeof(int);
        char* pc2 = (char*) &fr.ix + sizeof(int);
        memmove(pc1, pc2, sizeof(void*));
    }
    return *this;
}
ostream& operator<<(ostream& os, const foo& f)
{
    os << f.pt << ' ' << f.ix << ' ' << f.ry << '\n';
    return os;
}
int main()
{
    int x(123), y(456);
    foo bar1("Hello", 11, x);
    foo bar2("Dolly", 22, y);

cout << bar1; cout << bar2;

bar2 = bar1; // use overloaded assignment cout << bar2; return 0; }

Back to Article

Listing Six

#include <iostream.h>#include <string.h>
class Foo {
    char* m_ps;
    union {
        int& m_rny; // incorrect C++, but compiles
        void* m_vp;
    };
    int m_nz;
    friend ostream& operator<<(ostream& os, Foo& foo);
public:
    Foo(const char* ps, int& y, int z);
    ~Foo();
    Foo& operator=(Foo& foo);
};
Foo::Foo(const char* ps, int& y, int z) : m_rny(y), m_nz(z)
{
    m_ps = new char[strlen(ps)+1];
    strcpy(m_ps, ps);
}
Foo::~Foo()
{
    delete [] m_ps;
}
Foo& Foo::operator=(Foo& foo)
{
    if (&foo != this)   {
        delete [] m_ps;
        m_ps = new char[strlen(foo.m_ps)+1];
        strcpy(m_ps, foo.m_ps);
        m_nz = foo.m_nz;
        // --- fake the reference assignment with a union
        m_vp = foo.m_vp;
    }
    return *this;
}
ostream& operator<<(ostream& os, Foo& foo)
{
    os << foo.m_ps << ' ' << foo.m_rny << ' ' << foo.m_nz;
    return os;
}
int main()
{
    int y2(2), y3(3);
    Foo foo1("One:",y2,3);
    cout << foo1 << '\n';
    Foo foo2("Two:",y3,4);
    cout << foo2 << '\n';
    foo1 = foo2;
    cout << foo1 << '\n';
    return 0;
}

Back to Article

Listing Seven

#include <iostream.h>#include <string.h>
class Foo {
    char* m_ps;
    int& m_rny;
    int m_nz;
    friend ostream& operator<<(ostream& os, Foo& foo);
public:
    Foo(const char* ps, int& y, int z);
    ~Foo();
    Foo& operator=(Foo& foo);
};
Foo::Foo(const char* ps, int& y, int z) : m_rny(y), m_nz(z)
{
    m_ps = new char[strlen(ps)+1];
    strcpy(m_ps, ps);
}
Foo::~Foo()
{
    delete [] m_ps;
}
Foo& Foo::operator=(Foo& foo)
{
    if (&foo != this)   {
        delete [] m_ps;
        // --- imitate default assignment
        memmove(this, &foo, sizeof(Foo));
        m_ps = new char[strlen(foo.m_ps)+1];
        strcpy(m_ps, foo.m_ps);
        m_nz = foo.m_nz;
    }
    return *this;
}
ostream& operator<<(ostream& os, Foo& foo)
{
    os << foo.m_ps << ' ' << foo.m_rny << ' ' << foo.m_nz;
    return os;
}
int main()
{
    int y2(2), y3(3);
    Foo foo1("One:",y2,3);
    cout << foo1 << '\n';
    Foo foo2("Two:",y3,4);
    cout << foo2 << '\n';
    foo1 = foo2;
    cout << foo1 << '\n';
    return 0;
}

Back to Article


Copyright © 1998, Dr. Dobb's Journal