Columns


Stepping Up To C++

Initialization vs. Assignment

Dan Saks


Dan Saks is the owner of Saks & Associates, which offers consulting and training in C and C++. He is secretary of the ANSI C++ standards committee, and also contributing editor for Windows/DOS Developer's Journal. Dan recently finished his first book, C++ Programming Guidelines, written with Thomas Plum. You can write to him at 393 Leander Dr., Springfield, OH 45504, or dsaks@wit-tenberg.edu (Internet), or call (513)324-3601.

In C++, initialization and assignment are similar, but clearly distinct, operations. The distinctions pervade the language, and understanding them is an important part of learning to program in C++.

Although C also draws distinctions between initialization and assignment, the differences are not nearly so profound. For example, in C you usually can't distinguish the behavior of

void f(T i)
    {
    T v=i;
    ...
    }
from the behavior of

void f(T i)
    {
    T v;
    v = i;
    ...
    }
whenever T is a scalar (arithmetic or pointer), struct, or union type. That is, declaring an auto scalar, struct, or union variable v with initial value i generates the same code as declaring v with no initial value and then immediately assigning i to v. For the most part, C language data types have the same semantics in C++, so the preceding examples behave the same in C++ as they do in C.

However, if you change T to a uniquely C++ type, like a reference type or a class type with constructors and destructors, then the examples might behave quite differently from each other. In fact, you can even declare T as a class type such that one of the above examples is valid while the other is not.

The examples behave differently in C++ because initialization is different from assignment. Although I've contrasted initialization with assignment at various times in earlier columns, the issues are so important they deserve another, more detailed, look.

Copying Objects

Objects are copied not only in initializations and assignments, but also in some less obvious places like argument passing and function return. For example, consider the function

T f(T a)
    {
    T b;
    ...compute b from a ...
    return b;
    }
which accepts an argument of type T and returns a T. The compiler translates a call such as

T x, y;
...
y = f(x);
into code that copies the value of actual argument x to formal parameter a just before it jumps to f. Similarly, the return statement in f generates code that copies the value of b to a temporary location (possibly machine registers) set aside to hold the return value. After returning, the assignment expression copies the function return value from the temporary to the object denoted by y.

You might reasonably expect that C and C++ treat argument passing the same. But the language definitions use different words to describe argument passing semantics. The C standard says, "In preparing to call a function, the [actual] arguments are evaluated, and each [formal] parameter is assigned the value of the corresponding argument." On the other hand, the ARM (The Annotated C++ Reference Manual) states, "When a function is called, each formal argument is initialized with its corresponding actual argument." In short, C treats argument passing as assignment, but C++ treats it as initialization.

There's a corresponding difference in the way the languages describe the return statement. The C standard states, "If the [return] expression has a type different from that of the function in which it appears, it is converted as if it were assigned to an object of that type." The C++ version of the same rule is, "If required, the expression is converted, as in an initialization, to the return type of the function in which it appears." Function return is an assignment in C, but an initialization in C++.

Why the difference? For the C language types (common to C and C++), there really isn't any. C++ changed the wording of these rules to accommodate references and class objects.

Initializing and Assigning References

Initializing a reference is markedly different from assigning to it. I described references in detail in an earlier column (CUJ September 1991), but they are an important part of this discussion, and worth reviewing.

A declaration like

int i, j;
...
int &ri = i;
initializes (binds) ri to refer to i. It copies the address of i to the pointer that implements ri. On the other hand, a subsequent assignment to ri such as

ri = j;
copies the value of j to the object referenced by ri.

The initializer for a non-const T & must be a modifiable lvalue of type T. Thus, neither

double &rd1 = 0.0;
double &rd2 = i;
are valid (where i is an int as declared above). However, the initializer for a const T & may have any type convertible to T, and it may even be an rvalue. Thus both

const double &rd1 = 0.0;
const double &rd2 = i;
are valid. The compiler binds rd1 to the address of a temporary double object initialized to 0.0, and binds rd2 to a different temporary double object initialized by the value of i promoted to double.

When you consider functions with reference arguments, it's not hard to see why C++ treats argument passing as initialization rather than assignment. For example, think about what happens when you call f(i) given the declarations

