lecture-04
lecture-04
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
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?