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

lecture-04

The document discusses advanced C++ concepts including non-static data member initializers, delegate constructors, and memory management techniques. It emphasizes the importance of RAII (Resource Acquisition Is Initialization) for managing resources and preventing memory leaks, as well as the use of smart pointers like unique_ptr and shared_ptr. Additionally, it highlights the complexities of C++ object lifecycles and the significance of understanding constructors and destructors.

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)
5 views

lecture-04

The document discusses advanced C++ concepts including non-static data member initializers, delegate constructors, and memory management techniques. It emphasizes the importance of RAII (Resource Acquisition Is Initialization) for managing resources and preventing memory leaks, as well as the use of smart pointers like unique_ptr and shared_ptr. Additionally, it highlights the complexities of C++ object lifecycles and the significance of understanding constructors and destructors.

Uploaded by

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

Advanced C++

February 4, 2019

Mike Spertus
[email protected]
Non-static Data Member
Initializers
⚫ We can simplify constructors by giving initializers to members that usually have the same value
⚫ Bad:
struct A {
A(): a(7), b(5), hash_algorithm("MD5"), s("Constructor run") {}
A(int a_val) : a(a_val), b(5), hash_algorithm("MD5"), s("Constructor
run") {}
A(D d) : a(f(d)), b(g(d)), hash_algorithm("MD5"), s("Constructor run")
{}
int a;
int b;
// Cryptographic hash to be applied to all A instances
HashingFunction hash_algorithm;
std::string s; // String indicating state in object lifecycle
};
⚫ Good:
struct A {
A(){}
A(int a_val) : a(a_val), {}
A(D d) : a(f(d)), b(g(d)) {}
int a{7}
int b{5};
// Cryptographic hash to be applied to all A instances
HashingFunction hash_algorithm{"MD5"};
std::string s{"Constructor run"}; // String indicating state in object lifecycle
};
Delegate constructors
⚫ struct B {
B(int a, X x, C c, double d);
B(int a) : B(a, X(a), C{}, 2.4) {}
};
Default construction of
primitive types
⚫ Primitive types are not initialized when default
constructed unless an empty initializer is
explicitly passed
int i; // i contains garbage.
// Efficient but unsafe
int j{}; // Initialized to 0
struct A {
int j;
};
A a; // a.j could be anything
// How would you fix?
// More on this in a few slides
Copy constructors and copy
assignment
⚫ Classes can have constructors that show how to make copies.
⚫ Signature is T(T const &)
⚫ A default copy constructor is almost always generated
⚫ Calls the copy constructors of all the base classes and members in
the order we will discuss
⚫ T(T const &) = delete;
⚫ Classes can also have copy assignment operators inside the
class with signature
⚫ operator=(T const &);
⚫ T t; // default constructor
T t2 = t; // copy constructor (creates new object)
t2 = t; // copy assignment (modifies existing object)
Rule of three
⚫ A class should define all or none of the
following
⚫ Destructor
⚫ Copy constructor
⚫ Assignment operator
⚫ https://en.cppreference.com/w/cpp/language/rule_of_three
⚫ Modern “rule of 5” version also includes move constructor and move
assignment
Comparison with other
languages you may know
Free functions Methods
C X
Java X
C++ X X

Code uses Static type Dynamic type


C X
Java X
C++ X X (virtual method)

Parameter passing By value By reference By move


C X
Java X
C++ f(X) F(X&) F(X&&)
Memory management
⚫ In our animal game program, we didn’t worry
about freeing memory because the tree only
got bigger and all memory is released when
the program ends
⚫ But usually a program has to delete memory
when it is done with it
⚫ For example, suppose the animal game had
a command to reset the list of known animals
⚫ We would like to release the memory associated
with the animals we are forgetting
Manual memory management
⚫ Just call delete when you are done with an object
⚫ Like C
⚫ Let’s look at animal_manual_delete.cpp
⚫ Very complicated and error-prone
⚫ Like C
⚫ We can organize the logic a little bit by moving cleanup into
constructors as in animal_destructor.cpp
⚫ This is better as the point of destructors is to do cleanup
⚫ But all the deletion logic is still manually coded
⚫ Furthermore, manual memory management is even harder
in C++ than C
⚫ Why?
⚫ Exceptions!
What’s wrong with new and
delete?
⚫ Exceptions
⚫ Exceptions make control flow unpredictable, so it
is very difficult to know when to delete it
⚫ Threads
⚫ Which thread is the last to use an object likely
won’t be known until runtime
Exceptions
⚫ Can throw an exception (any type) with
throw
⚫ But usually they should inherit from
std::exception
⚫ You can catch an exception within a try block
with catch.
⚫ Exceptions make memory management very
difficult because program flow is hard to
predict
Tear down
⚫ Objects of automatic storage duration are
destroyed as you leave the try block
⚫ Exceptions filter upward to calling functions
destroying objects of automatic storage
duration as each block scope is left
⚫ This explains why there is no “finally” in C++
⚫ RAII
Example
#include <iostream>
#include <stdexcept>
using namespace std;
int main () {
try {
throw std::runtime_error("oops");
} catch (std::exception &e) {
cout << "Exception " << e << endl;
}
return 0;
}
Memory leak
#include <iostream>
#include <stdexcept>
using namespace std;
int f() {
try {
A *ap = new A;
if(somethingWentWrong(ap))
throw runtime_error("Uh oh");
delete ap; // May not be called
} catch (exception &e) {
cout << "Exception " << e.what() << endl;
}
return 0;
}
int main() { for(int = 0; i < 1<<20; i++) f(); }b
Flow of control is hard to
follow
class A;
A *ap1 = new A();
A *ap2 = new A();
delete ap2;
delete ap1;
⚫ May leak!
⚫ What if the second constructor or new expression
throws an exception
We need a better solution
⚫ To solve this, we will need to understand the
C++ object lifecycle
⚫ The payoff will be that C++ programmers
generally spend very little time on memory
management
THE C++ OBJECT LIFECYCLE
Object duration
⚫ Automatic storage duration
⚫ Local variables
⚫ Lifetime is the same as the lifetime of the function/method
⚫ Static storage duration
⚫ Global and static variables
⚫ Lifetime is the lifetime of the program
⚫ Dynamic storage duration
⚫ Lifetime is explicit
⚫ Created with “new” destroyed with “delete”
⚫ In all cases, the constructor is called when the
object is created and the destructor is called when
the object isdestroyed
Static storage duration
⚫ What orders are the constructors of static
storage duration objects called?
⚫ In each source file, they are constructed in
order
⚫ Static/global variables in different source files
are constructed in undefined orders
⚫ This creates interesting issues
Ways to create objects with
static storage duration
int i; // Created at program start
struct A {
static int j; // Created at program start
void f() {
static int k{}; // Created first time f() is called.
// Does not lose its value between calls
/* ... */
}
};

A a; // Created at program start

static A a2; // Created at program start


// Not visible outside of current translation unit

void g() {
static A a3; // Created first time g() is called
}
Tear down
⚫ Automatic and static duration objects are destroyed at the end of their
scope in the reverse order they were created:
struct A {
A() { cout << "A() "; }
~A() { cout << "~A()"; }
};
struct B {
B() { cout << "B()"; }
~B() { cout << "~B()"; }
};
void f() {
A a;
B b;
}
int main() { f(); return 0; }
// Prints A() B() ~B() ~A()
Function-static lifetimes
⚫ A static variable in a function is initialized the
first time the function runs
⚫ Even if the function is called from multiple
threads, the language is responsible for making
sure it gets initialized exactly once.
⚫ If the function is never called, the object is never
initialized
⚫ As usual, static duration objects are destroyed in
the reverse order in which they are created
Pointers
⚫ Pointers to a type contain the address of an object of
the given type.
A *ap = new A;
⚫ Dereference with *
A a = *ap;
⚫ -> is an abbreviation for (*_).
ap->foo(); // Same as (*ap).foo()
⚫ If a pointer is not pointing to any object, you should
make sure it is nullptr (If not yet in C++11, use 0)
ap = nullptr; // don’t point at anything
if(ap) { ap->foo(); }
References
⚫ Like pointers but implicitly dereferenced
⚫ Allow one object to be shared among different variables
⚫ Can only be set on creation and never changed
⚫ Reference members must be initialized in initializer lists
struct A {
A(int &i) : j{i} {} // OK
// A(int &i) { j = i; } // Ill-formed. Too late!
int &j;
};
⚫ Cannot be null
⚫ int i{3};
int &j = i;
i = 5; // j is also set to 5
j = 2; // i also becomes 2
Understanding function and
method arguments
⚫ Function and method signatures are very
complicated
⚫ Arguments can be passed by value or reference
⚫ Overloading can make it tricky to know which
function will be called
⚫ Template instantiation rules construct signatures
on the fly
Passing arguments by value or
reference
⚫ Pass by value
void v(int i) { i = 7; }
int x = 3;
v(x); // v gets its own copy of i
cout << x; // Prints 3
v(3); // OK. Doesn’t try to change the value
of 3
⚫ Pass by reference
void r(int &i) { i = 7; }
int x = 3;
r(x); // r "binds" i to the existing x
cout << x; // Prints 7
r(3); // Error! Can’t change 3
void c(int const &i); // Won’t modify i
c(3); // OK. Doesn’t modify 3
Dynamic storage duration
⚫ Traditionally created by expression of the form “new
typename” or
“new typename(constructor args)”
⚫ Returns a properly-typed pointer to the memory
⚫ int *ip = new int; // *ip is uninitialized
⚫ int *ip2 = new int{}; // *ip2 is zero
⚫ A *ap = new A(7, x);
⚫ A *arr = new A[7]; // Creates an array
⚫ Destroyed by calling delete
⚫ delete ip;
⚫ delete ap;
⚫ delete [] arr; // Deletes an array
C++ Memory Management
⚫ The first rules that every C++ programmer
learns
⚫ You create objects (with dynamic lifetime) by
calling new
⚫ When you are done with the object, you must
release it by calling delete to avoid a memory
leak
⚫ Our first goal tonight will be to discard these
rules
⚫ And then things will get interesting
Can we ensure the delete is
never skipped?
⚫ A Java programmer would wonder why we didn’t
just use finally
⚫ Actually, they would wonder why we didn’t use GC,
but more on that later
⚫ int f() {
try {
A *ap = new A;
g();
} catch (...) { cerr << "Exception\n“;}
} finally { delete ap; } // Whoops! Not C++
return 0;
}
⚫ One big problem
⚫ It’s not C++!
RAII
⚫ Why doesn’t C++ have finally?
⚫ Because it has something better
⚫ Destructors of local variables are always
called however you leave scope
⚫ Using this to manage resources is called RAII
⚫ Resource Acquisition Is Initialization
RAII
⚫ “Resource Acquisition is Initialization”
⚫ One of the most important idioms in C++
⚫ Another important use of constructors and
destructors
⚫ The basic idea is “how do we make sure we
won’t leak any objects or resources when we
leave a scope?”
unique_ptr
⚫ unique_ptr destructor deletes the object
pointed to
⚫ The memory leaked fixed:
⚫ int f() {
try {
unique_ptr<A> ap{new A};
g(ap);
} catch (...) {
cout << "Exception " << endl;
}
return 0;
}
Memory leak
#include <iostream>
#include <stdexcept>
using namespace std;
int f() {
try {
A *ap = new A;
if(somethingWentWrong())
throw runtime_error("Uh oh");
delete ap; // May not be called
} catch (exception &e) {
cout << "Exception " << e.what() << endl;
}
return 0;
}
int main() { for(int = 0; i < 1<<20; i++) f(); }b
Memory leak fixed
#include <iostream>
using namespace std;
int f() {
try {
unique_ptr<A> ap{new A}; // OK, but passe
if(/* error occurs */)
throw 20;
} catch (int e) {
cout << "Exception " << endl;
}
return 0;
}
int main() { for(int = 0; i < 1<<20; i++) f(); }b
Variants
⚫ unique_ptr<A[]> owns an array
⚫ Destructor uses delete []
⚫ Replaces C++98’s now deprecated auto_ptr
⚫ shared_ptr<A> is a reference counted
pointer to A
⚫ The object is deleted when its last shared_ptr
goes away
Best practice
⚫ Effective C++ item 17
⚫ Store newed objects in smart pointers in standalone
statements
⚫ Gets rid of delete
⚫ An interesting proposal to make this easier
⚫ Walter Brown, N3418: A Proposal for the World’s
Dumbest Smart Pointer, v3
⚫ observer_ptr acts like a raw pointer, but reminds
you that it doesn’t contribute to object ownership
⚫ Included in Library Fundamentals TS on the way to a
future C++ standard
Getting rid of new
⚫ Why get rid of new?
⚫ Even garbage collected languages like Java have it
⚫ The problem is that new returns an owning raw
pointer, in violation of the above best practice,
which can get you into trouble:
void f()
{
// g(A *, A *) is responsible for deleting
g(new A(), new A());
}
⚫ What if the second time A’s constructor is called, an
exception is thrown?
⚫ The first one will be leaked
make_shared and
make_unique
⚫ make_shared<T> and make_unique<T> create an object
and return an owning pointer
⚫ The following two lines act the same
⚫ auto ap = make_shared<T>(4, 7);
⚫ shared_ptr<T> ap = new T(4, 7);
⚫ make_unique wasn’t added until C++14
⚫ Oops
⚫ Now we can fix our previous example
void f()
{
auto a1 = make_unique<A>(), a2 = make_unique<A>();
// g(A *, A *) is responsible for deleting
g(a1.release(), a2.release());
}
⚫ Effective Modern C++ Item 21
⚫ Prefer std::make_unique and std::make_shared to
direct use of new
Let’s improve it a little more
⚫ If we can modify g(), we should really change it to take
unique_ptr<T> arguments because otherwise, we would have an
owning raw pointer
⚫ Remember, g() takes ownership, so it shouldn’t use owning raw pointers
⚫ g(unique_ptr<T>, unique_ptr<T>);
⚫ Now we can call
g(make_unique<T>(), make_unique<T>());
⚫ Interestingly, the following doesn’t work because ownership will no
longer be unique
⚫ auto p1 = g(make_unique<T>();
auto p2 = g(make_unique<T>();
g(p1, p2); // Illegal! unique_ptr not copyable
⚫ To fix, we need to move from p1 and p2
⚫ g(move(p1), move(p2)); // OK. unique_ptr is movable
⚫ We'll learn about moving in detail next week
Getting raw pointers from
smart pointers
⚫ Sometimes when you have a smart pointer, you need an actual
pointer
⚫ For example, a function might need the address of an object
but not participate in managing the object’s lifetime
⚫ If you are not an owner of the object, there is no reason to use a
smart pointer
⚫ f(A *); // Doesn’t decide when to delete its argument
auto a = make_unique<A>{};
f(a.get()); // a.get() gives you the raw A *
⚫ Sometimes you want to extend the lifetime of an object beyond
the lifetime of the unique_ptr.
⚫ g(A *); // g will delete the argument when done
auto a = make_unique<A>{};
g(a.release()); // a no longer owns the object
Putting it all together
⚫ In animal_unique_ptr.cpp, we put together
the above best practices by…
⚫ …replacing all the owning raw pointers with
unique_ptr
⚫ …replacing all the calls to new with make_unique
⚫ …using move to transfer ownership
⚫ Now, memory management works perfectly!
⚫ No calls to delete!
⚫ No need to write cleanup code in destructors!
HW4.1
⚫ Use static duration objects to write a program
that prints “Hello, world!” with the following
main function:
int
main()
{
return 0;
}
⚫ Extra credit—Give a solution that depends on
constructor ordering. The more intricate the
dependence, the greater the extra credit.
HW 4-2
⚫ Modify the binary tree class in Canvas taken from
http://www.cprogramming.com/tutorial/lesson18.html
⚫ Follow the best practices we have discussed in
class for memory management
⚫ For full credit, think about whether it needs a copy
constructor and write one if appropriate
⚫ Hint: slide 5 and 6
⚫ For extra credit, learn about move constructors and
write one if appropriate
⚫ Do you think your modifications are an
improvement? Why or why not?

You might also like