Lecture 15
In this lecture we will discuss function overloading, with a focus on constructors, and in particular, introducing the copy constructor and the move constructor.
Function overloading
In Assignment5, we overload the encrypt and decrypt methods to define different encryption schemes.
Here we take a look at overloading functions based on whether the argument is an lvalue or an rvalue. This is a lot less obvious than examples that simply take different numbers or types of arguments because it requires some understanding of how the compiler represents objects.
Copy and move constructors
We have seen simple constructors in many of the classes in assignments and lectures. Two other types of constructors that are commonly defined by classes are the copy and move constructors.
A copy constructor allows you to create a new object by copying from an existing one. For example, suppose you want to create a new Maze instance from an existing one (which you will no longer need), you would define that as
- Maze(const Maze& rhs) : size(rhs.size), maze(rhs.maze) {}
- //...
- // then in your main:
- Maze newMaze(oldMaze);
(assuming that the maze is defined as a 2-D char array private data member of Maze. Note that this is what is called a shallow copy, in that it only copies the pointer to the actual maze. A deep copy would loop over rhs.maze and assign it to this->maze element by element.)
Even though you may know that you'll never need oldMaze again, the compiler will keep two mazes allocated, oldMaze and newMaze -- there is no way to indicate that oldMaze can be discarded after the newMaze is constructed or to avoid having two mazes in memory at the same time.
So how do we get rid of such "temporary" objects? One solution is to use move semantics. In the case of object construction, this means implementing a move constructor.
lvalues and rvalues
To understand the difference between copy and move constructors, we first need to introduce the concepts of lvalues and rvalues.
An lvalue is an expression that refers to a memory location and allows us to take the address of that memory location via the & operator. You can also think of lvalue as something that can be the left-hand side of an assignment statement. An rvalue is an expression that is not an lvalue. Examples are:
- // lvalues:
- //
- int i = 42;
- i = 43; // ok, i is an lvalue
- int* p = &i; // ok, i is an lvalue
- int& foo();
- foo() = 42; // ok, foo() is an lvalue
- int* p1 = &foo(); // ok, foo() is an lvalue
- // rvalues:
- //
- int foobar();
- int j = 0;
- j = foobar(); // ok, foobar() is an rvalue
- int* p2 = &foobar(); // error, cannot take the address of an rvalue
- j = 42; // ok, 42 is an rvalue
Move semantics
A move constructor of class T
is a non-template constructor whose first parameter is T&&
, const T&&
, volatile T&&
, or const volatile T&&
, and either there are no other parameters, or the rest of the parameters all have default values.
In the lecture15/sheep.hpp
implementation:
- class Wool {
- public:
- // Constructors
- // Default constructor
- Wool() : color("White") {}
- // Constructor with initialization
- Wool(string c) : color(c) {}
- // Copy constructor:
- Wool(const Wool& other) : color(other.color) {}
- // Move constructor
- Wool(Wool&& other) : color(other.color) {}
- inline void setColor(string color);
- inline const string getColor();
- private:
- string color;
- };
So what is the difference between the copy constructor and the move constructor? The main distinction is that when using a move constructor, the compiler will not create temporary copies of values or variables on the stack.
Rvalue References in more detail
If X
is any type, then X&&
is called an rvalue reference to X
. For better distinction, the ordinary reference X&
is now also called an lvalue reference.
An rvalue reference is a type that behaves much like the ordinary reference X&
, with several exceptions. The most important one is that when it comes to function overload resolution, lvalues "prefer" old-style lvalue references, whereas rvalues "prefer" the new rvalue references:
- void foo(X& x); // lvalue reference overload
- void foo(X&& x); // rvalue reference overload
- X x;
- X foobar();
- foo(x); // argument is lvalue: calls foo(X&)
- foo(foobar()); // argument is rvalue: calls foo(X&&)
It is true that you can overload any function in this manner, as shown above. But in the overwhelming majority of cases, this kind of overload should occur only for copy constructors and assignment operators, for the purpose of achieving move semantics.
Overloading based on return value only
C++ does not allow definition of two functions that differ only in return values, for example, the following will produce an error:
int sum(); double sum();
However, templates offer a mechanism to do just that. More on this later...
template <class T> T sum() { T result; // code to compute result return result; }