Channels ▼
RSS

C++ Theory and Practice


September 1997/C++ Theory and Practice

C++ Theory and Practice

Dan Saks

Work-arounds for a Mistake

Dan continues his diatribe against overzealous attempts to keep C++ backward compatible with C.


Copyright © 1997 by Dan Saks

Last month, I described what I believe was a fundamental mistake in the design of C++, namely, that classes and structs are essentially the same construct. (See "C++ Theory and Practice: Maybe It Wasn't Such a Good Idea After All," CUJ, August 1997.)

On the surface, this design decision (which I dubbed the Grand Unification) appears to simplify the language by requiring only one set of rules to cover both constructs. Unfortunately, the language rules aren't any simpler than if classes and structs had been kept apart. Moreover, the Grand Unification is responsible for introducing some serious bugs into C++ programs. The bugs are a direct consequence of the rules by which C++ generates copy constructors and copy assignment operators. This month, I will look at various programming styles you can use to prevent compilers from generating unwanted functions.

The Grand Unification is the source of another, albeit lesser, problem in C++. I will explain that problem, and suggest a cure for it as well.

Generated Copy Operations (Again)

Last month, I briefly sketched the rules for copy constructors and copy assignment operators. This month, I provide more details on those rules so that you will understand how to exploit them more effectively.

A copy constructor for a class T is a constructor that can be called with a single argument of type T. The typical declaration for a copy constructor has the form

T(T const &t);

This is not the only form. Any of

T(T &t);
T(T volatile &t);
T(T const volatile &t);

can also be copy constructors. A copy constructor can have more than one parameter, as long as all the parameters after the first have default argument values. For instance,

T(T const &t, X x = v);

is also a copy constructor for class T.

A class can have more than one copy constructor. For example, the following class has two:

class T
    {
public:
    T(T &t);
    T(T const &t);
    ...
    };

In this case, if t is a const T object, then declarations such as

T x(t);
T x = t;

use the copy constructor T(T const &t) to copy t to x. If t is not const, then the declarations use T(T &t).

A constructor for class T with a single parameter of just plain T is not a copy constructor, nor is it even allowed:

T(T t);           // error

If a class T does not declare any copy constructors, a C++ compiler may generate one, but only if the program requires it. For example, if t is a T object, then declarations such as

T x(t);
T x = t;

require that T have a copy constructor.

Calling a function such as

void f(T x);

requires a copy constructor to pass the argument by value. That is, if t is a T object, then the call f(t) uses T's copy constructor to copy t to parameter x.

A function that returns a T object by value also requires a copy constructor. A function such as

T f()
    {
    T v;
    ...
    return v;
    }

uses a temporary T object to carry the return value back to its caller. The return statement copies v to that temporary object using T's copy constructor.

Typically, a generated copy constructor for class T has a declaration of the form

T(T const &t);

However, it might have the form

T(T &t);

In particular, the generated copy constructor for class T has the first form (with a parameter of type T const &) only if

  • for each direct or virtual base class B of T, B has a copy constructor whose first parameter has type B const & or B const volatile &, and
  • for each non-static data member m of T with class type (or array thereof), each such class type M has a copy constructor whose first parameter has type M const & or M const volatile &.

Otherwise, the generated copy constructor has the second form (with a parameter of type T &).

A copy assignment operator for class T is an operator= that can be called with an argument of type T. The typical declaration for a copy assignment operator has the form

T &operator=(T const &t);

However, this is not the only form. Any of

T &operator=(T &t);
T &operator=(T volatile &t);
T &operator=(T const volatile &t);

can also be copy assignments. Unlike a copy constructor, a copy assignment must have exactly one parameter. It cannot have additional parameters even if they have default values. However, a copy assignment can have a parameter whose type is just plain T, as in

T &operator=(T t);  // OK

Just as a class can have more than one copy constructor, it can have more than one copy assignment operator. For example,

class T
    {
public:
    T &operator=(T &t);
    T &operator=(T const &t);
    ...
    };

In this case, if t is a const T object, then the assignment

x = t;

uses the copy assignment operator=(T

const &t) to assign t to x. If t is not const, then the assignment uses operator=(T &t).

If a class T does not declare a copy assignment, C++ compilers may generate one, but again, only if the program requires it. Typically, the generated copy assignment operator for a class T has a declaration of the form:

T &operator=(T const &t);

