0% found this document useful (0 votes)
1 views

lecture-05

The document outlines best practices for resource management in C++ using smart pointers, emphasizing the use of unique_ptr and shared_ptr for memory management and ownership transfer. It discusses the importance of exception handling, the rule of five in class design, and introduces threading concepts along with mutexes for safe concurrent access. Additionally, it highlights the evolution of C++ features related to resource management and threading from C++98 to C++17.

Uploaded by

Tizi Martin
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
1 views

lecture-05

The document outlines best practices for resource management in C++ using smart pointers, emphasizing the use of unique_ptr and shared_ptr for memory management and ownership transfer. It discusses the importance of exception handling, the rule of five in class design, and introduces threading concepts along with mutexes for safe concurrent access. Additionally, it highlights the evolution of C++ features related to resource management and threading from C++98 to C++17.

Uploaded by

Tizi Martin
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 49

Advanced C++

February 11, 2019

Mike Spertus
[email protected]
Next week’s lecture
⚫ I will be at the C++ Standards Meeting, so
your TA Patrick Shriwise will be lecturing
⚫ As an extra credit HW problem, please
submit and also email directly to me a
question to ask the standards committee
⚫ I will choose one or two of them to be
answered by someone specializing in that
area
Resource management best
practices
⚫ Always use a smart pointer (e.g., unique_ptr
or shared_ptr) to manage the lifetime of an
object
⚫ Use make_shared or make_unique rather
than new to create the object
⚫ Likewise, use RAII to ensure other resources
get reliably released as well
⚫ We will see this shortly with scoped_lock
Review on unique_ptr
⚫ Using smart pointers like unique_ptr is
unquestionably tricky
⚫ However, in combination with move
semantics and RAII (also tricky), memory and
resource management become non-issues in
most C++ programs
⚫ Since memory management is a nightmare in
C and C++98 (tricky doesn’t even begin to
cover it), learning these still makes
programming simpler
unique_ptr
⚫ To get a raw pointer to the unique_ptr’s target,
call get()
⚫ void f() {
unique_ptr<A> ap = make_unique<A>(1, 2, 3);
g(ap.get());
} // the A object is destroyed on leaving scope
⚫ To get a raw pointer to the unique_ptr’s target
and stop managing the object, call release()
⚫ void f() {
unique_ptr<A> ap = make_unique<A>(1, 2, 3);
g(ap.release());
} // the A object is not destroyed on leaving scope
Moving objects
⚫ Suppose I have two overloaded functions
void f(B const &) { /* … */ } // #1
void f(B &&) { /* … */ } // #2
B g() { /* … */ } // Will use shortly
⚫ Which one is called when?
⚫ B b;
f(b); // #1 - can’t mess with b
f(g()); // #2 Moves from unnamed temporary
f(move(b)); // #2 – Said we don’t need b anymore
Move and unique_ptr
⚫ unique_ptrs can’t be copied because then there would
be two owners for the same object
⚫ unique_ptr<A> ap = make_unique<A>();
unique_ptr<B> bp = ap; // Ill-formed. Not unique ownership
⚫ What if we want to transfer ownership from one
unique_ptr to another?
⚫ Unique_ptr can move ownership
⚫ unique_ptr<A> f();
unique_ptr<A> ap = f(); // OK. Can move from temporary
unique_ptr<B> bp = move(ap); // Moves from ap
⚫ See our btree and animal_game code for practical
examples of this
Why did our btree copy
constructor look so weird
⚫ btree &operator=(btree &&other) noexcept {
swap(root, other.root);
return *this;
}
⚫ Well actually,
btree &operator=(btree &&other) = default;
would have been ok
⚫ But for a more complicated class, the full version
would be better
⚫ To understand the reason, we have to
understand exceptions better
What if release() threw an
exception

⚫ This would make even last week’s version


