Like Bigger Stones: Argument Passing in C++, Part 2

In my previous article, I talked about how C++ handles parameter passing when the arguments are primitive types or arrays. This article will talk about passing objects in C++. Objects are more complicated than primitives or arrays, so as you can imagine, passing them is also more complicated.

Before you read this article, you should understand the three basic C++ mechanisms for parameter passing: by value, by reference, or by pointer (or “simulated reference”). You should also understand how C++ hanldes automatic local variables. If you don’t understand this already, you should read my previous article on the subject. (If you don’t understand that, then I’m not a good writer, and you shouldn’t read this article anyway.)

Unlike primitives or arrays, object passing requires some knowledge of inheritance, subclass polymorphism, composition, and assignment vs. initialization. These subjects are complicated enough to deserve their own article (at least). I am actually writing that article, and will post it after this one. But, you do need to know the very basics. If you have done any programming in any other object-oriented language (Java, C#, Python, whatever), then you probably know how it works, so I’ll only cover this very briefly.

Here are some definitions, and their relevance to this article:

  • Inheritance means that one class can inherit the member functions (methods) and variables (fields) of another class. The class that is inherited is called a superclass or base class, and the class that does the inheriting is called a subclass or derived class. Subclasses can override the member functions of its superclass; this means that the subclass can create a different implementation (that is, different code) for the function in the superclass. To be overridden, C++ requires that the member function be declared virtual in the base class; for this reason, they are called virtual functions or virtual methods. If a subclass does not override a virtual function, then the implementation from the superclass is used.
  • Subclass polymorphism means that you can treat an object of a subclass type, as if it were an object of a base class type. For this article, I will deal only with passing and returning subclass objects as if they were base class objects. Remember that types apply not just to objects, but to pointers and references as well.
  • A composite object is an object that “has” another object. For this article, I will only talk about composite objects that have pointers to other objects.
  • Initialization means providing an object with values as it is being created. Assignment means assigning values to an object that has already been created. So by definition, assignment happens after initialization. In C++, the constructor is used for initialization, and the assignment operator is used for assignment.

This is a lot to take in, but hopefully you knew most of it already. To see why these ideas are important, read on.

Passing Objects By Value

Passing by value has a lot of advantages, as I covered in the last article, but it also has one big drawback. If we pass by value, we need to make a copy of everything that is passed. For example, if we could pass a large array by value, we would need to make a copy of every single element in that array.

To solve this problem, C++ passes arrays by “simulated reference” – a pointer that has the syntax of a reference. You might guess that objects are handled the same way in C++. After all, objects can have lots of member variables, so they may actually require more memory than a small array would. Furthermore, other object-oriented languages pass objects by simulated reference, so you would expect C++ to do the same.

But you would be wrong. By default, objects in C++ are passed by value. The main reason is that C++ classes are an extension of C structs. Structs are passed by value in C, so C++ adopted this behavior.

Behind the scenes, it works largely the same way as passing primitive types. The formal parameter is a local variable, created as the argument is passed. The argument’s value is pushed onto the stack, and resides in the stack frame. When the function returns, the stack unwinds, that value is destroyed, and the local variable goes out of scope.

But the stack creation and destruction are not as simple. Since the local variable’s value is an object, that object must be initialized, and that is the job of the constructor. So when the object is created, the function’s argument is passed to the constructor. The object must also be deleted from memory when the stack is unwound, so upon return, the object’s destructor is called.

In most cases, the object is created using the class’s copy constructor. A copy constructor is a constructor that takes, as its sole argument, an object of the same type. If there is no explicit copy constructor (that is, nobody wrote the code for one), then a default copy constructor will be generated by the compiler. The default copy constructor makes a bitwise copy of the object that is passed to it. A bitwise copy is exactly what it sounds like: it simply takes whatever is at the other object’s memory location, and copies it bit-by-bit into its own memory location. It is exactly what happens when you pass a primitive type by value.

This is also what happens with the assignment operator. If none is explicitly defined, the C++ compiler will generate one for you, and it will simply do a bitwise copy.

Note, however, that it is not always the copy constructor that is used. Let’s say you created a class, called MyClass, with a constructor that takes an int as a parameter. Consider the following code:

// Function signature
void printMyClass(MyClass myObj);

// in main()
printMyClass(3); // Huh???

Suprisingly, this code will compile and run with no problems. The reason has to do with function overloading. This is when a function with the same name can have different implementations, depending upon its signature. In order to accomplish this, the C++ compiler looks at the formal parameters associated with the function name, and compares it to the actual arguments that are passed to the function. The technical term for this is argument-dependent lookup. If there is an exact match, it uses that implementation. If not, it sees if the argument type can be converted to one that does match; and if so, it implictly converts the argument to that type.

You’ve probably seen this with math functions: a function accepts a double as a parameter, but if you pass it an integer literal, it will be implicitly converted. C++ has the same behavior with objects. In the example above, the compiler simply passes the integer to the constructor of the MyClass object.

Of course, this will only work if there is no ambiguity. Let’s say you have two classes that accept a single integer in its constructor, and you overload the function to accept arguments of both class types. The compiler won’t know which overloaded function to call, so the code won’t compile.

You may think this is polymorphism, and techically, you’d be right. It is a specific type of polymorphism called ad hoc polymorphism. But it is not the type of polymorphism that most object-oriented programmers refer to when they say “polymorphism,” and it is not the same type of polymorphism that I referred to earlier. That would be subtype polymorphism. I will cover that a little later in the article.

When returning by value, the same process happens. The code that called the function copies the object into a local variable, using the copy constructor or assignment operator as appropriate. After it is copied, the returned object is destroyed, and its destructor is called. (Some compilers will optimize this away in some cases; but this is compiler-specific behavior, so you shouldn’t count on it.)

Passing Objects by Pointer

If you don’t want to copy an entire object onto the stack, then one option is to pass a pointer to an object. Passing an object by pointer works the same as passing a pointer to a primitive type. The value of the pointer is copied to a local automatic variable. But this value is a memory address, so when the pointer is dereferenced inside the function, it also affects the object outside that function.

If you do not want the function to be able to modify the object it points to, you should make its formal parameter a pointer to a constant object. To do this, you must place the const keyword after the pointer declaration – that is, after the asterisk. If you place it before the asterisk, you are instead declaring a constant pointer. This is a pointer that can’t be re-assigned to point to a different object. To make it a constant pointer to a constant object, you place the const keyword on both sides of the asterisk.

Be forewarned: it is possible to delete a pointer to a constant object, just as it is outside the function. If your function accepts a pointer to a constant, the client code (the code that uses the function) can be certain that the object will not be changed. But there is no way to guarantee that it is not deleted.

When you pass a pointer to a function, the object itself is not copied to the stack. This means that the constructor is not called when the argument is passed, and the destructor is not called when the function returns. Because no constructor is involved, implicit type conversion is not possible when passing by pointer, as it is with passing by value.

There is one “gotcha” associated with passing by pointer. Remember that pointers can be null. If you try to use the member access operator on a null pointer, your program will crash. Pointers can be re-assigned at runtime, so there’s no way for the compiler to know the pointer is null; you have to explicitly do a null check in your code.

Of course, it is also possible to return by pointer. But you should do this with extreme care. Let’s say you write a function whose purpose is to create a pointer to a MyClass object, depending upon the parameters passed to it. Here’s an example of how you would not want to do it:

MyClass* create(int x)
{
    MyClass obj(x);
    return &obj;
}

The problem is that obj is an automatic variable, local to the create function. The address that you are returning is a location on the stack. When the function returns, the stack is unwound – and that object is destroyed. So, the address you are returning points to an object that no longer exists.

This is called a dangling pointer. A dangling pointer is a pointer that points to a location in memory that is no longer valid. Dangling pointers result in undefined behavior; usually, it means your application will crash at runtime. Returning a pointer to a local variable is the most common cause of dangling pointers. Most compilers will warn you when this happens, but will still compile the code.

If you do want to return a pointer, then you should return a pointer to an object created on the heap; that is, one created using the new keyword. You should make sure you document this behavior, because deleting the object will be the responsibility of the code that called the function.

Another cause of dangling pointers is deleting a pointer passed to the function. This is usually done unintentionally, when composite objects are destroyed. I’ll talk about that later in the article.

Passing Objects by Reference

When you pass by reference, you are simply declaring that the parameter is an alias to the object passed as an argument. This means passing by reference has much of the same behavior as passing by pointer. The object is not copied, so neither the constructor nor the destructor of the object is called. And when you change the object inside the function, the object is also changed outside the function.

But there is one big advantage to using references: references cannot be null. This means that your function can safely use the member access operator on an object passed by reference. It also guarantees that an object passed by reference will not be deleted inside the function.

Like pointers, references can be declared const, and this will mean that the parameter refers to a constant value. This does not mean that the argument must actually be constant; it means that it will be treated as if it were constant inside the function. If you are not going to modify the object inside the function, then that function should accept a constant reference.

There is one other, somewhat surprising, difference between pointers and references. Remember that implicit type conversion is not allowed with pointers; no object is copied, so no constructor is called. Passing by reference doesn’t copy an object either, so you would think that this sort of implicit type conversion also wouldn’t happen with references.

But that is not the case. In fact, implicit conversion will happen – but only if you pass by constant reference. The C++ compiler will construct a temporary object (an rvalue) on the stack, just as if it were an automatic variable, and the reference parameter is initialized to it. But since it is an rvalue, the C++ standard requires that it be initialized to a const reference. If you want to read more about this feature, read A Candidate For the “Most Important const” by Herb Sutter.

Because the parameter is a bona-fide reference, not a simulated reference, you can’t switch between pointer and non-pointer syntax (like you can with an array). Other than the function signature, the syntax is exactly the same as if you had passed the object by value.

It is possible to return a dangling reference, just as it is to return a dangling pointer. This normally occurs when you return a reference to a local automatic variable. For this reason, there are very few cases where you should return by reference. You should only return a reference to an object that outlives the function call.

This only happens if you are chaining operations on the same object. This happens when an operation mutates (changes) an object, and you want to return the mutated object, so that it can be operated on again. There are only two situations (that I’ve encountered) where you need to do this:
1. Operator overloading. For example, when overloading the stream output operator, you should return a reference to the ostream object. In these cases, return a reference to the same object that was passed in to the operator. Only do this when the operator actually mutates the object; most arithmetic operators, for example, do not.
2. Method chaining. For example, if a graphics object can be rotated and mirrored, you could write obj.rotate(90).mirror(). (This should be familiar to Java programmers.) To make this possible, return a reference to *this in your member function.

Whatever you do, you should never return a reference to any object created by your function. This includes temporary objects created via implicit type conversion, so it’s a bad idea to return objects that were passed in by constant reference.

Subtype Polymorphism

If you recall, subtype polymorphism is when an object of a subclass type is treated as if it were an object of a superclass type. In order to show how this works, let’s create two classes, a base class and a derived class. Here’s the code:

class MyBase
{
public:
    // Constructor
    MyBase(int val) : _val(val) {}
    // Virtual member function
    virtual int getValue(void) const { return _val; }
protected:
    int _val;
};

class MyDerived : public MyBase
{
public:
    // Constructor
    MyDerived(int val, int scale) : MyBase(val), _scale(scale) {}
    // Overrides MyBase implementation of virtual function
    virtual int getValue(void) const { return _val * _scale; }
private:
    int _scale;
};

Subtype polymorphism means that you can pass a MyDerived object to a function that accepts a MyBase object as a formal parameter. Let’s see an example in code:

// Function signature
void printMyClass(MyBase myObj);

// in main()
MyDerived subObj(3, 5);
printMyClass(subObj);

This code compiles and runs perfectly fine. But what is going on behind the scenes? Specifically, we have to ask ourselves two questions:
1. MyDerived has its own member variable, _scale, that MyBase does not. What happens to it?
2. If you call myObj.getValue() inside the function, will it resolve to the implementation in MyBase, or the one in MyDerived?

The answers, respectively, are:
1. It doesn’t get copied at all.
2. It will resolve to the one in MyBase.
Here’s why.

Remember that when an object is passed by value, its constructor is called. In this case, it is the copy constructor of MyBase. The default copy constructor takes the object that is passed to it, and does a bitwise copy of it. But in this case, the object that is passed to it is considered an object of the MyBase type. Indeed, there is nothing more that it could possibly do. If it tried to copy all of the fields, it would run out of memory first. So it only copies the fields for which it has allocated memory: the fields that are inherited from the base class.

The term for this is object slicing. It is so called because the compiler “slices off” the fields that are not inherited from the base class. It is not just limited to passing objects, either. This is the same way the assignment operator works, so the following code would also result in slicing:

MyBase myObj(3);
MyDerived subObj(5, 2);
myObj = subObj; // Snip!

The fact that the object is copied also explains why virtual functions will always resolve to the superclass. The object that is constructed actually is an object of the superclass type. Once the function starts execution, there is no way for it to tell what the type of the actual argument was. The only thing it has to work with is the copy: the one that currently resides in the stack frame.

Not all base classes can be passed by value. It is possible for a class to define a pure virtual function: a virtual function that does not have any implementation at all. A class that has even one pure virtual function is an abstract class. (A class that is not abstract is a concrete class.) Pure virtual functions are written to define the signatures of member functions that subclasses must implement. If they do not, then those subclasses are themselves abstract.

It is impossible to instantiate an abstract class; it would cause the program to crash if you ever tried to call a pure virtual function. Because it’s impossible to instantiate, it’s also impossible to pass by value. The object on the stack must be created when the function is called, and this is not possible with abstract classes.

None of this happens when an object is passed by pointer or by reference. No copy is made, so slicing does not occur. The virtual function that is called is the one associated with the object – not the one associated with the type of the formal parameter.

This makes it possible for a function to accept a pointer or reference to an abstract base class. It does not matter that the base class is abstract; the parameter refers to some object that is actually instantiated. That object’s actual type must be of some concrete subclass, so all virtual functions must be implemented.

This is the real power of polymorphism. You can write a function whose declared type is of a base class, but whose behavior is that of the actual type of the object. This is simply not possible if you pass an object by value.

It is also not possible if you return an object by value. If you are returning an object by value, it should be of the exact type that you are returning. If you want the returned object to be polymorphic, you must return a pointer or a reference. Of course, doing so incorrectly can lead to dangling pointers and dangling references – so be careful.

There is only one advantage to returning by value: it is easier from a memory management perspective. The returned object is copied, so may be assigned to an automatic local variable in the client code. This means that the client code won’t need to explicitly delete the returned object; it will be deleted automatically as the stack is unwound.

Composite Objects

Passing by value may not have the advantage of polymorphism, but it might still have benefits. Like passing primitive types by value, one benefit should be that your function is a pure function. Remember, pure functions cannot alter the arguments that are passed to it; and if the function only has access to a copy, it seems that this would be accomplished automatically.

Unfortunately, this is not necessarily true when you pass composite objects by value. Recall that a composite object, for our purposes, is one that has a member variable that is a pointer to another object.

It is probably easier to show this in code. Here is an example of a very simple (and, as we’ll see, badly designed) composite class:

class MyComposed
{
public:
    MyComposed(int x)
    {
        ptr = new MyClass(x);
    }
    ~MyComposed()
    {
        delete ptr;
    }
private:
    MyClass* ptr;
};

Since this code doesn’t contain an explicit copy constructor, the compiler will generate one that does a bitwise copy. So when it has a variable that is a pointer, it only copies the pointer’s value. This means that the original and the copy will both point to the same memory address – thus to the same object. If the class has any member function that changes that object, then calling that function on the copy will also change the object in the original.

It gets worse. Composite objects handle the life cycles of the objects they compose. This means that when the composite object is destroyed, it should also destroy the object it composes. To be more specific: in its destructor, it should call delete on the pointer variable. If the composite object didn’t do this, then the object it points to would never get deleted, causing memory leaks.

But remember what happens when a function returns. As the stack unwinds, the destructors are called on all of the function’s automatic local variables. If the variable is a composite object, then the object that it composes is also deleted. This means that when the object is deleted in the local variable, it is also deleted in the original.

This results in a dangling pointer in the original object. Any function that is passed an object of this class will corrupt the original, even if the function does nothing.

This also happens when returning composite objects by value. When the object is returned, it is copied into some local variable in the client code, and the one that you returned is destroyed (along with the composed object).

There is only one way to avoid this. When the composite object is copied or assigned, it must also copy any objects that it points to. The technical term for this is a deep copy. The creator of the composite class must explicitly create deep copies in the copy constructor and assignment operator; there is no way to get the compiler to do it for you.

The need for deep copies leads to what is known in the C++ community as the rule of three. This rule says that if you define any of the destructor, copy constructor, or assignment operator, then you should define all of them.

Of course, this doesn’t occur if you are passing a composed object by pointer or by reference. No copy is made in those cases, and the destructor is not called as the function returns. But this does not mean you should ignore the rule of three when you are writing classes, as there is no way to guarantee that the client code will not pass or return an object by value.

Rules of Thumb

To summarize everything in this article, I’ve put together some rules of thumb that you should follow when passing and returning objects.

When passing objects:

  • Avoid passing by value.
  • Pass by pointer only if the function can accept a null pointer.
  • If the object should not be modified inside the function, pass by constant reference.
  • If the object should be modified inside the function, pass by non-constant reference.

When returning objects:

  • If you are chaining operations on a mutated object, return that object by reference.
  • If the returned object should not be treated polymorphically, then return by value.
  • In all other cases, create the returned object on the heap, and return by pointer.

When writing classes:

  • Constructors and overloaded operators are also “functions,” so they should follow the above rules of thumb.
  • Always obey the rule of three.

See For Yourself

To demonstrate all of this, I have created a project on GitHub that you can download and compile. It is very simple, and uses the console to output everything. I created it using Code::Blocks, and if you have this IDE, you can simply use the .cpb file in the repo. If you need a Makefile or are using Visual Studio, you’re on your own, though it’s simple enough for you to figure out by yourself.

Here is a link to the repo:
https://github.com/kgiesing/Passing

Hope that helps, and that you understood all of it. If not – or if I got anything wrong – then please let me know.

Advertisements

About Karl

I live in the Boston area, and am currently studying for a BS in Computer Science at UMass Boston. I graduated with honors with an AS in Computer Science (Transfer Option) from BHCC, acquiring a certificate in OOP along the way. I also perform experimental electronic music as Karlheinz.
This entry was posted in c++, Programming. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s