However, it might have the form

T &operator=(T &t);

In particular, the generated copy assignment for class T has the first form (with a parameter of type T const &) only if

  • for each direct base class B of T, B has a copy assignment whose parameter has type B const &, B const volatile & or (plain) B, and
  • for each non-static data member m of T with class type (or array thereof), each such class type M has a copy assignment whose parameter has type M const &, M const volatile & or (plain) M.

Otherwise, the generated copy constructor has the form

T &operator=(T &t);

When to Say No

As I mentioned last month, the generated copy constructor and copy assignment operator use memberwise copies. Memberwise copies work just fine for some classes, but not for others. It's helpful to have a general rule by which you can tell when memberwise copy is okay, and when it isn't.

The generated copy functions are appropriate for any class in which all the data members have arithmetic or enumeration type (or array thereof), such as:

class complex
    {
    ...
private:
    double real, imag;
    };

For this example, the generated copy constructor is equivalent to:

complex::complex(complex const &c)
:   real(c.real), imag(c.imag)
    {
    }

The member initializer real(c.real) copies c.real to real as if by assignment. Ditto for imag(c.imag). This is the right behavior. The generated copy assignment has essentially the same behavior, and works equally well.

In contrast, the generated copy constructor and copy assignment are almost certainly wrong for any class that contains at least one pointer to dynamically-allocated memory. For such classes, a memberwise copy results in a shallow copy, which leaves two or more objects competing over dynamically-allocated memory. Shallow copies typically leak memory and corrupt the free store. The only way to copy this kind of object is by a deep copy that duplicates all the dynamically-allocated memory that comprises the object's value.

If you're not already familiar with the damage that shallow copies can do, read Item 11 in Meyers[1] . The title of Item 11 actually suggests the following rule of thumb:

Define a copy constructor and an assignment operator for classes with dynamically allocated memory.

Item 27 augments this rule with:

Explicitly disallow use of implicitly generated member functions you don't want.

This is a good start, but I would merge the two rules into one and word it a little differently:

For any class that manages dynamically-allocated resources, either define a copy constructor and a copy assignment operator, or prevent compilers from generating their definitions.

Although memory is probably the most common dynamically- allocated resource, it is by no means the only one. Classes can manage other dynamically-allocated resources, such as files and devices. The data members that designate such resources need not be pointers.

For instance, a class that manages a file might refer to the file using a UNIX-style file descriptor, which is simply a signed integer. In this case, the generated copy constructor and copy assignment would leave two objects fighting over a single file.

Sometimes writing a copy constructor and copy assignment for a class is more trouble than it's worth, or makes no sense at all. In such cases, you should do something to prevent a compiler from generating these functions. Otherwise it will write them for you, and you probably won't like what you get.

Preventing Generated Declarations

There are various techniques for suppressing the generated definitions for the copy functions. By far, the most commonly used technique is to declare the copy constructor and copy assignment private, and omit their definitions. That is, defining a class T as

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

renders T's copy constructor and copy assignment inaccessible to users of T. Any attempt to assign one T object to another, pass a T by value, or return a T by value will produce a compile-time access violation. Well, almost.

This technique still allows members and friends of T to call the copy functions. However, the copy functions have no definitions, so attempts to copy T objects that make it past the compiler will be caught by the linker. The diagnostic messages from the linker might not be as clear or timely as you'd like, but at least they prevent the errors from making their way into the executable program.

I see the need to omit the definitions as a weakness in this technique. How does someone reading the code know whether the definitions are missing on purpose or by accident? The class definition probably needs a comment such as:

class T
    {
    ...
private:
    T(T const &);
        // no definition by design
    T &operator=(T const &);
        // no definition by design
    };

As an alternative, you can clarify intent by bundling the declarations in a macro such as:

#define disallow_copying_for(C) \
private: \
    C(C const &); \
    C &operator=(C const &)

Since these member functions are supposed to be unusable, the const qualifiers in the parameter lists serve no purpose. You can omit them and write the macro more compactly as:

#define disallow_copying_for(C) \
private: C(C &); C &operator=(C &);

Using this macro, you can define class T as

class T
    {
    ...
    disallow_copying_for(T)
    };

which states the intent more clearly.

I prefer a slightly different approach that catches errors at compile time rather than at link time. This approach relies on knowing when compilers do not generate a copy constructor or copy assignment for a class.