leak memory
⚫ g(arg1.release(), arg2.release());
⚫ If the first release() worked but the second threw
an excpetion, g would never be called to take over
ownership of the released pointer
⚫ If anything could throw an exception at any time, it is
pretty much impossible to write correct code
The wrong approach:
Exception specifications
⚫ C++98 introduced exception specifications that declared what
exceptions might be thrown by a function
⚫ These looked superficially like Java exception specifiers
⚫ void f(int i) throws(std::runtime_exception,
std::invalid_argument);
⚫ Unfortunately, exception specifications are so broken that they
were worse than useless and were deprecated in C++11 and will
be removed from the language in C++17
⚫ See http://www.gotw.ca/publications/mill22.htm and
http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2015/p0003r0.html for details
⚫ Unfortunately, that still leaves us with the problem of how to write
safe code if we have no protection against an exception
occurring at any time
noexcept
⚫ C++11 added the noexcept keyword that declares that a
function won’t throw exceptions (without catching them
before leaving the function)
⚫ If they do, std::terminate will be called immediately
⚫ unique_ptr<T>’s release() method is noexcept
⚫ T *release() noexcept;
⚫ (Actual signature is a little more complicated but still has the
noexcept)
⚫ This is really shorthand for noexcept(true).
noexcept(false) would mean it may throw exceptions
⚫ Now that we know release() can’t throw an exception,
we see it is safe to transfer ownership to g via
⚫ g(arg1.release(), arg2.release());
Some more exception best
practices
⚫ Always inherit your exceptions from std::exception
⚫ Never throw an exception from a destructor
⚫ Since RAII calls destructors while processing an exception, you
could have two exceptions unwinding at once, which will cause
your program to terminate abruptly
⚫ Throw by value
⚫ throw(std::runtime_exception("something bad");
⚫ Avoids leaking or premature destruction
⚫ Catch by reference
⚫ try {...} catch(std::exception &e) { ... }
⚫ Handles polymorphism without slicing
⚫ Minimizes the calling of object destructors and constructors,
which might allocate memory, which might have been what
caused the exception in the first place
Back to btree’s move behavior
⚫ In general, it is good practice to try to make move constructors
and move assignment noexcept
⚫ Suppose I am pushing back objects on a vector
⚫ Occasionally, the vector needs to allocate a bigger region of
contiguous memory once it outgrows the old one
⚫ Will it move the old objects to the new region or copy them?
⚫ From a performance viewpoint, it would be nice to move
⚫ But if move throws an exception midway through, you’ve
corrupted both the old and new location, so vector will copy the
elements unless the move constructor is noexcept
⚫ Similar things occur with the move assignment operator
⚫ So now, let’s look at Victor Dyachenko’s comment in
https://stackoverflow.com/questions/6687388/why-do-some-
people-use-swap-for-move-assignments
How do I make a type
movable?
⚫ First, you don't need to make a class movable. It is only a performance optimization
because C++ will just copy if there are no move operations
⚫ If moving is more efficient, you should create move constructors and move assignment
operators, just like standard library containers do:
⚫ template<class T> class vector {
// ...
vector(vector<T> const &); // copy constructor
vector(vector<T> &&); // move constructor
vector& operator=(const vector<T>&); // copy assignment
vector& operator=(vector<T>&&); // move assignment };
⚫ Sometimes, the compiler will automatically generate move constructors and move
assignment operators that just move all the members
⚫ Basically if you don't define a copy constructor/assignment operator or a destructor
⚫ If you want to force the compiler to generate the default move constructor even though it
wouldn't normally, you can force that with default
⚫ struct S {
S(S const &); // OK, but stops move constructor generation
S(S &&) = default; // Gets it back
/* ... */
};
Rule of five?
⚫ There is a lot of discussion about whether the rule of 3 should be
extended to a “rule of 5,”
⚫ If you define any of
⚫ The destructor
⚫ The copy constructor
⚫ Copy assignment operator
⚫ Move constructor
⚫ Move assignment operator
⚫ You should consider all of them (only worry about move if it is different
than copying)
⚫ C++11 deprecated some features to better mesh with the rule of
5
⚫ A proposal (with history) to remove the deprecated features was rejected
for C++14. Even though it was rejected it makes interesting and
illuminating reading for aspiring language lawyers
⚫ http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3578.pdf
⚫ Warning: This paper is hard-core. Only recommended if you have substantial
C++ experience
shared_ptr
⚫ What if there are several owners of an object
⚫ For example, multiple threads are using the
object and you don’t know which will finish last
⚫ Or an object is stored in a DAG (directed acyclic
graph) and it should not be deleted until all its
inbound edges are removed
⚫ A unique_ptr won’t do the trick because we
don’t want the object to be destroyed until all its
owners are gone
shared_ptr
⚫ shared_ptr<T> is a reference counted pointer that
waits until all owners go away
void g(shared_ptr<A>, shared_ptr<A>)
void f()
{
auto arg1 = make_shared<A>();
auto arg2 = make_shared<A>();
g(arg1, arg2);
// can still use arg1 and arg2
}
Best practice
⚫ Always make sure an object is owned by a
smart pointer
⚫ Never call new or delete
⚫ That’s so 1998
THREADS
Computers are not getting
faster
⚫ Perhaps the biggest secret in computer
progress is that computer cores have not
gotten any faster in 10 years
⚫ 2005’s Pentium 4 HT 571 ran at 3.8GHz, which is
better than many high-end CPUs today
⚫ The problem with increasing clock speeds is heat
⚫ A high end CPU dissipates over 100 watts in about 1
cubic centimeter
⚫ A light bulb dissipates 100 watts in about 75 cubic
centimeters
Computers are faster
⚫ Even though cores have not gotten faster, the
continued progression of Moore’s law means
that computers today have many cores to run
computations in parallel
⚫ Even cell phones can have 4 cores
⚫ 12 to 24 cores are not unusual on high-end
workstations and servers
⚫ 24 to 48 if you count hyperthreading
What does this mean to me?
⚫ If you don’t want your code to run like it’s
2005, you need your code to run in parallel
across multiple cores
⚫ In other words, you need threads!
Hello, threads
#include <iostream>
#include <thread>

void hello_threads() {
std::cout<<"Hello Concurrent World\n";
}

int
main(){
// Print in a different thread
std::thread t(hello_threads);
t.join(); // Wait for that thread to complete
}
What happened?
⚫ Constructing an object of type std::thread
immediately launches a new thread, running the
function given as a constructor argument (in this
case, hello_threads).
⚫ We’ll talk about passing arguments to the thread
function in a bit.
⚫ Joining on the thread, waits until the thread
completes
⚫ Be sure to join all of your threads before ending the
program
⚫ Exception: Later we will discuss detached threads,
which don’t need to be joined
Locks
⚫ The simplest way to protect shared data is
with a std::mutex.
⚫ How can we make sure we release the mutex
when we are done no matter what?
⚫ RAII!
⚫ C++11 includes a handy RAII class
std::lock_guard for just this purpose.
⚫ C++17 has replaced it with
std::scoped_lock
Locks
#include <list>
#include <mutex>
#include <algorithm>

std::list<int> some_list; // A data structure accessed by multiple threads


std::mutex some_mutex; // This lock will prevent concurrent access to the shared data structure

void
add_to_list(int new_value)
{
std::lock_guard<std::mutex> guard(some_mutex); // Since I am going to access the shared data struct, acquire the lock
some_list.push_back(new_value); // Now it is safe to use some_list. RAII automatically releases lock at end of function
}

bool
list_contains(int value_to_find)
{
std::lock_guard<std::mutex> guard(some_mutex); // Must get lock every time I access some_list
return
std::find
(some_list.begin(),some_list.end(),value_to_find)
!= some_list.end();
}
Locks: C++11
#include <list>
#include <mutex>
#include <algorithm>

std::list<int> some_list; // A data structure accessed by multiple threads


std::mutex some_mutex; // This lock will prevent concurrent access to the shared data structure

void
add_to_list(int new_value)
{
std::lock_guard<std::mutex> guard(some_mutex); // Since I am going to access the shared data struct, acquire the lock
some_list.push_back(new_value); // Now it is safe to use some_list. RAII automatically releases lock at end of function
}

bool
list_contains(int value_to_find)
{
std::lock_guard<std::mutex> guard(some_mutex); // Must get lock every time I access some_list
return
std::find
(some_list.begin(),some_list.end(),value_to_find)
!= some_list.end();
}
Locks: C++17
Class Template Argument Ded
#include <list>
#include <mutex>
#include <algorithm>

std::list<int> some_list; // A data structure accessed by multiple threads


std::mutex some_mutex; // This lock will prevent concurrent access to the shared data structure

void
add_to_list(int new_value)
{
std::scoped_lock guard(some_mutex); // Template arguments are deduced as of C++17
some_list.push_back(new_value); // Now it is safe to use some_list. RAII automatically releases lock at end of function
}

bool
list_contains(int value_to_find)
{
std::scoped_lock guard(some_mutex); // Must get lock every time I access some_list
return
std::find
(some_list.begin(),some_list.end(),value_to_find)
!= some_list.end();
}
shared_mutex
⚫ A shared_mutex can be acquired either in shared
ownership mode or unique ownership mode
⚫ shared_mutex sm;
// RAII for acquiring shared ownership
shared_lock<shared_mutex> sl(sm);
// RAII for acquiring sole ownership
unique_lock ul(sm);
⚫ Motivating use case: reader-writer lock
⚫ Multiple threads can read from a data structure at the same
time as long as no thread is modifying it
⚫ If a thread is modifying the data structure, it should acquire
sole ownership of the object so no thread sees the data
structure in a partially modified state
Locks: C++17
Shared mutex
#include <list>
#include <mutex>
#include <algorithm>

std::list<int> some_list; // A data structure accessed by multiple threads


std::mutex some_mutex; // This lock will prevent concurrent access to the shared data structure

void
add_to_list(int new_value)
{
std::unique_lock guard(some_mutex); // Unique writer access
some_list.push_back(new_value); // Now it is safe to use some_list. RAII automatically releases lock at end of function
}

bool
list_contains(int value_to_find)
{
std::shared_lock guard(some_mutex); // Shared reader access
return
std::find
(some_list.begin(),some_list.end(),value_to_find)
!= some_list.end();
}
Not so basic: Thread
arguments
⚫ You can add arguments to be passed to the
new thread when you construct the
std::thread object as in the next slide
⚫ But there are some surprising and important
gotchas that make passing arguments to
thread function different from passing
arguments to ordinary functions, so read on
Passing arguments to a thread
#include <iostream>
#include <thread>
#include <string>
#include <vector>
#include <mutex>
using namespace std;
mutex io_mutex;

void hello(string name) {


lock_guard<mutex> guard(io_mutex);
cout <<"Hello, " << name << endl;
}

int
main(){ // No parens after thread function name:
vector<string> names = { "John", "Paul"};
vector<thread> threads;
for(auto name : names) {
threads.push_back(thread(hello, name));
}
for(auto it = threads.begin(), it != threads.end(); it++) {
it->join();
}
}
Deceptively simple
⚫ A different notation is used from arbitrary
function calls, but otherwise fairly
straightforward looking:
⚫ void f(int i);
f(7); // Ordinary call
thread(f, 7);// f used as a thread function
Gotcha: Passing pointers and
references
⚫ Be very careful about passing pointers or references to
local variables into thread functions unless you are sure the
local variables won’t go away during thread execution
⚫ Example (based on Boehm)
void f() {
int i;
thread t(h, &i);
bar(); // What if bar throws an exception?
t.join(); // This join is skipped
} // h keeps running with a pointer
// to a variable that no longer exists
// Undefined (but certainly bad) behavior
⚫ Use try/catch or better yet, a RAII class that joins like the
thread_guard class in Concurrency In Action book
Gotcha: Signatures of thread
functions silently “change”
⚫ What does the following print?
void f(int &i) { i = 5; }
int main() {
int i = 2;
std::thread t(f, i);
t.join();
cout << i << endl;
return 0;
}
A compile error
⚫ Of course, 5 was intended
⚫ Unfortunately, thread arguments are
not interpreted exactly the same way
as just calling the thread function with
the same arguments
⚫ This means that even an application
programmer using threads needs to
understand something subtle about
templates
What went wrong, continued
⚫ Imagine std::thread’s constructor looks like the following
struct thread { ...
// 0 arg thrfunc constructor
template<typename func>
thread(func f);
// 1 arg thrfunc constructor
template<typename func, typename arg>
thread(func f, arg a);
...
};
...
// Deduces thread::thread<void(*)(int), int)
std::thread t(f, i);
...
⚫ In fact, thread constructors use “variadic argument lists,”
which we haven’t (yet) covered
IOW, Templates don’t know f
takes its argument by reference
⚫ To do this, we will use the “ref” wrapper in
<functional>
⚫ void f(int &i) { i = 5; }
int main() {
int i = 2;
std::thread t(f, std::ref(i));
t.join();
cout << i << endl;
return 0;
}
Does thread’s constructor
really look like that?
⚫ No, C++11 has “variadic templates” that can take
any number of arguments, so we don’t need to
separate 0-arg, 1-arg, etc. constructors:
struct thread {
template
<typename F, typename... argtypes>
thread(F f, argtypes... a);
...};
⚫ We’ll learn about these later
Review the code
⚫ Most of the content of this lecture is
illustrated in the code on Canvas, especially
the different implementations of distributed
counters. Carefully review those code
samples in conjunction with the lecture notes
below
Thread References
⚫ C++ Concurrency in Action Book
⚫ http://www.manning.com/williams/
⚫ If you buy from Manning rather than Amazon, you can download a
preprint right now without waiting for the official publication
⚫ The author Anthony Williams is one of the lead architects of
C++11 threads, the maintainer of Boost::Thread, and the author
of just::thread
⚫ Anthony’s Multithreading in C++0x blog
⚫ http://www.justsoftwaresolutions.co.uk/threading/multithreading-
in-c++0x-part-1-starting-threads.html
⚫ Free with concise coverage of all the main constructs
⚫ The standard, of course
⚫ Also look at the papers on the WG21 site
Lock ordering
⚫ If you want to avoid deadlocks, you want to
acquire locks in the same order!
⚫ Suppose thread 1 acquires lock A and then lock B
⚫ Suppose thread 2 acquires lock B and then lock A
⚫ There is a window where we could deadlock with
thread 1 owning lock A and waiting for lock B while
thread 2 owns lock B and is waiting for lock A forever
⚫ The usual best practice is to document an order
on your locks and always acquire them
consistent with that order
⚫ See
http://www.ddj.com/hpc-high-performance-computing/204801163
Sometimes it is hard to fix a
lock order
⚫ From http://www.justsoftwaresolutions.co.uk/threading/multithreading-
in-c++0x-part-7-locking-multiple-mutexes.html
⚫ Consider
class account {
mutex m;
currency_value balance;
public:
friend void transfer(account& from,account& to,
currency_value amount) {
lock_guard<mutex> lock_from(from.m);
lock_guard<mutex> lock_to(to.m);
from.balance -= amount;
to.balance += amount;
}
};
⚫ If one thread transfers from account A to account B at the same time as
another thread is transferring from account B to account A: Deadlock!
C++ provides a solution
⚫ std::lock(…) allows you to acquire multiple locks “at the same time”
and guarantees there will be no deadlock,
⚫ Like magic! (Actually, it will try releasing locks and then acquiring in different
orders until no deadlock occurs)
⚫ class account {
mutex m;
currency_value balance;
public:
friend void transfer(account& from,account& to,
currency_value amount) {
scoped_lock lck(from.m, to.m);
from.balance -= amount;
to.balance += amount;
}
};
⚫ After acquiring the lock, “adopt” it into the lock_guard to manage the
lifetime of the lock.
C++ provides a solution
⚫ As of C++17, it is a lot easier
⚫ class account {
mutex m;
currency_value balance;
public:
friend void transfer(account& from,account&
to,
currency_value amount)
{
scoped_lock lck(from.m, to.m);
from.balance -= amount;
to.balance += amount;
}
};
shared_ptr and lock ordering
⚫ shared_ptrs silently delete an object when the reference
count goes to zero
⚫ The programmer probably doesn’t know when that will be (or
wouldn’t have needed a shared_ptr to begin with!)
⚫ Recall that locks need to be acquired in a specified order
⚫ The problem! If an object that is managed by a shared_ptr’s
destructor acquires locks, we won’t know which locks are held
and won’t know if we are respecting the lock ordering constraints
⚫ Best practice
⚫ Do not acquire (non-leaf) locks in the destructor of any class that
may be managed by a shared_ptr
⚫ If necessary, do the portion of destruction that requires
synchronization in a separate thread (like Java finalizers)
HW 5-1: Extra credit
⚫ Email me a question to ask the standards
committee during next week’s C++ standards
meeting
⚫ I will choose one or two for personal answers
by the appropriate experts
HW 5-2
⚫ The purpose of this problem is to ensure that you can write
basic multithreaded code on your system. Use the C++
threads
⚫ Some compilers require special flags to build multithreaded code, so
consult your compiler documentation if necessary. For example,
g++ foo.cpp –pthread
⚫ Write a program that creates 3 threads that each count up to
100 and output lines like:
Thread 3 has been called 4 times
⚫ To get a thread number, use
std::this_thread.get_id()
⚫ Make sure you use synchronization to keep different threads
from garbling lines like the above.
⚫ There are correct solution with locks and without locks. Full credit for
either. Extra credit for both
⚫ Submit the output from your program. What does it tell you
about how threads are actually scheduled on your system?
HW 5-3
⚫ Write a thread-safe stack using locks
⚫ The only required operations are push and pop
⚫ E.g., multiple threads can concurrently do things
like the following without corrupting the stack
⚫ mpcs50144::stack<int> s;
s.push(7);
s.push(5);
cout << s.pop();
⚫ For extra credit, add additional useful
functionality (e.g., initializer list constructor, etc.)

You might also like