This week first covers the important concepts of smart pointers, move semantics and rvalue references.
Due to a variety of reasons C++ requires the programmer to handle memory allocations and deallocations manually. This has possible advantages for the very experienced programmer, since it allows you to control memory explicitly in order to squeeze the most performance from the code. Most commonly though, the disadvantage is that you can easily introduce bugs, memory leaks, crashes etc. Fortunately, some level of garbage collection is provided by the standard library in C++11 through a form of "reference counting" to ease the pain of manual memory management. These features are implemented as wrappers around regular pointers and are collectively called smart pointers.
Raw pointers also have some additional drawbacks, making it hard (but not impossible!) to work with them:
delete
or delete[]
)?This being said, raw pointers are not wrong. As long as you use them correctly.
Smart pointers can be used by including the <memory>
header:
#include <memory>
See also:
shared_ptr<T>
A shared_ptr<T>
implements the concept of shared ownership.
A shared_ptr<T>
- the manager object - wraps a pointer to an object of type T
- the managed or referenced object - while keeping track of how many shared pointers are referring to the same object.
If a copy of the shared_ptr<T>
is made, its reference count is increased (the reference count is also referred to as the use count), i.e. there's one more pointer that
refers to the same object.
So, a shared_ptr<T>
can actually share ownership of a managed object among several shared_ptr<T>
objects.
If, on the other hand, the shared_ptr<T>
goes out of scope (or is reset explicitly by reset()
or the shared_ptr<T>
is set to nullptr
), the reference count is decreased.
If the reference count becomes 0, there's no more shared_ptr<T>
objects referring the original piece in memory and the object pointed to - the managed object - is destructed.
shared_ptr<T>
in action:
std::shared_ptr<Foo> sp1(new Foo); // make a new shared_ptr, reference count is now at 1
std::cout << sp1.use_count() << std::endl; // check the reference count
std::shared_ptr<Foo> sp2 = sp1; // copy constructor, reference count increases again by 1
std::cout << sp1.use_count() << std::endl; // check the reference count
sp2->printFoo(); // you can call a member function like a built-in pointer
std::cout << *sp1 << std::endl; // you can dereference it a like a built-in pointer
std::cout << &(*sp1) << std::endl; // ask for its address
std::cout << &(*sp2) << std::endl; // ask for its address
// the addresses should match up
std::shared_ptr<Foo> sp3 = sp2;
sp2 = nullptr; // decreases the reference count
std::cout << sp3.use_count() << std::endl; // check the reference count
sp1.reset(); // decreases the reference count
std::cout << sp3.use_count() << std::endl; // check the reference count
Another nice thing about shared_ptr<T>
s (and unique<T>
s, but you'll learn about those in one of the next sections) is that you can pass custom deleters to them.
When you define a custom deleter and pass them to the shared_ptr<T>
object, the moment its reference counts reaches 0 and the referenced object needs to be destroyed, not the common delete
will be executed, but your custom defined deleter.
auto loggingDeleter = [](Foo* f) { // remember lambda's?
std::cout << "Deleting a Foo object..." << std::endl;
// you can simply print something here, call a log function, etc.
delete f;
};
std::shared_ptr<Foo> fooInstance(new Foo, loggingDeleter);
You do not have to stick to lambda's, you can pass all kind of callable things: normal functions, functors, ... Look it up and try it!
Note that, thanks to operator overloading, shared pointers behave very much like regular pointers; this includes polymorphism etc.
std::shared_ptr<Base> b_ptr(new Derived(/* ctor arguments */));
b_ptr->someVirtualMethod();
Instead of initializing b_ptr
using a manually allocated object with the new
operator you can use the make_shared
function which allocates an object and calls the object's constructor:
std::shared_ptr<Base> b_ptr = std::make_shared<Derived>(/* ctor arguments */);
// NOTE: You can actually just pass the Derived constructor arguments to make_shared()!
b_ptr->someVirtualMethod();
It is actually preferred to use make_shared<T>
instead of shared_ptr<T>(new T)
.
One of the reasons is performance: shared_ptr<T>(new T)
actually entails two memory allocations (one for the so-called control block - in which all management information is stored - and one for the managed object, i.e., the new
allocation), while make_shared<T>()
only allocates one memory chunk (and thus only does one memory allocation) which reduces the static program size and increases the speed of the executable code.
BUT there are also arguments to not use make_shared<T>
, e.g., it does not permit the specification of a custom deleter.
More information on:
weak_ptr<T>
The weak_ptr<T>
smart pointer is something different from shared_ptr<T>
: it holds a weak reference to the held object.
To put it informally: it allows you to look if an object is still around, but it does not keep the object around if nobody else needs it.
A weak_ptr<T>
can point to an object by copy or assignment from a shared_ptr<T>
or an existing weak_ptr<T>
.
std::shared_ptr<Foo> sp(new Foo);
std::weak_ptr<Foo> wp(sp); // constructs a weak_ptr from a shared_ptr
std::weak_ptr<Foo> wp2(wp); // construct weak_ptr out of copy constructor
std::weak_ptr<Foo> wp3; // empty weak_ptr
wp3 = wp; // assignement of weak_ptr
While shared_ptr<T>
has an influence on the lifetime of the managed object, a weak_ptr<T>
only observes the object, meaning that when you destroy or reassign a weak_ptr<T>
, it will not influence the reference count in the manager object.
Actually, the manager object also has an additional counter, the weak count, that keeps track of how many weak_ptr<T>
objects are pointing to it, plus 1 if the normal reference count > 0 (to understand why 1 is added, read here and here).
std::shared_ptr<Foo> sp(new Foo);
std::shared_ptr<Foo> sp2(sp); // reference count increases
std::cout << sp.use_count() << std::endl; // 2
std::weak_ptr<Foo> wp(sp); // reference count does NOT increase
std::cout << sp.use_count() << std::endl; // 2
It is actually not even possible to get the referenced object directly from a weak_ptr<T>
, you can only check if the referenced object was already deleted or not, by using the expired()
member function:
std::shared_ptr<Foo> sp(new Foo);
std::weak_ptr<Foo> wp(sp);
// wp.reset(); // REMARK: unlike shared_ptr<T>,
// you can not do wp = nullptr;
if (wp.expired()) // test if the weak pointer
std::cout << "Pointing to nothing..." << std::endl; // is pointing to nothing
else
std::cout << "Alive and kicking!" << std::endl;
If you want to get access to the referenced object, you can create a shared_ptr<T>
from the weak_ptr<T>
, using the lock()
member function or the shared_ptr<T>
constructor:
std::shared_ptr<Foo> sp(new Foo);
sp = nullptr;
std::weak_ptr<Foo> wp(sp);
std::shared_ptr<Foo> sp2 = wp.lock(); // Option 1
std::shared_ptr<Foo> sp3(wp); // Option 2
Find out yourself what the difference is between using the lock function or the shared_ptr<T>
constructor when the weak_ptr<T>
already expired before extracting the shared_ptr<T>
out of it!
In addition to tracking a referenced object and even allow ownership over it (by converting it to a shared_ptr<T>
), weak_ptr<T>
can also be used to break shared_ptr<T>
cyclic references: here you'll find a nice example.
More information on:
unique_ptr<T>
A unique_ptr<T>
implements, in contrast to shared_ptr<T>
, exclusive ownership.
So, while you can also point to an allocated object with unique_ptr<T>
, the pointed-to object gets automatically destroyed when the unique_ptr<T>
goes out-of-scope.
The semantics of unique_ptr<T>
are different from the shared ownership semantics of shared_ptr<T>
.
More specifically, a unique pointer to an object may not be copied because "No two unique_ptr instances can manage the same object".
The copy constructor and copy assignment are actually not allowed (i.e., =delete
).
In other words: there's just one unique pointer to an object at the same time.
An object's ownership can be transferred to another unique pointer though!
An advantage of this exclusive ownership is that the cost of a unique_ptr<T>
compared to that raw pointer is nothing (except when you provide a custom deleter)!
In contrast to the overhead of shared_ptr<T>
with its manager object and incrementing/decrementing/testing the counters, a unique_ptr<T>
is much more cost-friendly.
std::unique_ptr<Foo> up(new Foo); // uniquely-owned object
std::unique_ptr<Foo> up2(new Foo); // uniquely-owned object
// std::unique_ptr<Foo> up3(up); // ERROR - copy constructor not allowed!
// up2 = up; // ERROR - copy assignment not allowed!
To transfer ownership over a unique_ptr<T>
, we can use move construction and/or move assignment:
std::unique_ptr<Foo> up(new Foo); // up owns a Foo object
std::unique_ptr<Foo> up2; // up2 owns nothing!
std::unique_ptr<Foo> up3(std::move(up)); // move construction
// up3 now owns the Foo object, up owns nothing anymore
up2 = std::move(up3); // move assignment:
// up3 owns nothing anymore, up2 owns the Foo object
We'll go deeper into move semantics, rvalue references, std::move()
(which casts its element to a rvalue reference) later this session.
Good to know already is that you can pass the ownership of a unique_ptr<T>
out of a function:
unique_ptr<Foo> createFoo() {
std::unique_ptr<Foo> tmpFoo(new Foo); // create a Foo object
return tmpFoo;
}
std::unique_ptr<Foo> foo = createFoo(); // foo now owns the object
If you need shared ownership over a uniquely owned object, you can also easily convert a unique_ptr<T>
object a shared_ptr<T>
object:
std::shared_ptr<Foo> sharedFoo = createFoo();
As in the case of shared_ptr<T>
, there's a utility function to allocate an object and call its constructor: make_unique<T>
.
In contrast to make_shared<T>
which was introduced in C++11, make_unique<T>
was only introduced in C++14.
auto up = std::make_unique<Foo>();
The unique_ptr
is actually the C++11's replacement of the C++98 std::auto_ptr
smart pointer.
While std::auto_ptr
worked, the lack of move semantics (introduced in C++11) made it so much harder to work with.
Note that std::auto_ptr
is deprecated now!
More information on:
As seen in the previous section, a unique_ptr<T>
can not be copied.
However, the ownership of the resource to which unique_ptr<T>
points, can be transferred.
To transfer ownership to another object, move semantics were introduced, based on a proposal published in 2002.
More generally, move semantics are useful in following two scenarios: when you want to avoid expensive copies and when you deal with move-only types.
First, we'll have a closer look at these problems, afterwards we show how move semantics can help and how we can implement/use it.
Move semantics allows an object to take the ownership of another object's resources. This can be important in the following two scenarios:
When an object deals with resources that are expensive to copy, and you do not need the left-over object after copying the resource to the new object, moving the resources is the better choice.
Imagine having a class that holds resources that are expensive to construct, copy, and/or destruct.
As an example, we look at the class Foo
that holds a heap-allocated char*
.
class Foo {
public:
Foo();
Foo(const Foo&);
Foo& operator=(const Foo&);
private:
size_t m_length;
char* m_data;
};
Foo::Foo() {
m_data = new char[10];
for(unsigned int i = 0; i < 10; i++){
m_data[i] = (char)i;
}
}
Following the rule of three, we have to define a copy constructor, an assignment operator and the destructor on Foo
.
This copy constructor would look like this:
Foo::Foo(const Foo& other) {
size_t size = strlen(other.m_data) + 1;
m_data = new char[size]; // create a new string
memcpy(m_data, other.m_data, size); // copy it into "this" object
}
The copy assignment operator looks like this:
Foo& Foo::operator=(const Foo& other)
{
// check for self-assignment
if (this == &other)
return *this;
// first, delete the data in this object
delete[] m_data;
size_t size = strlen(other.m_data) + 1;
m_data = new char[size]; // create a new string
memcpy(m_data, other.m_data, size); // copy it into "this" object
return *this;
}
We neglect the destructor for now. Having this copy constructor you can make copies the following ways:
Foo giveMeAFoo() {
Foo foo;
// perhaps do something with foo
return foo;
}
Foo fooNew(foo1);
Foo fooNew(foo1 + foo2); // if, of course, the + operator is defined on Foo
Foo fooNew;
fooNew = giveMeAFoo();
In the first case, it is possible that foo1
is still used after this copy construction.
Remember that foo1
is a lvalue.
In the latter two however, re-using is not possible as now we are dealing with temporary objects, i.e., foo1 + foo2
and giveMeAFoo()
are rvalues.
So, in the latter two cases, why should we do an inefficient copy of the contents of Foo
and not just move the resources from the temporary object and let the temporary's destructor destruct that original resource.
This would be much more efficient!
Important Note that since C++11, the compiler is sometimes allowed to use copy elision
instead of a copy / move constructor as an optimization, therefore potentially changing the semantics of your code depending on the used optimizations!
Since C++17, the compiler is even required to do this in certain cases.
Read more about copy elision here.
The unique_ptr<T>
smart pointer implements exclusive ownership over an object - in contrast to shared_ptr<T>
.
To do so, copy constructing from and copy assigning to a unique_ptr<T>
is not allowed.
std::unique_ptr<Foo> up(new Foo); // uniquely-owned object
std::unique_ptr<Foo> up2(new Foo); // uniquely-owned object
// std::unique_ptr<Foo> up3(up); // ERROR - copy constructor not allowed!
// up2 = up; // ERROR - copy assignment not allowed!
Other move-only objects can be objects for which copying does not make sense such as locks, file handlers, etc.
In this section, we'll learn how we can enable the moving of resources of an object and avoid expensive resource copies. The solution is to define move constructors and move assignment operators.
A move constructor for our Foo
example that steals the contents of the other object, looks like this:
Foo::Foo(Foo&& other) { // rvalue reference alert, see next section!
// just get the pointer
m_data = other.m_data;
// put the other object's pointer to a nullptr
other.m_data = nullptr;
}
The move assignment operator would look like this:
Foo& operator=(Foo&& other) { // rvalue reference alert, see next section!
// check for self-assignment
if (this == &other)
return *this;
// just get the pointer
m_data = other.m_data;
// put the other object's pointer to a nullptr
other.m_data = nullptr;
return *this;
}
How efficient! Overloading the traditional copy constructor and assignment operator with these move constructor and move assignment operator makes sure that when a Foo
object is initialized with an rvalue or assigned an rvalue, the contents is moved and not copied.
So, in these examples we move the contents of Foo:
Foo foo1;
Foo foo2;
Foo foo3(foo1 + foo2); // if, of course, the + operator is defined on Foo
Foo foo4;
foo4 = giveMeAFoo(); // giveMeAFoo() returns a Foo object
Remember: we should move the contents as we can not re-use the temporary objects of foo1 + foo2
and giveMeAFoo()
.
Again, the compiler might not actually even use the move constructor here, but directly construct the temporary object in the address of foo3 as an optimization.
Use the command-line parameter -fno-elide-constructors
to tell the compiler to disable copy elision and compare the difference!
However, when using lvalues, the normal copy constructor and assignment operators are called:
Foo foo3;
Foo foo4(foo3); // normal copy constructor is called
Foo foo5;
foo5 = foo3; // assignment operator is called
How this magic happens, is explained in the next section where we discuss the weird Foo&&
syntax.
If X
is a type, than X&&
is called an rvalue reference to X
.
This should not be interpreted as a reference to a reference (which does not exist in C++)!
Traditional references like X&
are now referred to as lvalue references.
T. Becker explains rvalue references as follows: An rvalue reference allows a function to branch at compile time (via overload resolution) on the condition "Am I being called on an lvalue or an rvalue?"
So, what this means is that when it comes to function overload resolution, a function call with an rvalue parameter prefers a function signature with an rvalue reference, while lvalues prefers lvalue references. This is exactly what we observed happening in the previous section with the copy constructor, assignment operator, move constructor and move assignment operator!
More information on rvalue reference:
As just learned in the previous section, move semantics are normally only used on rvalues.
C++ however allows you to also apply it to lvalues, at your own risk.
To do so, you can use std::move
: it casts its argument to an rvalue, nothing more.
So while its name suggest that is moves something, it doesn't.
In the past, there were actually suggestions for a better name such as rvalue_cast
.
Going back to our Foo
example, we can now do this:
Foo foo1;
Foo foo2(std::move(foo1)); // while foo1 is an lvalue, it will be moved!
Foo foo3;
foo3 = std::move(foo2); // while foo2 is an lvalue, it will be moved!
Knowing this, we can now figure out how to move unique_ptr<T>
:
std::unique_ptr<Foo> up(new Foo); // uniquely-owned object
std::unique_ptr<Foo> up2(new Foo); // uniquely-owned object
std::unique_ptr<Foo> up3(std::move(up)); // OKAY - move constructor called!
up2 = std::move(up3); // OKAY - move assignment operator called!
Read the following two documents in order to answer the two questions that are left open in the following sections:
When dealing with class inheritance and move semantics, you want to be sure that move semantics are also applied to the base/parent part of your child objects. One should be aware on how to implement the move constructor and assignment operator of the child object:
// FooChild inherits from Foo
FooChild(FooChild&& other) : Foo(other) { // WRONG move constructor implementation!
// FooChild moving
}
FooChild(FooChild&& other) : Foo(std::move(other)) { // OKAY move constructor implementation!
// FooChild moving
}
Why is the first considered wrong and the second correct?
We've overloaded the copy constructor and assignment operator with the move constructor and move assignment operator for Foo
.
Assume we want to return a Foo
object out of a function:
// FooChild inherits from Foo
Foo giveMeAFoo() {
Foo foo;
// ...
return foo; // Option 1
return std::move(foo); // Option 2
}
Which option should be preferred here and why? (Keep copy elision in mind!)
Important A convenient feature of C++11 are initializer lists. For example:
# include <initializer_list>
class A {
public:
A(std::initializer_list<int> l);
};
Where the constructor is defined as:
A::A(std::initializer_list<int> l) {
for (auto i : l) {
std::cout << i << std::endl;
}
}
Now you can do the following, because A
accepts an std::initializer_list<int>
:
A a_obj = {1, 2, 3};
Note that the use of std::initializer_list
is already integrated into the standard library. That is why you can do this:
std::vector<int> v = {1, 2, 3};
Go to the assignment: https://classroom.github.com/a/cvTjEYtV
Go to the assignment: https://classroom.github.com/a/14KvQNRH
Go to the assignment: https://classroom.github.com/a/7Ex0r2mO