A C++ compiler will not generate a copy constructor for class T if T has:

  • a non-static data member of class type (or array thereof) with an inaccessible or ambiguous copy constructor, or
  • a base class with an inaccessible or ambiguous copy constructor.

Similarly, a compiler will not generate a copy assignment operator for T if T has:

  • a non-static data member of class type (or array thereof) with an inaccessible copy assignment operator, or
  • a base class with an inaccessible copy assignment operator.

According to these rules, you can define:

class uncopyable
    {
private:
    uncopyable(uncopyable &);
    void operator=(uncopyable &);
    };

Then, if you want to suppress the generated copy functions for class T, you can define T as:

class T
    {
    ...
private:
    uncopyable u;
    };

Since T has no copy constructor, the compiler will try to generate one on demand. As always, T's generated copy constructor will try using memberwise initialization, so that the constructor would be equivalent to:

T::T(T const &t)
:   u(t.u) ...
    {
    }

However, u's copy constructor is private, and so the member-initializer u(t.u) causes an access violation at compile-time. The generated copy assignment operator hits the same snag. Therefore, any attempt to copy a T object, even within a member or friend of T, will produce a compile-time error.

Surprisingly, this particular technique has a slight run-time cost. Class uncopyable has no members, and therefore appears to have a size of zero. However, C++ does not allow zero-sized objects, including zero-sized data members. Therefore, adding a member of type uncopyable to class T actually increases the size of all T objects.

On the other hand, C++ does allow zero-sized base classes. Defining class T as

class T : uncopyable
    {
    ...
    };

inhibits the generated copy functions without increasing the size of T objects.

In the definition for T just above, the base class specifier

: uncopyable

has no access specifier. By default, the access to base classes of a class defined with the keyword class is private. Therefore, the definition for T is equivalent to:

class T : private uncopyable
    {
    ...
    };

Class uncopyable has no inheritable public members, so I don't think it really matters whether you use public, private, or protected inheritance.

As defined above, class uncopyable has a problem, as illustrated by the following example. Suppose class T is:

class T : uncopyable
    {
public:
    T(int i);
private:
    int t;
    };

where the constructor is defined by:

T::T(int i) : t(i)
    {
    }

Since T has a base class of type uncopyable, this constructor initializes T's base class sub-object by calling uncopyable's default constructor. However, uncopyable has no default constructor, and the compiler will not generate one because uncopyable already has at least one explicitly-declared constructor. (C++ generates a default constructor for a class only if that class has no explicitly-declared constructors.)

With the previous definition for class uncopyable, it's impossible to write any constructors for a class derived from uncopyable. Class uncopyable needs an explicitly-defined public default constructor, as in:

class uncopyable
    {
public:
    uncopyable() { }
private:
    uncopyable(uncopyable &);
    void operator=(uncopyable &);
    };

My current advice for disabling copy functions is:

To prevent memberwise copying for a class T, define class uncopyable and specify it as a base of T.

I caution you that I've been using this technique in my own work for only a few months, and I haven't seen anyone else recommend it. I know of no problems with it, but that's hardly a proof of correctness. If you try it and run into problems, please let me know and I'll alert the masses.

More Peculiarities from Unification

The C language rules for naming structs are a little eccentric, but they're pretty harmless. However, when extended to classes in C++, those same rules open little cracks for bugs to crawl through.

In C, the name s appearing in

struct s
    {
    ...
    };

is a tag. A tag name is not a type name. Given the definition above, declarations such as

s x;    /* error in C */
s *p;   /* error in C */

are errors in C. You must write them as

struct s x;     /* OK */
struct s *p;    /* OK */

The names of unions and enumerations are also tags rather than types.

In C, tags are distinct from all other names (for functions, types, variables, and enumeration constants). C compilers maintain tags in a symbol table that's conceptually if not physically separate from the table that holds all other names. Thus, it is possible for a C program to have both a tag and an another name with the same spelling in the same scope. For example,

struct s s;

is a valid declaration which declares variable s of type struct s. It may not be good practice, but C compilers must accept it. I have never seen a rationale for why C was designed this way. I have always thought it was a mistake, but there it is.

Many programmers (including yours truly) prefer to think of struct names as type names, so they define an alias for the tag using a typedef. For example, defining

struct s
    {
    ...
    };
typedef struct s S;