int i;
void f(int &r);
If argument passing behaved like assignment, then calling f(i) would assign i to the object referenced by formal parameter r. But then what does r refer to? Nothing, unless you decide the compiler should bind it to a temporary. But then any change to r inside f would alter the temporary object rather than the actual argument i. And, when the function returned, it would discard both r and the temporary, leaving i unchanged. Clearly, that is not the intent of passing non-const reference arguments. The intent is to bind formal parameter r to actual argument i (by initialization!) so that any change to r inside f alters i directly.

There was a time when C++ compilers bound non-const references to temporaries. For example, early compilers let you call f(12), binding r to a temporary int initialized to 12. Even worse, when you wrote

float x;
void g(double &r);
and called g(x) expecting g to alter x, you'd find that x never changed. Rather, the call would bind r to a temporary double initialized with the value of x promoted to double. This behavior proved to be, in the words of the ARM, "a major source of errors and surprises." Thus, binding a non-const reference to a temporary is now an error.

Copying Class Objects

C++ uses constructors to initialize class objects, and uses assignment operators to assign to class objects. For example, if a is a previously declared object of class X, the declaration

X b = a;
initializes b with a copy of a using X's copy constructor. In contrast,

X b;
...
b = a;
initializes b using X's default constructor, and then replaces b's value with a copy of a's value using one of X's assignment operators.

Since argument passing is initialization, passing a class object by value calls a constructor. For example, given function f declared as

void f(X x);
the call f(y) initializes formal parameter x using an X constructor that takes y as an argument. If y is an X object, the call uses the copy constructor. If y has some other type, the compiler uses whichever constructor accepts y as an argument.

A function that returns a class object initializes its return value by calling a constructor. For example,

X f(int i)
    {
    int j = i;
    ...
    return j;
    }
initializes the temporary X object returned by f using a constructor that accepts an int argument. A call such as

X a;
...
a = f(10);
copies the X object returned by f to a using one of X's assignment operators.

In earlier columns, I introduced constructors and the default constructor (CUJ May 1991), copy constructors (CUJ September 1991), and assignment operators (CUJ January 1992). However, I intentionally omitted details that were inappropriate at the time. These functions are very important in C++ programming, and worth exploring in greater detail.

Constructors and Default Constructors

A constructor for a class X is a special member function of X that initializes an X object when it is created. A constructor for class X always has the name X. For example,

class X
    {
public:
    X(int n);
    ...
    };
has a constructor that accepts a single int argument. Hence, a declaration such as

X x0(k);
(where k is an int) not only allocates storage for x0, but also applies the constructor to x0 using k as the argument to the constructor.

A constructor may have formal parameters, but not a return type. Constructors may be overloaded. For example,

