By default, C++ passes objects to and from functions by value (a char- acteristic it inherits from C). Unless you specify otherwise, function parameters are initialized with copies of the actual arguments, and function callers get back a copy of the value returned by the function.
These copies are produced by the objects’ copy constructors. This can make pass-by-value an expensive operation. For example, consider the following class hierarchy:
class Person { public:
Person(); // parameters omitted for simplicity
virtual ~Person(); // see Item 7 for why this is virtual ...
private:
std::string name;
std::string address;
};
class Student: public Person { public:
Student(); // parameters again omitted
virtual ~Student();
...
private:
std::string schoolName;
std::string schoolAddress;
};
ptg7544714 Now consider the following code, in which we call a function, validat-
eStudent, that takes a Student argument (by value) and returns whether it has been validated:
bool validateStudent(Student s); // function taking a Student // by value
Student plato; // Plato studied under Socrates
bool platoIsOK = validateStudent(plato); // call the function What happens when this function is called?
Clearly, the Student copy constructor is called to initialize the parame- ter s from plato. Equally clearly, s is destroyed when validateStudent returns. So the parameter-passing cost of this function is one call to the Student copy constructor and one call to the Student destructor.
But that’s not the whole story. A Student object has two string objects within it, so every time you construct a Student object you must also construct two string objects. A Student object also inherits from a Per- son object, so every time you construct a Student object you must also construct a Person object. A Person object has two additional string objects inside it, so each Person construction also entails two more string constructions. The end result is that passing a Student object by value leads to one call to the Student copy constructor, one call to the Person copy constructor, and four calls to the string copy constructor.
When the copy of the Student object is destroyed, each constructor call is matched by a destructor call, so the overall cost of passing a Student by value is six constructors and six destructors!
Now, this is correct and desirable behavior. After all, you want all your objects to be reliably initialized and destroyed. Still, it would be nice if there were a way to bypass all those constructions and destructions.
There is: pass by reference-to-const: bool validateStudent(const Student& s);
This is much more efficient: no constructors or destructors are called, because no new objects are being created. The const in the revised parameter declaration is important. The original version of validateStu- dent took a Student parameter by value, so callers knew that they were shielded from any changes the function might make to the Student they passed in; validateStudent would be able to modify only a copy of it. Now that the Student is being passed by reference, it’s necessary to also declare it const, because otherwise callers would have to worry about validateStudent making changes to the Student they passed in.
Passing parameters by reference also avoids the slicing problem. When a derived class object is passed (by value) as a base class object, the
ptg7544714 base class copy constructor is called, and the specialized features that
make the object behave like a derived class object are “sliced” off.
You’re left with a simple base class object — little surprise, since a base class constructor created it. This is almost never what you want.
For example, suppose you’re working on a set of classes for imple- menting a graphical window system:
class Window { public:
...
std::string name() const; // return name of window virtual void display() const; // draw window and contents };
class WindowWithScrollBars: public Window { public:
...
virtual void display() const;
};
All Window objects have a name, which you can get at through the name function, and all windows can be displayed, which you can bring about by invoking the display function. The fact that display is virtual tells you that the way in which simple base class Window objects are displayed is apt to differ from the way in which the fancier Window- WithScrollBars objects are displayed (see Items 34 and 36).
Now suppose you’d like to write a function to print out a window’s name and then display the window. Here’s the wrong way to write such a function:
void printNameAndDisplay(Window w) // incorrect! parameter
{ // may be sliced!
std::cout << w.name();
w.display();
}
Consider what happens when you call this function with a Window- WithScrollBars object:
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
The parameter w will be constructed — it’s passed by value, remem- ber? — as a Window object, and all the specialized information that made wwsb act like a WindowWithScrollBars object will be sliced off.
Inside printNameAndDisplay, w will always act like an object of class Window (because it is an object of class Window), regardless of the type of object passed to the function. In particular, the call to display inside
ptg7544714 printNameAndDisplay will always call Window::display, never Window-
WithScrollBars::display.
The way around the slicing problem is to pass w by reference-to-const: void printNameAndDisplay(const Window& w) // fine, parameter won’t
{ // be sliced
std::cout << w.name();
w.display();
}
Now w will act like whatever kind of window is actually passed in.
If you peek under the hood of a C++ compiler, you’ll find that refer- ences are typically implemented as pointers, so passing something by reference usually means really passing a pointer. As a result, if you have an object of a built-in type (e.g., an int), it’s often more efficient to pass it by value than by reference. For built-in types, then, when you have a choice between pass-by-value and pass-by-reference-to-const, it’s not unreasonable to choose pass-by-value. This same advice applies to iterators and function objects in the STL, because, by con- vention, they are designed to be passed by value. Implementers of iter- ators and function objects are responsible for seeing to it that they are efficient to copy and are not subject to the slicing problem. (This is an example of how the rules change, depending on the part of C++ you are using — see Item 1.)
Built-in types are small, so some people conclude that all small types are good candidates for pass-by-value, even if they’re user-defined.
This is shaky reasoning. Just because an object is small doesn’t mean that calling its copy constructor is inexpensive. Many objects — most STL containers among them — contain little more than a pointer, but copying such objects entails copying everything they point to. That can be very expensive.
Even when small objects have inexpensive copy constructors, there can be performance issues. Some compilers treat built-in and user- defined types differently, even if they have the same underlying repre- sentation. For example, some compilers refuse to put objects consist- ing of only a double into a register, even though they happily place naked doubles there on a regular basis. When that kind of thing hap- pens, you can be better off passing such objects by reference, because compilers will certainly put pointers (the implementation of refer- ences) into registers.
Another reason why small user-defined types are not necessarily good pass-by-value candidates is that, being user-defined, their size is sub- ject to change. A type that’s small now may be bigger in a future
ptg7544714 release, because its internal implementation may change. Things can
even change when you switch to a different C++ implementation. As I write this, for example, some implementations of the standard library’s string type are seven times as big as others.
In general, the only types for which you can reasonably assume that pass-by-value is inexpensive are built-in types and STL iterator and function object types. For everything else, follow the advice of this Item and prefer pass-by-reference-to-const over pass-by-value.
Things to Remember
✦Prefer pass-by-reference-to-const over pass-by-value. It’s typically more efficient and it avoids the slicing problem.
✦The rule doesn’t apply to built-in types and STL iterator and func- tion object types. For them, pass-by-value is usually appropriate.