Reliable Software Logo

C++ In Action: Language

Passing by Value

Copy constructor, overloading the assignment operator, default copy constructor and operator =, return by value, passing by value, implicit type conversions.


So far we've been careful to pass objects from and to methods using references or pointers. For instance, in the following line of code

Parser  parser (scanner, store, funTab, symTab);
all the arguments to the Parser's constructor--scanner, store, funTab and symTab--are passed by reference. We know that, because we've seen the following declaration (and so did the compiler):
Parser (Scanner & scanner,
        Store & store,
        FunctionTable & funTab,
        SymbolTable & symTab);
When we construct the parser, we don't give it a copy of a symbol table. We give it access to an existing symbol table. If we gave it a private copy, we wouldn't have been able to see the changes the parser made to it. The parser may, for instance, add a new variable to the symbol table. We want our symbol table to remember this variable even after the current parser is destroyed. The same goes for store--it must remember the values assigned to symbolic variables across the invocations of the parser.

But what about the scanner? We don't really care whether the parser makes a scratch copy of it for its private use. Neither do we care what the parser does to the function table. What we do care about in this case is performance. Creating a scratch copy of a large object is quite time consuming.

But suppose we didn't care about performance. Would the following work?

Parser (Scanner scanner,
        Store & store,
        FunctionTable funTab,
        SymbolTable & symTab);

Notice the absence of ampersands after Scanner and FunctionTable. What we are telling the compiler is this: When the caller creates a Parser, passing it a scanner, make a temporary copy of this scanner and let the Parser's constructor operate on that copy.

There are many reasons why such approach will not work as expected, unless we make several further modifications to our code. First of all, the temporary copy of the scanner (and the function table) will disappear as soon as the execution of the Parser's constructor is finished. The parser will store a reference to it in its member variable, but that's useless. After the end of construction the reference will point to a nonexistent scratch copy of a scanner. That's not good.

If we decide to pass a copy of the scanner to the parser, we should also store a copy of the scanner inside the parser. Here's how you do it--just omit the ampersand.

class Parser
{
    ...
private:
    Scanner         _scanner;
    Node          * _pTree;
    Status          _status;
    Store         & _store;
    FunctionTable   _funTab;
    SymbolTable   & _symTab;
};

But what is really happening inside the constructor? Now that neither the argument, scanner, nor the member variable, _scanner, are references, how is _scanner initialized with scanner? The syntax is misleadingly simple.

Parser::Parser (Scanner scanner, 
            Store & store,
            FunctionTable funTab,
            SymbolTable & symTab)
: _scanner (scanner), 
  _pTree (0), 
  _status (stOk), 
  _funTab (funTab),
  _store (store),
  _symTab (symTab)
{
}
What happens behind the scenes is that Scanner's copy constructor is called. A copy constructor is the one that takes a (possibly const) reference to the object of the same class and clones it. In our case, the appropriate constructor would be declared as follows,
Scanner::Scanner (Scanner const & scanner);

But wait a minute! Scanner does not have a constructor of this signature. Why doesn't the compiler protest, like it always does when we try to call an undefined member function? The unexpected answer is that, if you don't explicitly declare a copy constructor for a given class, the compiler will create one for you. If this doesn't sound scary, I don't know what does.

Beware of default copy constructors!

The copy constructor generated by the compiler is probably wrong! After all, what can a dumb compiler know about copying user defined classes? Sure, it tries to do its best--it

  • does a bitwise copy of all the data members that are of built-in types and
  • calls respective copy constructors for user-defined embedded objects.

But that's it. Any time it encounters a pointer it simply duplicates it. It does not create a copy of the object pointed to by the pointer. That might be okay, or not--only the creator of the class knows for sure.

This kind of operation is called a shallow copy, as opposed to a deep copy which follows all the pointers. Shallow copy is fine when the pointed-to data structures can be easily shared between multiple instances of the object.

But consider, as an example, what happens when we make a shallow copy of the top node of a parse tree. If the top node has children, a shallow copy will not clone the child nodes. We will end up with two top nodes, both pointing to the same child nodes. That's not a problem until the destructor of one of the top nodes is called. It promptly deletes its children. And what is the second top node pointing to now? A piece of garbage! The moment it tries to access the children, it will stomp over reclaimed memory with disastrous results. But even if it does nothing, eventually its own destructor is called. And that destructor will attempt to delete the same children that have already been deleted by the first top node. The result? Memory corruption.