class X
    {
    x();
    X(int n);
    ...
has two constructors. The compiler selects the constructor it calls based on the argument list in the declaration. For example, the declaration

X x1(10);
invokes X(int n), passing 10 as the value of n.

A declaration with no argument list, such as

X x3;
calls the default constructor, X(). In general, a default constructor is any constructor that can be called with no arguments. Thus, a constructor with default argument values for all of its parameters is also a default constructor. For example, if the declaration

X x3;      // calls X();
has the same effect as

X x3(0);   // calls X(int n);
then you can combine the two constructors into a single constructor with a default argument, as in

class X
    {
public:
    X(int n = 0);
    ...
Then the declaration

X x3;
invokes X(int n), supplying 0 as the value of n.

Always remember that

X x4(); // surprise?!
is not a declaration for an X object — it's a declaration for a function x4 that accepts no arguments and returns an X.

Copy Constructors

A copy constructor is a constructor that initializes an object by copying a single object of the same type. Typically, you declare the copy constructor for class X as

X(const X &x)
But in general, any constructor for class X that can be called with a single argument of type X is a copy constructor. Thus, any of the following

X(X &x)
X(const X &x, const char *p = 0)
X(X &x, size_t n = 100)
is a valid alternative copy constructor for class X. However, X's copy constructor may not have an argument of type X, so that X (X x) is not valid.

In effect, a copy constructor with a const argument promises not to change the object it copies. (This is true for any function with a const reference argument.) In most cases, this is what you want. A copy constructor with a non-const argument reserves the right to change the object it copies. I have never had occasion to write one. I guess these might be useful if your objects need to know if they have been copied.

A class X can overload its copy constructors with both const and non-const arguments. For example, class X can have both

X(const X &)
X(X &)
as copy constructors. In this case, when you declare an X object as a copy of another X object, the compiler uses X(const X &) if the object being copied is a const object, and uses X(X &) if the object being copied is non-const.

For example, in Listing 1, the declaration

X x1(3);
declares x1 as a non-const object using the constructor X (int). The declaration

const X x2(x1);
declares x2 as a const object. However, it uses the constructor X(X &), not X(const X &), because the object it copies, x1, is a non-const object. The last declaration,

X x3(x2);
declares x3 as a non-const object initialized by copying x2. It uses X(const X &) because x2 is a const object. In short, the compiler selects the copy constructor to match the const-ness of the object being copied, not the const-ness of the object being declared.

If class X has only X(const X &) as its copy constructor, then every declaration needing a copy constructor uses it. There's no problem if the copied object is non-const — you can always bind a const references to a non-const object. However, if class X has only X(X &x) as its copy constructor, you cannot use it to copy const objects.

For example, if you delete the X(const X &x) constructor from Listing 1, then the declaration

X x3(x2);
becomes an error. The compiler will try to use X(X &x) as the copy constructor, only to find that it cannot bind the non-const reference argument x to const object x2.

The = Operator

For the most part, C++ treats the = operator as it does any other overloaded operator. For an object a of class X, the compiler interprets the expression a = b as the function call a.operator=(b). The compiler selects whichever X::operator= accepts b as an argument.

As in C, an = appearing in a C++ declaration indicates the presence of an initializer, as in

X a = b;
This syntax is valid even when X is a class with constructors. Intuitively, the declaration means what you think it means — it declares a as an X object initialized by b. In this context, the = operator doesn't call an X::operator=; it calls a constructor.

Which brings me to one of my favorite C++ quiz questions: If X is a class with constructors, what if any, is the difference between

X a(b);
and

X a = b; // ?
Their meanings are very similar. In fact, if b is also of type X, the declarations mean the same thing, namely, "initialize a with a copy of b using X's copy constructor." But, when b is not an X, the two declarations are subtly different. Whereas,

X a(b);
means "initialize a using an X constructor that accepts b as an argument,"

X a = b;
means "construct a temporary X object using an X constructor that accepts b as an argument, and copy the temporary to a using X's copy constructor." More succinctly, it's equivalent to

X a(X(b));
The ARM notes that a good compiler will often optimize away the temporary and initialize a directly from b. Most compilers do. If the compiler can't avoid creating the temporary, it must destroy it by calling X's destructor.

An optimizing compiler must not forget that = initializers have different semantics from () initializers. For example, suppose b is an int, and you declare X as

class X
    {
public:
    X(int);
private:
    X(const X &);
    ...
    };
so that the copy constructor is private. (Constructors, like all other member functions, are subject to access specifiers.) Then the declaration

X a(b);
works fine, as it always did, but now

X a = b;
is an error, because the copy constructor is private. Even if the compiler can eliminate the temporary, it can't ignore the access restriction.

And by the way, if X is a scalar,

X a(b);
is supposed to be the same as

X a = b;
For example, you should be able to initialize int n to 1 by writing either

int n = 1;
or

int n(1);
Unfortunately, not all compilers are yet up to this task.

Generated Copy Constructors

If you don't declare a copy constructor for a class, the compiler typically defines one for you. If all the data members of X are scalar types (which do not have constructors), or class objects with copy constructors accepting const reference arguments, then the compiler declares X's copy constructor as

X(const X &x)
Otherwise, the declaration is

X(X &x)
In other words, the only way the compiler will generate a copy constructor with a non-const reference argument is if the class has at least one member whose only copy constructor also has a non-const reference argument.

Strictly speaking, the compiler doesn't actually define X's generated copy constructor unless it's used somewhere in the program. If X has a member that is an object of a class with a private constructor, the compiler can't generate X's copy constructor. A generated copy constructor is always public.

The generated copy constructor always uses memberwise initialization. That is, each member of the constructed object gets initialized by the corresponding member of the copied object. For example, if X has data members a and b, the generated memberwise copy constructor behaves just like

X(const X &x) : a(x.a), b(x.b) { }
Often this is just the right behavior. It works fine for my rational number class. However, for classes with complex substructure, like strings and vectors, memberwise initialization is inappropriate.

Consider a class String that stores variable-length strings in character arrays allocated from the free store. Each String object has a data member str that points to the first character in the array, and another member len that stores the array length (excluding the null character at the end). Figure 1 depicts the storage for a String whose value is "Hello".

Listing 2 contains the declaration for a simple version of the String class, along with a simple test program. The class has only one constructor, String(const char *s), that initializes a String to hold a copy of a null-terminated string s. A declaration such as

String s1("Hello");
creates the String in Figure 1.

The constructor grabs storage for the String's character array using the new operator. The delete operator in String's destructor returns the storage to the free store. (The empty square brackets in

delete [] str;
indicate that str points to the first character in an array, rather than to a single char.)

Since the String class in Listing 2 has no copy constructor, the compiler defines one that implements memberwise copy. Figure 2 shows the constructor's effect on the declaration

String s2 : s1;
Immediately afterwards, both s2 and s1 share the same character array. This leads to all sorts of unpleasantness.

For example, String::operator+=(char) appends a single character to the end of a String. In doing so, it deletes the String's character array, and allocates a new array that's one character longer. If, as happens in Listing 2, s1 and s2 share the same array, the call s1 += '!' deletes the shared array, and allocates a new array for s1. Unfortunately, it leaves s2's str pointing to a deleted array.

The cure is simple. Just write an explicit copy constructor that replicates the entire array, like the one in Listing 3. This constructor performs a "deep copy" — it copies the entire substructure of the object. It contrast, the default memberwise copy constructor does a only a "shallow copy."

Generated Assignment Operators

If you don't declare an assignment operator for a class, the compiler defines one for you. If all the data members of X are scalar types, or class objects with assignment operators accepting const reference arguments, then the compiler declares X's assignment operator as

X &operator=(const X &x)
Otherwise, it declares the assignment operator as

X &operator=(X &x)
and you won't be able to assign to an X object by copying a const X object. In either case, the operator returns a reference to its left operand (the object referenced by this).

As with the generated copy constructor, the compiler doesn't actually define X's generated operator= unless it's used somewhere in the program. If X has a member that is a const object, a reference, or an object of a class with a private operator=, the compiler can't generate X's operator=.

The generated assignment operator always uses memberwise assignment. That is, each member of the constructed object gets assigned the corresponding member of the copied object. For example, if X has data members a and b, the generated memberwise assignment behaves just like

X &operator=(const X &x)
    {
    a = x.a;
    b = x.b;
    return *this;
    }
As with memberwise initialization, memberwise assignment is appropriate for shallow objects like rational numbers. But for classes like Strings, you should replace the generated assignment with one that does a deep copy.
Listing 4 shows a more appropriate assignment operator for Strings.

For objects requiring deep copies, the essential difference between assignment and initialization is that, while initialization plies in virgin territory, assignment copies into previously initialized objects. Initialization can blithely overwrite storage, whereas assignment might have to discard resources before overwriting the links to them.

In the case of String objects, operator= tests if the length of the source String's array is different from the length of the destination String's array. If they're the same, operator= simply copies the source array to the destination array. But if they're different, operator= deletes the old destination array and grabs a new one of the proper size. Then it proceeds to copy.

Generated Default Constructors

If a class has no user-defined constructors, the compiler generates a default constructor as well as the copy constructor. For a class X, the generated default constructor behaves like

x() { }
If all of X's members are scalar types, this constructor acts like the empty function it appears to be. But, for any member of X that is an object of a class with a public default constructor, X's default constructor initializes that member using that member's default constructor. If any member of X is an object of a class without a public default constructor, the compiler cannot generate X's default constructor.

The compiler will not define a default constructor for a class that has any user-defined constructors.

Declarations as Executable Statements

In C++, a declaration is an executable statement, which you can place anywhere inside a block. For example, Listing 1 intermixes declarations with other statements. You no longer need to put all your local declarations at the beginning of a block as required by C. However, you must still declare each block-local name before you use it.

When I started programming in C++, I resisted this new freedom. I continued placing my declarations where nature intended them to be — at the beginning of each block. That is, until I understood why C++ relaxed the rule.

In C++, a class object declaration without an initializer invokes the default constructor. Each assignment into that object must test whether the object uses resources that must be released before assigning a new value. On the other hand, a declaration with an initializer simply constructs the object's initial value without releasing any resources. Thus, for almost any class X with constructors and operator=,

X a = b; // direct initialization
generates smaller and faster code than

X a;     // default initialization
...
a = b;   // assignment
Often, you don't know the value of the initializer until after you've done some processing. Rather than force you to use unnecessary default initialization, C++ simply lets you delay a declaration until you know its initial value.

I generally recommend that you write the declaration of a block-scope object at its point of first use. But be careful about moving declarations inside loops. An object with a constructor and destructor declared inside a loop will be created and destroyed on each iteration.