Intermediate C++ 1/83
Sections I. Memory Management Basics II. The C++ Standard Library III. Casting IV. Resource Management: RAII 2/83
I. Memory Management Basics 1. Checking for memory leaks 2. Pass by value 3. Pass by reference 4. Const 3/83
Preliminary notes - For the purpose of brevity, we shall refer to objects and primitive types collectively as just objects - If you see information in a box like this one, it means this information is helpful to understand, but not necessary for the course 4/83
1. Checking for memory leaks - Remember new and delete? - Failing to delete can cause memory leaks! - Valgrind: Tool used to check code for memory leaks - Run from Qt s Analyze drop-down menu - Tells you where leaked memory was allocated - Check your code for leaks! - Leaks will slow down your program and eventually crash it - The TAs will deduct heavily for leaks 5/83
2. Pass by value - Functions often take arguments - They have a number of ways of receiving those arguments - These differences are important - Let s look at the first: Pass by value 6/83
// main.cpp void increaseandprintunicorn(unicorn uni) { // 3. Take a Unicorn as an argument uni.increaseval(); // 4. Call some member function. uni.printval(); // 5. Output: 6 } int main(int argc, char* argv[]) { Unicorn u(5); // 1. Create a Unicorn with initial value of 5. increaseandprintunicorn(u); // 2. Pass the Unicorn by value. This invokes the Unicorn s copy constructor u.printval(); // 6. Output: 5 } // Syntax note: Use period (.) to access class members of objects. Use arrow (->) when dealing with a pointer to an object. // Bonus note: The dereference operator (->) can be overridden (operator overloading is a thing in C++). The structure reference // operator (.) cannot be overridden. 7/83
2. Pass by value cont. - Passing by value creates a copy of an object to give to the function - If you do stuff to a copy of an object - You re not changing the original one! - Remember this! - But what can we notice about this example? - Hint: Something inefficient is happening... 8/83
// main.cpp void increaseandprintunicorn(unicorn uni) { // 3. This Unicorn is constructed as main s Unicorn is copied uni.increaseval(); uni.printval(); } // 5. Call Unicorn s destructor for uni. int main(int argc, char* argv[]) { Unicorn u(5); // 1. Create a Unicorn with initial value of 5. increaseandprintunicorn(u); // 2. Pass the Unicorn by value. This invokes the Unicorn s copy constructor u.printval(); } // Call Unicorn s destructor for u. 9/83
2. Pass by value cont. - A Unicorn object was constructed and destroyed twice - This is inefficient - Imagine passing an object through several layers of function calls - it just gets worse and worse! - Big objects will take even more time and memory - This is Not A Good Thing - What do? 10/83
3. Pass by reference - A reference is, well, a reference to an object - A reference is like another name for the object - It refers to the same object - It is not a pointer - References are denoted with the & symbol 1 - Passing by reference is A Good Thing 1 The technical name of which is the lvalue reference declarator 11 /83
// main.cpp void increaseandprintunicorn(unicorn &uni) { // 3. This function takes a reference to a Unicorn. No constructor called here // Unlike before, this is not a copy of main s Unicorn, it is main s Unicorn. uni.increaseval(); uni.printval(); // Output: 6 } // 4. No destructor called here int main(int argc, char* argv[]) { Unicorn u(5); // 1. Create a Unicorn with initial value of 5. increaseandprintunicorn( u ); // 2. Pass the Unicorn by reference. This does not invoke Unicorn s copy constructor u.printval(); // 5. Output: 6 } // Call Unicorn s destructor for u now that it is going out of scope 12/83
3. Pass by reference cont. - Two important changes - No copy and destruction of a new Unicorn - The Unicorn passed in can be modified - Question: What if we want to efficiently pass an object by reference, but don t want it to be modified? 13/83
4. Const - Keyword const is used to signal that something will not or cannot change - Pass by const reference is the solution to the last problem and is A Good Thing - Prefer pass by const reference unless - You are passing a very small argument (e.g. int) - You want to modify the argument 14/83
// main.cpp void changeunicorn(unicorn &uni) { // Non-const ref = can change the argument } uni.setval(7); void printunicorn(const Unicorn &uni) { } uni.printval(); // Const ref = can t change the argument // wum does not change int main(int argc, char* argv[]) { Unicorn u(5); // Initialize u to 5 changeunicorn(u); // Do something to u printunicorn(u); // Output: 7 } 15/83
// main.cpp void changeunicorn(const Unicorn &uni) { } uni.setval(7); // This can t be right. Change a const Unicorn? // Error: Unicorn uni is const, can t change the value // Either don t change u or remove the const qualifier void printunicorn(const Unicorn &uni) { } uni.printval(); // uni does not change. As expected. Nothing to see here. int main(int argc, char* argv[]) { Unicorn u(5); // Initialize u to 5 changeunicorn(u); // Do something to u printunicorn(u); // Output: None because this code doesn t compile } 16/83
4. Const cont. - Classes can use keyword const as well - Const member variables can t be changed - Const member functions are very useful - They signal that the object s state will not change when the function is called 17/83
// unicorn.h class Unicorn { public: Unicorn(int x); int getx() const; // A const member function declaration private: int m_x; } // unicorn.cpp #include unicorn.h Unicorn::Unicorn(int x) : {} m_x(x) int Unicorn::getX() const { } return m_x; // A const member function definition. Nothing about Unicorn changes 18/83
II. The C++ Standard Library 1. Overview 2. Templates 3. Case Study: std::map 4. Iterators 5. Other Container Classes 19/83
1. Overview - The C++ Standard Library is a collection of useful classes provided with C++ - They re not just standard issue tools - They re literally part of the C++ Standard 20/83
1. Overview cont. - The Standard Library is A Good Thing - Check to see if the Standard Library has what you need before you try to make it yourself - Theirs is better - Unless you re working in a very specific high-performance and/or nonstandard environment - Which you re not 21/83
1. Overview cont. - What s in the Standard Library? - std::string - words - std::vector - dynamic array - std::map - key/value map - std::cout, std::cin, std::cerr - standard i/o - <cmath> header includes many common math functions 22/83
2. Templates - C++ templates are used for generic programming - They re similar to Java Generics - In 123 you will never need to create templates so we will only go over how to use them in the context of the Standard Library - See the Advanced C++ slides for more 23/83
// main.cpp #include <vector> #include Unicorn.h // Include the vector header // A unicorn class int main(int argc, char *argv[]) { std::vector<unicorn> myunicorns; for (int i = 0; i < 10; i++) { Unicorn u(i); myunicorns.push_back(f); } for (size_t i = 0; i < 10; i++) { myunicorns[i].print(); } } // Make a vector of Unicorns. Note that we could put any type inside the angle // brackets and the vector would behave the same way // Some constructor for a Unicorn // Add the Unicorns to the vector // Iterate through the vector s contents // Call some function on each Unicorn. 24/83
// main.cpp #include <vector> #include Horn.h // Some Horn class instead int main(int argc, char *argv[]) { std::vector<horn> myhorns; // Unicorn or Horn for (int i = 0; i < 10; i++) { Horn h(i); myhorns.push_back(b); } } for (size_t i = 0; i < 10; i++) { } myhorns[i].bang(); 25/83
2. Templates cont. - In general, templates are used for making containers - data structures that don t care about what s inside them - There are more uses, but we won t go over them here - See the Advanced C++ Slides for cool things like template metaprograming! 26/83
3. Case Study: std::map - std::map is a homogeneous map between key objects and value objects - That is, all the keys are all of type K and the values are all of type V - K and V may be different types or the same type - Sort of like a hashmap in Java although std::map is actually a tree. std::unordered_map is actually a hashmap - We ll go over how to use it in C++ 27/83
// main.cpp #include <map> #include <string> // Include appropriate headers int main(int argc, char *argv[]) { std::map<std::string, int> mycolors; // Map from strings to ints mycolors[ Red ] = 200; mycolors[ Green ] = 150; mycolors[ Blue ] = 25.2f; // Store the key and value via the []= operator // Implicit conversion warning - converts float to int } std::cout << Red: << mycolors[ Red ] << std::endl; // Access values the same way you stored them std::cout << Green: << mycolors[ Green ] << std::endl; std::cout << Blue: << mycolors[ Blue ] << std::endl; // Output: // Red: 200 // Green: 150 // Blue: 25 28/83
3. Case Study: std::map cont. - So far so good - But...what about const? 29/83
3. Case Study: std::map cont. - What gives? - The bracket operator isn t const - It returns a non-const reference - You may look at and change the value - However, at() is const 1 - It returns a const reference - Read-only 1 Technically, a const-qualified overload of at() is provided. There is also a non-const-qualified at() function that works like the bracket operator. The cv-qualification (const-volatile qualification) of the object determines which is called. 30/83
int printmapval(const std::map<std::string, int> &mycolors, std::string key) { std::cout << key << : << mycolors[key] << std::endl; // Error: mycolors is const } int printmapval(const std::map<std::string, int> &mycolors, std::string key) { std::cout << key << : << mycolors.at(key) << std::endl; // This is fine } 31/83
3. Case Study: std::map cont. - std::map also comes with some other basic functions - empty() - whether the map is empty or not - clear() - clears all key-value pairs from the map - size() - number of elements stored in the map 32/83
4. Iterators - Iterators are special objects used by container classes to iterate over the container s contents 33/83
// main.cpp #include <map> // other headers omitted int main(int argc, char *argv[]) { std::map<std::string, int> mycolors; // Notice that this type matches the iterator below. They must match mycolors[ Red ] = 100; mycolors[ Green ] = 200; mycolors[ Blue ] = 400; std::cout << Contents of mycolors map << std::endl } // This is the type of your map. It matches the above definition exactly for (std::map<std::string, int>::iterator it = mycolors.begin(); it!= mycolors.end(); it++) { std::cout << it->first << : << it->second << std::endl; } 34/83
// main.cpp #include <map> // other headers omitted int main(int argc, char *argv[]) { std::map<std::string, int> mycolors; // Notice that this type matches the iterator below. They must match mycolors[ Red ] = 100; mycolors[ Green ] = 200; mycolors[ Blue ] = 400; std::cout << Contents of mycolors map << std::endl } // This is the type of your map. It matches the above definition exactly // Access the class s iterator type. It s part of the class, not the instance, so it s accessed via the scope operator for (std::map<std::string, int>::iterator it = mycolors.begin(); it!= mycolors.end(); it++) { std::cout << it->first << : << it->second << std::endl; } 35/83
// main.cpp #include <map> // other headers omitted int main(int argc, char *argv[]) { std::map<std::string, int> mycolors; // Notice that this type matches the iterator below. They must match mycolors[ Red ] = 100; mycolors[ Green ] = 200; mycolors[ Blue ] = 400; std::cout << Contents of mycolors map << std::endl } // This is the type of your map. It matches the above definition exactly // Access the class s iterator type. It s part of the class, not the instance, so it s accessed via the scope operator // std::map has functions that return markers for the beginning and end of your container. // The actual order of the elements is unimportant - the point is that you can loop through all of them for (std::map<std::string, int>::iterator it = mycolors.begin(); it!= mycolors.end(); it++) { std::cout << it->first << : << it->second << std::endl; } 36/83
// main.cpp #include <map> // other headers omitted int main(int argc, char *argv[]) { std::map<std::string, int> mycolors; // Notice that this type matches the iterator below. They must match mycolors[ Red ] = 100; mycolors[ Green ] = 200; mycolors[ Blue ] = 400; std::cout << Contents of mycolors map << std::endl } // This is the type of your map. It matches the above definition exactly // Access the class s iterator type. It s part of the class, not the instance, so it s accessed via the scope operator // std::map has functions that return markers for the beginning and end of your container. // The actual order of the elements is unimportant - the point is that you can loop through all of them for (std::map< std::string, int >::iterator it = mycolors.begin(); it!= mycolors.end(); it++) { // Iterator contents are accessed via simple members or operators. Here, first is the key, and second is the value. std::cout << it->first << : << it->second << std::endl; } 37/83
5. Other Container Classes - The Standard Library has a number of other containers: - std::stack - std::queue - std::set - std::unordered_map - std::unordered_set - Their syntax is similar to that of std::map s 38/83
/83
1. Overview - In C++, you can cast an object of one type into an object of another - Within obvious limitations and reason - There are 4 ways to cast an object, each with its own function 40/83
2. static_cast<type> - Should be your first choice of cast - Implicit conversions (e.g. unsigned int to int) - If your compiler complains about implicit conversions, you re likely missing a static_cast - Can cast up inheritance hierarchies (derived to base) - Unnecessary in this case - Performs no runtime checks - So you need to know that the conversion is correct to avoid issues 41/83
// main.cpp int main(int argc, char *argv[]) { float f = 123.321f; int i = static_cast<int>(f); } std::cout << My float is << f <<. << std::endl; // My float is 123.321 std::cout << My int is << i <<. << std::endl; // My int is 123 // Truncates the float, doesn t round 42/83
3. dynamic_cast<type> - Used for handling polymorphism - Base to Derived - Also Derived to Base, like static_cast, but this is again implicit and unnecessary - You don t know what the type of the class is - dynamic_cast returns nullptr when the cast fails 43/83
// cartest.cpp bool isferrari(car *car) { if (dynamic_cast<ferrari*>(car)!= nullptr) { return true; } return false; } 44/83
4. reinterpret_cast<type> - Turns one type directly into another - No type safety - it just does it - Only general guarantee is that you get what you started with if you cast back to the original type - This is dangerous - You re overriding the intended use of an object - Real world use - Interfacing with opaque data types - Occurs sometimes when using external APIs 45/83
5. const_cast<type> - const_cast changes the const-ness of an object - e.g. a const int can become an int - This is dangerous - By changing const-ness, you may end up breaking an invariant elsewhere and putting the program into an indeterminate state - Avoid unless you specifically cannot - Should not need to 46/83
6. C-Style Casting - Objects may also be cast using the C syntax - TypeA *a = (TypeA*) ptrtotypeb; - This is dangerous because which of the 4 casts used is not immediately clear - C-style casts try a number of C++ casts, sometimes two successively, until one succeeds - This style of casting exists as a C legacy - Use C++ casts, not C-style casts (or we ll deduct). - int => float and float => int: Use static_cast! 47/83
6. C-Style Casting - C++ is also easier to search for - The casting operation is an explicit word rather than the type and some symbols - C++ style casts keep code more maintainable - Easier to remove or change casts - The intent of the programmer is clear - The number of possible errors is reduced 48/83
IV: RAII 1. RAII Defined 2. Consistency 3. Resource Ownership 4. Memory Management 2.0 5. Smart Pointers a. std::unique_ptr b. std::shared_ptr 6. The Rule of 3 and the Rule of 5 49/83
1. RAII Defined - Resource Acquisition Is Initialization - RAII is a C++ coding idiom that makes memory management logical, useful, less error-prone, and generally better - This is A Good Thing 50/83
1. RAII cont. - RAII means that the acquisition of a resource is done during its initialization - If Foo owns a Bar, it initializes its Bar in its constructor, and frees that Bar in its destructor - If Foo opens a file, it closes it - Foo must release the resources it acquires! - The class giveth, and the class taketh away 51/83
// unicorn.h class Unicorn { public: Unicorn(); void init(); private: Horn *m_horn; } // An init function? Uh-oh // unicorn.cpp Unicorn::Unicorn() {} // Initializer list? Hello? Where s the horn? (Don t answer that) void Unicorn::init() { // This is bad. // When a Unicorn is constructed, it is left in an unusable state. m_horn = new Horn(); // The caller must also call this init function. They might not, and that s bad. } 52/83
1. RAII cont. - A class must be ready to use after construction - Problems with the previous code: - Destructor: Even if we had one, how does it know if we ve init d or not? - Maybe add a boolean? But then how do we know if that s been init d? - Should we have a destroy() function? - What if we call init twice? Memory leak! 53/83
// unicorn.h class Unicorn { public: Unicorn(); ~Unicorn(); private: Horn *m_horn; } // Much better: A destructor. This looks useful // unicorn.cpp Unicorn::Unicorn() : m_horn(new Horn()) {} // Much better: Unicorn acquires a Horn during initialization Unicorn::~Unicorn() { } delete m_horn; // Much better: Unicorn frees the Horn during destruction // Unicorn owns this Horn resource 54/83
1. RAII cont. - RAII entails two very important concepts - Object consistency - Resource ownership - These force you to code well 55/83
2. Consistency - An object must be in a consistent state after construction - You should not have to call a special series of functions before the object is ready to use - Anti-patterns: - An init() function - A destroy() function 56/83
2. Consistency cont. - Clarifying note: An object must be in a consistent state and ready to use after construction - e.g. A list may be empty. Empty is ready to use - RAII doesn t necessarily mean every possible resource is acquired, but that objects are ready to use upon construction and clean up after themselves 57/83
3. Resource Ownership - Whoever initializes the resource owns it - Resources can be shared, but ultimately the object who creates it is responsible for it - Unless explicitly understood otherwise - e.g. Builder pattern - builder technically instantiates the object, but it s only used as a tool by the real owner of the object. 58/83
// builder.cpp CarBuilder carbuilder; carbuilder.setengine(...); carbuilder.setrims(...); carbuilder.settires(...); // This is a builder! Car *car = carbuilder.buildcar(); // Build the car! carbuilder technically creates the car, but the owner is the caller! delete car; // The caller deletes the car! 59/83
3. Resource Ownership - When ownership is clear, memory management duties are clear - Why? - Because RAII - I own it, I create it, I destroy it - If I give it to you, it must be clear if I am sharing or transferring ownership 60/83
4. Memory Management 2.0 - We know that objects should be in a consistent state after construction - We know that ownership of resources should be clearly defined - We know that automatic storage is preferred - We sit down to code - And we immediately realize we can t get all 3 at once - Good news: We realized wrong 61/83
4. Memory Management 2.0 - You can use pointer member variables to delay initialization - Sometimes you can t just call an object s constructor from the parent s initializer list - It is okay if a direct member variable is not possible - Don t violate consistency and make init and destroy functions - They will infect the rest of your code - But wait, this isn t automatic storage... 62/83
5. Smart Pointers - C++ includes as part of its Standard Library objects collectively referred to as smart pointers - Smart pointers are A Good Thing - unique_ptr - shared_ptr 63/83
5. Smart Pointers cont. - Smart pointers do two things - Manage memory - Clearly delineate resource ownership - Using raw new and delete is an antipattern! - (oh no!) - This is another common deduction 64/83
5. Smart Pointers cont. - Smart pointers are wrapper classes for raw pointers - They take care of deleting objects when they (the smart pointers) go out of scope - Smart pointers are to be allocated with automatic storage - This is the whole point - you can allocate dynamic memory but reap the benefits of automatic storage! 65/83
5.a. std::unique_ptr - As the name implies, unique_ptr denotes unique (sole) ownership - This is my horn - You may not have the horn - unique_ptrs cannot be copied - This is the one, true horn, of which graven images are strictly prohibited by the compiler - unique_ptr deletes its object in its destructor - I take my horn to the grave 66 /83
5.a. std::unique_ptr cont. - You can use.get() to get the raw pointer - If you really insist, you may touch the horn - A raw pointer indicates that you do not own the resource - The unique_ptr owns it - This can obviously be abused - Who put these sparkles on my horn? - But then it s your (the caller s) fault. This should be understood because you don t own it - You won t need to use.get() yourself in 123 but you might see it in the stencil code 67 /83
// unicorn.h class Unicorn { public: Unicorn(); private: } std::unique_ptr<horn> m_horn; // unicorn.cpp Unicorn::Unicorn() : m_horn(std::make_unique<horn>()) {} // We can initialize a unique_ptr with std::make_unique (or like a regular pointer) Unicorn::~Unicorn() { // Remember, unique_ptr has automatic storage, so its destructor is automatically called } // when Unicorn s destructor is called (that is, when unique_ptr goes out of scope). 68/83
// main.cpp #include Unicorn.h int main(int argc, char *argv[]) { std::unique_ptr<unicorn> uniqueunicorn = std::make_unique<unicorn>(); // Instantiate the unique_ptr uniqueunicorn = std::make_unique<unicorn>(); // Delete the old Unicorn and replace it with a new one uniqueunicorn->func(); // We can access the pointer via the overloaded -> operator // Yes, you can overload operators in C++ } // No need to call a destructor. uniqueunicorn has automatic storage, // and it cleans up that Unicorn object for us. 69/83
5.b. std::shared_ptr - As the name implies, shared_ptr denotes multiple ownership - Use when the lifetime of a resource could extend beyond any particular owner s need - Twilight Sparkle and Amethyst Star share a horn and we don t know who will find their own horn first. 70/83
5.b. std::shared_ptr cont. - Shared resources are not common - Central planners generally don t understand unicorns - They can make your code confusing and hard to reason about - Who gets the horn when and for how long? - According to Bjarne Stroustrup (C++ s creator), shared_ptr should only be used as a last resort - A true unicorn has a mighty horn - no horn-sharing needed! 71/83
// unicorn.h class Unicorn { public: Unicorn(); private: } std::shared_ptr<horn> m_horn; // unicorn.cpp Unicorn::Unicorn() : m_horn( std::make_shared<horn>() ) {} // We initialize a shared pointer with std::make_shared Unicorn::~Unicorn() { } 72/83
5.c. Sneak-peak - What if I want to change who owns something? - E.G. a std::unique_ptr - What if I want to change an object s location in memory but not *copy* it? - Stay tuned: More semantics in the Advanced C++ slides 73/83
6. The Rule of 5 - The Rule of 5: If you define any of - Destructor - Copy constructor - Copy assignment operator - Move constructor - Move assignment operator 74/83
6. The Rule of 5 - What happens conceptually when object is copied? - Do both objects point to the same resource? - Does the new object get a copy of the resource? - Can it even be copied? 75/83
6. The Rule of 5 - Example: std::string - Should both strings objects point to the same block of memory holding the chars? - Or should the new string have a copy of that memory? - The second choice is preferred 76/83
6. The Rule of 5 - Example: A VBO class - Should the new VBO class copy the VBO id of the first one? - Should it attempt to make a copy of the GPU memory and make a new vbo id? - Should it even be copyable? - Probably not 77/83
6. The Rule of 5 - But what does copying have to do with destructors? - std::string example: Assume the two copies pointed to the same block of memory. One is freed and has its destructor called. The other now has an invalid pointer! - VBO example: Same deal. If you just copied the VBO id, when one was deleted, it would invalidate the other 78/83
6. The Rule of 5 - If copying doesn t make sense or is undesirable, you can enforce that in your code - C++11 and beyond (what we use in 123): Define copy constructor and copy assignment operator as = delete. 79/83
// vbo.h class VBO { public: VBO(); // Rule of 3: VBO(const &VBO) = delete; // Copy constructor VBO& operator=(const &VBO) = delete; // Copy assignment operator ~VBO(); // Destructor private: } GLuint m_vboid; 80/83
// vbo.cpp VBO::VBO() : m_vboid(0) // We initialize the id safely to 0 { glgenbuffers(1, &m_vboid); // Do initialization that can t be done in the initializer list } // Copying here doesn t make sense // We probably don t even want the client to be able to move huge chunks of GPU memory around VBO::~VBO() { } gldeletebuffers(1,&m_vboid);// Free resources in the destructor 81/83