But wait, there's more! C++ not only sneaks a default copy constructor on you. It also provides you with a convenient default assignment operator.

Alert!Beware of default assignments!

The following code is perfectly legal.

SymbolTable symTab1 (100);
SymbolTable symTab2 (200);
// ...
symTab1 = symTab2;

Not only does it perform a shallow copy of symTab2 into symTab1, but it also clobbers whatever already was there in symTab1. All memory that was allocated in symTab1 is lost, never to be reclaimed. Instead, the memory allocated in symTab2 will be double deleted. Now that's a bargain!

But wait, there's even more! C++ not only offers a free copy constructor and a free assignment, it will also quietly use these two to return objects from functions. Here's a fragment of code from our old implementation of a stack-based calculator, except for one small modification. Can you spot it?

class Calculator
{
public:
    int Execute (Input & input);
    IStack const GetStack () const { return _stack; }
private:
    int Calculate (int n1, int n2, int token) const;

    IStack  _stack;
};

What happened here was that I omitted the ampersand in the return type of Calculator::GetStack and IStack is now returned by value. Let's have a very close look at what happens during such transfer. Also, let's assume for a moment that the compiler doesn't do any clever optimizations here. In particular, let's define GetStack out of line, so a regular function call has to be executed.

IStack const Calculator::GetStack () const
{
    return _stack;
}
//...
IStack const stk = calc.GetStack ();  // <- by value!