lets you use S in place of struct s, as in

S x;
S *p;

A program cannot use S as the name of both a type and a variable (or function or enumeration constant):

S S;    // error

This is good.

The tag name in a struct, union, or enum definition is optional. Many programmers fold the struct definition into the typedef and dispense with the tag altogether, as in:

typedef struct
    {
    ...
    } S;

Once again, as a consequence of the Grand Unification, classes and structs are essentially the same in C++. Although the draft C++ Standard doesn't call them tags, class names act very much like tags. For example, you can declare an object of class string with a declaration such as

class string s;

Of course, no one actually does.

C++ was designed so that user-defined types can look as much as possible like built-in types (whenever appropriate). Using the keyword class in the declaration above serves only to remind readers that string is not a built-in type. Therefore, C++ lets you use class names as if they were type names. That is, you can omit the keyword class from the declaration just above, and write it as just:

string s;

The draft C++ Standard never utters the word tag. In C++, the names of classes, structs, unions, and enumerations are just type names. However, there are several rules that single out these type names for special treatment. I find it easier to continue to refer to class, struct, union, and enum names as tags.

If you want, you can imagine that C++ generates a typedef for every tag name, such as

typedef class string string;

Unfortunately, this is not entirely accurate. I wish it were that simple, but it's not. C++ can't generate such typedefs for structs, unions, or enums without introducing incompatibilities with C.

For example, suppose a C program declares both a function and a struct named status:

int status();
struct status;

Again, this may be bad practice, but it is C. In this program, status (by itself) refers to the function; struct status refers to the type.

If C++ did automatically generate typedefs for tags, then when you compiled this program as C++, the compiler would generate:

typedef struct status status;

Unfortunately, this type name would conflict with the function name, and the program would not compile. That's why C++ can't simply generate a typedef for each tag.

In C++, tags act just like typedef names, except that a program can declare an object, function, or enumerator with the same name and the same scope as a tag. In that case, the object, function, or enumerator name hides the tag name. The program can refer to the tag name only by using the keyword class, struct, union, or enum (as appropriate) in front of the tag name. A type name consisting of one of these keywords followed by a tag is an elaborated-type-specifier. For instance, struct status and enum month are elaborated-type-specifiers.

Thus, a C program that contains both:

int status();
struct status;

behaves the same when compiled as C++. The name status alone refers to the function. The program can refer to the type only by using the elaborated-type-specifier struct status.

So how does this allow bugs to creep into programs? Consider the program in Listing 1. This program defines a class foo with a default constructor, and a conversion operator that converts a foo object to char const *. The expression

p = foo();

in main should construct a foo object and apply the conversion operator. The subsequent output statement

cout << p << '\n';

should display class foo, but it doesn't. It displays function foo.

This surprising result occurs because the program includes header lib.h shown in Listing 2. This header defines a function also named foo. The function name foo hides the class name foo, so the reference to foo in main refers to the function, not the class. main can refer to the class only by using an elaborated-type-specifier, as in

p = class foo();

The way to avoid such confusion throughout the program is to add the following typedef for the class name foo:

typedef class foo foo;

immediately before or after the class definition. This typedef causes a conflict between the type name foo and the function name foo (from the library) that will trigger a compile-time error.

I know of no one who actually writes these typedefs as a matter of course. It requires a lot of discipline. Since the incidence of errors such as the one in Listing 1 is probably pretty small, you many never run afoul of this problem. But if an error in your software might cause bodily injury, then you should write the typedefs no matter how unlikely the error.

I can't imagine why anyone would ever want to hide a class name with a function or object name in the same scope as the class. The hiding rules in C were a mistake, and they should not have been extended to classes in C++. Indeed, you can correct the mistake, but it requires extra programming discipline and effort that should not be necessary.

Reference

[1] Scott Meyers, Effective C++ (Addison-Wesley, 1992).

Dan Saks is the president of Saks & Associates, which offers training and consulting in C++ and C. He is active in C++ standards, having served nearly seven years as secretary of the ANSI and ISO C++ standards committees. Dan is coauthor of C++ Programming Guidelines, and codeveloper of the Plum Hall Validation Suite for C++ (both with Thomas Plum). You can reach him at 393 Leander Dr., Springfield, OH 45504-4906 USA, by phone at +1-937-324-3601, or electronically at dsaks@wittenberg.edu.


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