The process of returning an object by value consists of two steps: copy construction and assignment.

  • First of all, before executing the call to GetStack, the compiler pre-allocates some scratch space on the stack (I'm talking here about the internal call stack, where function arguments and local variables live at runtime). This scratch space is then passed to the function being called. To copy the value of _stack into scratch space, GetStack calls IStack's copy constructor.
  • After returning from GetStack, the assignment operator is called to copy the value from scratch space to the local variable stk.

It might look pretty complicated at first, but if you think about it, this is really the only sensible fool-proof way of returning an object by value. Especially if the programmer took care of both defining a copy constructor and overloading the assignment operator for the class in question.

Value Semantics

Let's try to make some sense of this mess. We've just learned about several features of C++ that, each taken separately, may turn our code against us. It's time to find out what they can do for us when used in proper context.

The question whether a given object should be passed by reference or by value should be decided on the level of its class. For some objects it makes little sense to be passed by value, for others it makes a lot of sense. By designing the class properly, we can accommodate either case.

First of all, we can easily protect objects of a given class from being passed by value by declaring a private copy constructor and a private assignment operator. No implementation of these methods is necessary. They are there just to stop the compiler from doing the magic behind our backs.

Suppose we want to prevent IStack objects from being passed by value. All we have to do is add these two lines to its class definition (strictly speaking, one of them would suffice).

class IStack
{
public:
    // ...
private:
    IStack (IStack const & i);
    IStack & operator = (IStack const & i);
    // ...
};

Now try compiling any of the code that attempted to pass an IStack to a function, return an IStack from a function or assign one IStack to another. It simply won't compile! If you make a mistake of omitting an ampersand following IStack's class name in any such context, the compiler will immediately catch it for you.

Should you, therefore, be adding copy constructors and assignment overloads to every class you create? Frankly, I don't think it's practical. There are definitely some classes that are being passed around a lot--these should have this type of protection in their declarations. With others--use your better judgment.

However, there are some classes of objects for which passing by value makes a lot of sense. Such classes are said to have value semantics. Usually, but not always, objects of such classes have relatively small footprint. Size is important if you care about performance. On the other hand, quite often the only alternative to passing some objects by value is to keep allocating new copies from the free store using new--a much more expensive choice. So you shouldn't dismiss value semantics out of hand even for larger object. Such would be the case, for instance, with some algebraic classes that we'll discuss in the following chapter.

In many cases giving a class value semantics does not require any work. As we've seen earlier, the compiler will gladly provide a default copy constructor and the default assignment operator. If the default shallow copy does adequate job of duplicating all the relevant data stored in the object, you shouldn't look any further.

In case shallow copy is not enough, you should provide the appropriate copy constructor and overload the assignment operator. Let's consider a simple example.

Object of class Value represents an integer value. However, the value is not stored as an int--it's stored as a string. What's more, the strings records the "arithmetic history" of the number.

class Value
{
public:
    Value ()
    {
        cout << "  Default constructor\n";
        _numString = new char [1];
        _numString [0] = '\0';
    }

    Value (int i)
    {
        cout << "  Construction/conversion from int " << i << endl;

        stringstream buffer;
        buffer << i << ends; // terminate string
        Init (buffer.str ().c_str ());
        Display ();
    }

    Value (char const * buf)
    {
        Init (buf);
    }

    Value (Value const & v)
    {
        cout << "  Copy constructor ( " << v._numString << " )\n";
        Init (v._numString);
        Display ();
    }

    Value & operator= (Value const & v)
    {
        cout << "  operator = ( " << v._numString << " )\n";
        if (_numString != v._numString)
        {
            delete _numString;
            Init (v._numString);
        }
        Display ();
        return *this;
    }

    friend Value operator+ (Value const & v1, Value const & v2 );
 
private:
    void Init (char const * buf)
    {
        int len = strlen (buf);
        _numString = new char [len + 1];
        strcpy (_numString, buf);
    }

    void Display ()
    {
        cout << "\t" << _numString << endl;
    }

    char * _numString;
};

A lot of things are happening here. First of all, we have a bunch of constructors. Let's use this opportunity to learn more about various types of constructors.

  • The default constructor with no arguments. You must have it, if you want to create arrays of objects of this class. A default constructor could also be a constructor with the defaults provided for all its arguments. For instance, Value::Value (int i = 0); would work just fine.
  • The constructor with a single argument.
    Unless declared as explicit, such constructor provides implicit conversion from the type of its argument to the type represented by the class. In this case, we are giving the compiler a go-ahead to convert any int to a Value, if such conversion is required to make sense of our code. We'll see examples of such conversions in a moment.
  • The copy constructor. It takes a (usually const) reference to an object of the same class and clones it.

Next, we have the overloading of the assignment operator. By convention it takes a (usually const) reference to an object and copies it into an existing object of the same class. Unlike the copy constructor, the assignment operator must be able to deal with an already initialized object as its target. Special precautions, therefore, are required. We usually have to deallocate whatever resources the target object stores and allocate new resources to hold the copy of the source. However, beware of the following, perfectly legitimate use of assignment:

Value val (1);
val = val;
Here, testing for "source equal to target" in the assignment operator will prevent us from trying to deallocate the source before making the copy. In fact, no copying is necessary when target is identical to the source.

There's also a bit of magic from the standard C++ library that I used to convert an int to its string representation. It's just like printing an int to cout, only that instead of cout I used a stringstream. It's a stream that has a string for its output. You can get hold of this string by calling the method stringstream::str. (Before you do that, make sure to null-terminate the string by outputting the special ends (end string) object). To get the string-stream functionality you have to #include <sstream> in your code and add the appropriate using statements.

Finally, to make things really interesting, I added an overloading of the plus operator for objects of type Value.

inline Value operator+ (Value const & v1, Value const & v2 )
{
    cout << "  operator + (" << v1._numString << ", " 
         << v2._numString << ")\n";
    stringstream buffer;
    buffer << v1._numString << " + " << v2._numString << ends;
    Value result (buffer.str ().c_str ());
    cout << "  Returning by value\n";
    return result;
}

Our operator+ takes two arguments of the type const reference to Value and returns another Value--their "sum"--by value. Next time the compiler sees an expression like val1 + val2, it will turn it into the call our method Value::operator+. In fact, the following two statements are equivalent:

val3 = val1 + val2;
val3.operator= (operator+ (val1, val2));

For us, humans, the first one is usually much easier to grasp.

Notice that I haven't declared operator+ to be a method of Value. Instead it is a free function. You can execute it outside of the context of any particular object. There is a good reason for doing that and I am about to explain it. But first, let me show you the alternative approach.

Value Value::operator+ (Value const & val) const
{
    cout << "  Value::operator + (" << val._numString << ")\n";
    stringstream buffer;
    buffer << _numString << " + " << val._numString << ends;
    Value result;
    result.Init (buffer.str ().c_str ());
    cout << "  Returning by value\n";
    return result;
}

Here, operator+ is defined as a member function of Value, so it requires only one argument. The first addend is implicit as the this object. The syntax for using this version of operator+ is identical to the free-function one and the equivalence works as follows,

val3 = val1 + val2;
val3.operator= (val1.operator+ (val2));

There is just one subtle difference between these two definitions--the way the two addends are treated in the "method" implementation is not symmetric. Normally that wouldn't be a problem, except when you try to add a regular integer to a Value object. Consider this,

Value v (5);
Value vSum = v + 10;

In general you'd need to define a separate overload of operator+ to deal with such mixed additions. For instance, using the free-function approach,

Value operator+ (Value const & val, int i);
or, using the "method" approach,
Value Value::operator+ (int i);

You see what the problem is? You can deal with adding an int to a Value using either method. But if you want to do the opposite, add a Value to an int, only the free-function approach will work. Like this:

Value operator+ (int i, Value const & val);
Value v (5);
Value vSum = 10 + v;

By the way, notice how smart the compiler is when parsing arithmetic expressions. It looks at the types of operands and, based on that, finds the correct overloading of the operator. Actually, it is even smarter than that!

I told you that you needed a special overloading of operator+ to deal with mixed additions. It's not entirely true. In fact both, adding an int to a Value and adding a Value to an int would work without any additional overloads of operator+. How? Because there is an implicit conversion from an int to a Value that the compiler may, and will, use in its attempts to parse a mixed arithmetic expression. This is only a slight generalization of what happens when you are adding an int to a double. For instance,

double x = 1.2;
int n = 3;
double result = x + n;
In order to perform this addition, the compiler converts n to a double and uses its internal implementation of "double" addition. Similarly, in
Value v (12);
int n = 3
Value result = v + n;
the compiler is free to convert n to a Value and use the overloaded operator+ that takes two (const references to) Values. In case you're wondering about the implicit conversion from int to Value, look again at the set of Value's constructors.

A constructor that takes a single argument, defines an implicit conversion from the type of its argument to its own type.

But converting an int into a Value is one thing. Here, the compiler has to do even more. It has to convert an int into a const reference to Value. It does it by creating a temporary Value, initializing it with an integer and passing a const reference to it to the called function. The tricky part is that the temporary Value has to be kept alive for the duration of the call. How the compiler does it is another story. Suffice it to say that it works.

What does not work is if you tried to do the same with a non-const reference. A function that takes a non-const reference expects to be able to have a side effect of changing the value of that argument. So using a temporary as a go-between is out of the question. In fact, the compiler will refuse to do implicit conversions to non-const references even in the case of built-in primitive types. Consider the following example,

void increment (double & x) { x += 1.0; }
int n = 5;
increment (n);   // <- error!

There is no way the compiler could figure out that you were expecting n to change its value from 5 to 6. And it can't pass the address of an int where the address of a double is expected, because the two have a completely different memory layout (and possibly size). Therefore it will tell you something along the lines, "Error: There's no conversion from int to (non-const) reference to double."

In contrast, the following code compiles just fine,

void dontIncrement (double const & x);
int n = 5;
dontIncrement (n);  // <- ok!
because the compiler is free to generate a temporary double, initialize it with the value on n and pass a const reference to it.

One last thing. Why did I declare operator= to return a reference to Value and operator+ to return Value by value? Let's start with operator=. It has to return something (by reference or by value) if you like this style of programming,

Value v1, v2, v3 (123);
v1 = v2 = v3;
or
if (v1 = v2) // notice, it's not ==
    ...
The chaining of assignments works, because it is interpreted as
v1.operator= (v2.operator= (v3));
and the assignment as condition works because it is equivalent to
if (v1.operator= (v2))
    ...
and the return value from operator= is interpreted as true if it's different from zero.

On the other hand, if you don't care for this style of programming, you may as well declare operator= to return void.

void Value::operator= (Value const & val)

I wouldn't recommend the same when overloading operator+, because we are usually interested in the result of addition. So instead, let me try to explain why returning the result of addition by value is preferable to returning it by reference. It's simple--just ask yourself the question, "Reference to what?" It can't be the reference to any of the addends, because they are supposed to keep their original values. So you have to create a new object to store the result of addition. If you create it on the stack, as a local variable, it will quickly disappear after you return from operator+. You'll end up with a reference to some stack junk. If you create it using new, it won't disappear from under your reference, but then you'll never be able to delete it. You'll end up with a memory leak.

Think of operator+ (or any other binary operator for that matter) as a two-argument constructor. It constructs a new entity, the sum of the two addends, and it has to put it somewhere. There is very little choice as to where to store it, especially when you're inside a complicated arithmetic expression. Return it by value and let the compiler worry about the rest.


To summarize, primitive built-in types are usually passed around by value. The same kind of value semantics for a user-defined type is accomplished by the use of a copy constructor and an overloaded operator=. Unless specifically overridden, the compiler will create a default copy constructor and default assignment for any user-defined type.


NextNext: Techniques