C++ Sections I.Memory Management Basics II.The C++ Standard Library III.Casting IV.Resource Management: RAII V.The Compilation Process and You I. Memory Management Basics Memory Management Basics 1.Automatic vs. Dynamic memory 2.Checking for memory leaks 3.Pass by value 4.Pass by reference 5.Const Preliminary note - For the purpose of brevity, we shall refer to objects and primitive types collectively as just objects 1.Automatic vs. Dynamic Memory - Objects can be instantiated in memory in two main ways: - Automatic allocation - Frequently the stack but also the heap - Dynamic allocation - Frequently the heap - Don’t confuse automatic/dynamic with stack/heap 1.a. Automatic Storage - Objects with automatic storage are - Constructed on definition - Destructed when the scope is exited - Not visible outside of their scope - Example on next slide // main.cpp int computeExponentialLimes( int numLimes ) // Function we’re going to call { int base = 1; for(int i = 0; i < numLimes; i++) // base is allocated with automatic storage // i is also allocated with automatic storage, but inside the scope of the for-loop { base *= 2; // base is visible in scopes directly contained within it (e.g. loops) } // i goes out of scope, memory is freed up return base; // base is returned and goes out of scope, memory is freed up } int main(int argc, char* argv[]) // Main function: The hallmark of languages people actually use { // Scope “opened” int limes = 10; // limes is created with automatic storage int moreLimes = computeExponentialLimes( limes ); // base from above is copied to construct moreLimes when the function returns std::cout << “Why can’t I hold all these limes?” << std::endl; std::cout << “Maybe ” << moreLimes << “ is too many.” << std::endl; 1.a. Automatic storage - Memory? What memory? - The fact that you didn’t have to think about memory means the abstraction is doing its job - Automatic storage is A Good Thing - Objects are guaranteed to have their destructors called and be freed - This is very, very good 1.b. Dynamic Storage - Sometimes automatic storage is insufficient - Want to allocate a lot of memory - Want lifetime of object to persist beyond present scope - Want to delay object initialization - Dynamic memory is manually allocated - Placed on the heap on typical computers - Thus slower than stack allocation - Potential for memory leaks! 1.b. Dynamic Allocation - Objects are dynamically allocated via keyword new - Objects are freed with keyword delete - Failing to delete an object allocated by new causes a memory leak! - See Part IV on how to manage this like a champ - Don’t use malloc or free - That’s so 1972 // main.cpp int main(int argc, char* argv[]) { Foo *foo = new Foo(); // The * symbol means foo is a pointer to a Foo // foo holds the location in memory where the new Foo was created // foo itself is an automatically allocated variable. Remember, it points to // the Foo object that lives in dynamic memory } foo->doStuff(); // Use the arrow operator to access member functions and member data of Foo. delete foo; // Foo object freed // foo pointer freed - it’s gone out of scope // Alternate main.cpp int main(int argc, char* argv[]) { Foo *foo = new Foo(); // The * symbol means foo is a pointer to a Foo // foo holds the location in memory where the new Foo was created // foo itself is an automatically allocated variable. Remember, it points to // the Foo object that lives in dynamic memory foo->doStuff(); } // Use the arrow operator to access member functions and member data of Foo. // foo pointer freed - it’s gone out of scope // Memory leak: we never called delete on foo! // Clarifying note: foo is a pointer. It’s 4 bytes. It is a variable that takes up space in its own right. It has automatic storage. // By virtue of being a pointer, it holds the memory location where the Foo object we care about is actually (dynamically) stored // The Foo pointer is freed. The Foo object itself is not freed. // Alternate alternate main.cpp int main(int argc, char* argv[]) { Foo *foo = new Foo(); // The * symbol means foo is a pointer to a Foo // foo holds the location in memory where the new Foo was created // foo itself is an automatically allocated variable. Remember, it points to // the Foo object that lives in dynamic memory foo->doStuff(); // Use the arrow operator to access member functions and member data of Foo. foo = nullptr; // Reassign foo to hold a null pointer. // Oops! Now don’t have a handle to that Foo object… // This is now a memory leak! We have no way to delete that Foo! delete foo; } // Deleting a null pointer is always safe and does nothing // foo pointer freed - it’s gone out of scope // Alternate alternate main.cpp int main(int argc, char* argv[]) { Foo *foo = new Foo(); // The * symbol means foo is a pointer to a Foo // foo holds the location in memory where the new Foo was created // foo itself is an automatically allocated variable. Remember, it points to // the Foo object that lives in dynamic memory foo->doStuff(); // Use the arrow operator to access member functions and member data of Foo. foo = = new Foo(); // Reassign foo to hold a different Foo. // Oops! Now don’t have a handle to that Foo object… // This is also a memory leak! We have no way to delete that Foo! delete foo; } // Deleting a null pointer is always safe and does nothing // foo pointer freed - it’s gone out of scope // Bar.h class Bar { public: Bar(); ~Bar(); private: Baz *m_baz; } // Bar.cpp Bar::Bar() : m_baz( new Baz() ) // Member variable allocated in constructor { } Bar::~Bar() { delete m_baz; } // Member variable freed in destructor 2. Checking for 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! - The TAs will 3. 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 // main.cpp void increaseAndPrintWumbo( Wumbo wum ) // 3. Take a Wumbo as an argument { wum.increaseVal(); wum.printVal(); // 4. Call some member function. // 5. Output: 6 } int main(int argc, char* argv[]) { Wumbo w(5); increaseAndPrintWumbo( w ); w.printVal(); // 1. Create a Wumbo with initial value of 5. // 2. Pass the Wumbo by value. This invokes the Wumbo’s copy constructor // 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. 3. 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 much, are you? - Remember this! - But what else can we notice about this example? - Hint: Something inefficient is happening... // main.cpp void increaseAndPrintWumbo( Wumbo wum ) // 3. This Wumbo is constructed as main’s Wumbo is copied { wum.increaseVal(); wum.printVal(); } // 5. Call Wumbo’s destructor for wum. int main(int argc, char* argv[]) { Wumbo w(5); increaseAndPrintWumbo( w ); // 1. Create a Wumbo with initial value of 5. // 2. Pass the Wumbo by value. This invokes the Wumbo’s copy constructor w.printVal(); } // Call Wumbo’s destructor for w. 3. Pass by value cont. - A Wumbo 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? 4. 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* *The technical name of which is the lvalue reference declarator 4. Pass by reference cont. - Passing objects by reference is A Good Thing - Let’s revisit the example from earlier - This time we’ll use references - Let’s see what changes // main.cpp void increaseAndPrintWumbo( Wumbo &wum ) // 3. This function takes a reference to a Wumbo. No constructor called here { // Unlike before, this is not a copy of main’s Wumbo, it is main’s Wumbo. wum.increaseVal(); wum.printVal(); } // Output: 6 // 4. No destructor called here int main(int argc, char* argv[]) { Wumbo w(5); increaseAndPrintWumbo( w ); // 1. Create a Wumbo with initial value of 5. // 2. Pass the Wumbo by reference. This does not invoke Wumbo’s copy constructor w.printVal(); } is going out of scope // 5. Output: 6 // Call Wumbo’s destructor for w now that it 4. Pass by reference cont. - Two important changes - No copy and destruction of a new Wumbo - The Wumbo 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? 5. 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 You somehow have no choice - Let’s take a gander: // main.cpp void changeWumbo( Wumbo &i ) // Non-const ref = can change the argument { i = 7; } void printWumbo( const Wumbo &i ) // Const ref = can’t change the argument { i.printVal(); // i does not change } int main(int argc, char* argv[]) { } Wumbo x(5); // Initialize x to 5 changeWumbo( x ); // Do something to x printWumbo( x ); // Output: 7 // main.cpp void changeWumbo( const Wumbo &i ) // This can’t be right. Change a const Wumbo? { i = 7; // Error: Wumbo i is const, can’t change the value // This is a dumb function. Either don’t change x or remove the const qualifier } void printWumbo( const Wumbo &i ) { i.printVal(); // i does not change. As expected. Nothing to see here. } int main(int argc, char* argv[]) { } Wumbo x(5); // Initialize x to 5 changeWumbo( x ); // Do something to x printWumbo( x ); // Output: None because this code doesn’t compile 5. 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 - Example on next slide // walken.h class Walken { public: Walken( int x ); int getX() const; // A const member function declaration private: int mX; } // walken.cpp #include “walken.h” Walken::Walken( int x ) : mX( x ) {} int Walken::getX() const { return mX; } // A const member function definition. Nothing about Walken changes Pop quiz! - Is Java pass by value or pass by reference? - Answer: Pass by value - In fact, it’s strictly pass by value - According to the Java Language Specification - Section 8.4.1 Formal Language Parameters - See this article for an explanation II. The C++ Standard Library II. The C++ Standard Library 1.Overview 2.Templates 3.Case Study: std::map 4.Iterators 5.Other Container Classes 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 1.Overview cont. - The Standard Library is very useful - It 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 highperformance and/or nonstandard environment - Which you’re not 1.Overview cont. - What’s in the Standard Library? - std::string - words yo std::vector - resizable array std::map - key/value map std::cout, std::cin, std::cerr - standard i/o <cmath> header includes many common math functions - As you can see, everything is in the std namespace 1.Overview cont. - Many of the Standard Library classes are used for holding objects of any type - Before we can talk about them, we need to talk about how this class-independent behavior is achieved 2. Templates - C++ templates are used for generic programming - They’re similar to Java Generics - Here, we will only go over how to use them in the context of the Standard Library - See the Advanced C++ help session for more - Example on next slide // main.cpp #include <vector> // Include the vector header #include “Foo.h” // Some Foo class int main( int argc, char *argv[] ) { std::vector<Foo> myFoos; // Make a vector of Foos. Notice the angle brackets for( int i = 0; i < 10; i++ ) { Foo f( i ); myFoos.push_back( f ); // Some constructor for a Foo // Add the Foo to the vector } for( size_t i = 0; i < 10; i++ ) // Iterate through the vector’s contents { myFoos[ i ].print(); } } // Call some function on each Foo. 2. Templates cont. - Notice how the vector class doesn’t care about the type // main.cpp #include <vector> #include “Bar.h” // Some Bar class instead int main( int argc, char *argv[] ) { std::vector<Bar> myBars; for( int i = 0; i < 10; i++ ) { Bar b( i ); myBars.push_back( b ); } for( size_t i = 0; i < 10; i++ ) { myBars[ i ].bang(); } } // Foo or Bar 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 - Tis a deep rabbit hole of Turing-complete shenanigans indeed... 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 - You (should) already know how to use this data structure - We’ll go over how to use it in C++ *Technically, the full declaration is: template < class Key, class T, class Compare = less<Key>, class Alloc = allocator<pair<const Key,T> > > class map; // main.cpp #include <map> // Include appropriate headers #include <string> int main( int argc, char *argv[] ) { std::map<std::string, int> myColors; myColors[ “Red” // Map from strings to ints ] = 200; // Store the key and value via the []= operator myColors[ “Green” ] = 150; myColors[ “Blue” ] = 25.2f; std::cout << “Red: ” << myColors[ “Red” ] << std::endl; std::cout << “Green: ” << myColors[ “Green” ] << std::endl; std::cout << “Blue: ” << myColors[ “Blue” ] << std::endl; } // Output: // Red: 200 // Green: 150 // Blue: 25 // Implicit conversion warning - converts float to int // Access values the same way you stored them 3. Case Study: std::map cont. - So far so good - But...what about const? int printMapVal( const std::map<std::string, int> &map, std::string key ) { std::cout << key << “: ” << myColors[ key ] << std::endl; // Error: map is const } int printMapVal( const std::map<std::string, int> &map, std::string key ) { std::cout << key << “: ” << myColors.at( key ) << std::endl; } // This is fine 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* - It returns a const reference - Looking only *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. 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 - But what if we want to loop over the contents of a map? 4.Iterators - Iterators are special objects used by container classes - They’re used to (wait for it) iterate over the container’s contents - Best seen through example // main.cpp #include <map> // other headers omitted int main( int argc, char *argv[] ) { std::map< std::string, int > myColors; myColors[ “Red” ] = 100; myColors[ “Green” ] = 200; myColors[ “Blue” ] = 400; std::cout << “Contents of myColors map” << std::endl // What? for( std::map< std::string, int >::iterator it = myColors.begin(); it!= myColors.end(); it++ ) { std::cout << it->first << “: ” << it->second << std::endl; } } // 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; } } // main.cpp #include <map> // other headers omitted int main( int argc, char *argv[] ) { std::map< std::string, int > myColors; myColors[ “Red” ] = 100; myColors[ “Green” ] = 200; myColors[ “Blue” ] = 400; std::cout << “Contents of myColors map” << std::endl // 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; } } // main.cpp #include <map> // other headers omitted int main( int argc, char *argv[] ) { std::map< std::string, int > myColors; myColors[ “Red” ] = 100; myColors[ “Green” ] = 200; myColors[ “Blue” ] = 400; std::cout << “Contents of myColors map” << std::endl // 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; } } // main.cpp #include <map> // other headers omitted int main( int argc, char *argv[] ) { std::map< std::string, int > myColors; myColors[ “Red” ] = 100; myColors[ “Green” ] = 200; myColors[ “Blue” ] = 400; std::cout << “Contents of myColors map” << std::endl 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; } } 4.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 III. Casting III. Casting 1.Overview 2.static_cast 3.dynamic_cast 4.reinterpret_cast 5.const_cast 6.C-Style Casting 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 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 - You know that the conversion is correct // 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; std::cout << “My float is ” << i << “.” << std::endl; 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 // cartest.cpp bool isFerrari( Car *car ) { if( dynamic_cast<Ferrari*>( car ) != nullptr ) { return true; } return false; } 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 - Use very cautiously - Know explicitly why you must use it and why other methods won’t work before you use it 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 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 it 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 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 IV: RAII RAII Overview 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 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 - But what does it mean? 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 acquires a mutex lock, it releases it - If Foo opens a file, it closes it - Foo must release the resources it acquires! - “The class giveth, and the class taketh away” // walken.h class Walken { public: Walken(); void init(); // An init function? Uh-oh private: Watch *m_watch; } // walken.cpp Walken::Walken() // Initializer list? Hello? Where’s the watch? (Don’t answer that) {} void Walken::init() { // This is bad. // When a Walken is constructed, it is left in an unusable state. m_watch = new Watch(); } // The caller must also call this init function. They might not, and that’s bad. 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! // walken.h class Walken { public: Walken(); ~Walken(); // Much better: A destructor. This looks useful private: Watch *m_watch; } // walken.cpp Walken::Walken() : m_watch( new Watch() ) // Much better: Walken acquires a Watch during initialization {} Walken::~Walken() { delete m_watch; // Much better: Walken frees the Watch during destruction } // Walken owns this Watch resource 1.RAII cont. - RAII entails two very important concepts - Object consistency - Resource ownership - These force you to code well 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 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 - e.g. A logging service may be outputting to an empty stream, but it’s 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 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 only used as a tool by the real owner of the object. 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 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 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... 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 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!) - The exception to this is when you’re making smart pointers with them 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! 5.a. std::unique_ptr - As the name implies, unique_ptr denotes unique (sole) ownership - This is my hat - You may not have the hat - unique_ptrs cannot be copied - This is the one, true hat, of which graven images are strictly prohibited by the compiler - unique_ptr deletes its object in its destructor - I take my hat to the grave 5.a. std::unique_ptr cont. - You can use .get() to get the raw pointer - If you really insist, you may touch the hat - A raw pointer indicates that you do not own the resource - The unique_ptr owns it - This can obviously be abused - Who put this sticker on my hat? - But then it’s your (the caller’s) fault. This should be understood because you don’t own it // walken.h class Walken { public: Walken(); private: std::unique_ptr<Watch> m_watch; } // walken.cpp Walken::Walken() : m_watch( new Watch() ) // We can initialize a unique_ptr just like a regular pointer {} Walken::~Walken() { // Remember, unique_ptr has automatic storage, so its destructor is automatically called } when unique_ptr goes out of scope). // when Walken’s destructor is called (that is, // main.cpp #include “Foo.h” int main( int argc, char *argv[] ) { std::unique_ptr<Foo> uniqueFoo( new Foo() ); uniqueFoo.reset( new Foo() ); // Instantiate the unique_ptr // Delete the old Foo and replace it with a new one uniqueFoo->func(); // We can access the pointer via the overloaded -> operator // Yes, you can overload operators in C++ } // No need to call a destructor. uniqueFoo has automatic storage, // and it cleans up that Foo object for us. 5.b. std::shared_ptr - As the name implies, shared_ptr denotes multiple ownership - A true Soviet hat, comrade - Use when the lifetime of a resource could extend beyond any particular owner’s lifetime - Ivan and Vlad share the hat, but who will perish in the bread line first, we do not know 5.b. std::shared_ptr cont. - Shared resources are not common - Central planners generally don’t understand economics - They can make your code confusing and hard to reason about - Who gets the hat when and for how long, comrade? - According to Bjarne Stroustrup (C++’s creator), shared_ptr should only be used as a last resort - A true Soviet has a mighty mane - no hat-sharing needed! // walken.h class Walken { public: Walken(); private: std::shared_ptr<Watch> m_watch; } // walken.cpp Walken::Walken() : m_watch( std::make_shared<Watch>() ) {} Walken::~Walken() { } // We initialize a shared pointer with std::make_shared 5.b. shared_ptr cont. - Why do we use std::make_shared instead of new Type(...) like with std::unique_ptr? - Because the C++ committee forgot to include std::unique_ptr in C++11 - No, we’re not kidding - Luckily, C++14 has std::make_unique 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: Move semantics in the Advanced C++ lecture 6. The Rule of 3 and the Rule of 5 - The Rule of 3: If you define any of - Destructor - Copy constructor - Copy assignment operator - Then you must define all 3 - The Rule of 5 is the C++11 version of the Rule of 3, adding - Move constructor - Move assignment operator 6. The Rule of 3 - 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? 6. The Rule of 3 - 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? - Clearly the second choice is preferred 6. The Rule of 3 - 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 6. The Rule of 3 - 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 6. The Rule of 3 - If copying doesn’t make sense or is undesirable, you can enforce that in your code - Prior to C++11: Declare copy constructor and copy assignment operator as private and don’t define them - C++11 and beyond: Define copy constructor and copy assignment operator as = delete. // vbo.h class VBO { public: VBO(); // Rule of 3: VBO(const &VBO) = delete; VBO& operator=(const &VBO) = delete; ~VBO(); private: GLuint m_vboID; } // Copy constructor // Copy assignment operator // Destructor // 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 6. The Rule of 5 - The Rule of 5 extends the original Rule of 3 to include the move constructor and the move assignment operator - We will be learning about moving in the Advanced C++ Help Session - Just know that the Rule of 5 is the extended Rule of 3 updated for C++11 V. The Compilation Process and You Overview 1.Intro 2.The Preprocessor 3.The Compiler 4.The Linker 5.Linker Errors 6.Cleaning 1.Intro - How do you turn code into a program? - When you hit “build,” a number of processes are invoked to do this job - Preprocessor Compiler Assembler Linker - It is important to understand these steps 2. The Preprocessor - The preprocessor runs on your code before anything else - It prepares your code for compilation - It executes statements starting with the # symbol - Let’s look at some important ones 2.a. #include - #include “headerfile.h” - This replaces the include statement with the contents of the specified file - Use mainly to include header files - You can include other things - Don’t 2.b. #define - #define macro replacement - The define keyword replaces the first term with the second term - Example: #define MAX_THINGS 1024 - Do not use this to conveniently define constants (like above) - Use C++ instead 2.b. #define - You can also use #define to define function macros - You may have seen this in CS33 - That was C - This is C++ - Do not make function macros - They are poor C++ coding - Use C++ functions instead 2.b. #define -#define macro - You can also just define a special word - Can be done in code - Usually passed as an argument to the compiler - Specify these in Qt’s .pro file - This should be your main use of #define - This becomes useful with the next directive 2.c. #ifdef, #ifndef, #endif - #ifdef defined_macro // If defined compile this code // This compiles #endif - #ifndef defined_macro // If NOT defined compile this code // This does instead #endif - Using the previous #define macro, this does 2.c. #ifdef, #ifndef, #endif - The most common use of #ifdef, #ifndef, #endif is in header guards like this #ifndef HEADER_NAME_H #define HEADER_NAME_H // Header code #endif - This prevents headers from being included more than once any time you compile 2.c. #ifdef, #ifndef, #endif - These are also used to select system specific code - E.G. Cross platform development. #define different words to indicate whether you’re compiling for Android, PC, Mac, etc. - These are typically defined for you by the preprocessor - You should not use these for “actual coding” 2. Preprocessor cont. - The preprocessor does text replacement - It is not executed when your program runs - Its directives are not “live code” - Its logic is carried out and all macros are evaluated before the compiler touches the code 3. The Compiler - The compiler takes each source file generated by the preprocessor and compiles it into assembly language - From there, the assembler compiles the assembly into object code - The assembler is an intermediate step between the compiler and the linker - It is purely mechanical and not important for us 3. The Compiler - The major relevant parts of the compiler (for us) are its compiler flags - These specify certain behavior - Optimization - Warning and error generation - Plenty of other stuff - Different compilers exist; SunLab uses gcc - We’ll go over just a few of gcc’s options 3. The Compiler - -Wall: All warnings should be reported - Turn this on! - In .pro file, add QMAKE_CXXFLAGS += -Wall - You will learn lots of little bits from seeing everything - -Werror: All warnings become errors - Hardcore version of above - Turn this on if you want to force yourself to code proper C++ - Your employer will probably turn this on 3. The Compiler - -O or -O1: Turn on level 1 optimizations - -O2: Optimize more - -O3: Optimize morer - -O0 (That’s O and zero): Turn off optimizations - You can look up the specs on these, but they allow the compiler to make your code run faster - Hurray! 4. The Linker - The linker runs after each preprocessed source file has been compiled and assembled into object code - The linker turns this object code into an executable 4. The Linker - The linker will pull in external libraries - E.G. Windows .dll files, .a libraries, etc. - Header files almost always only say that certain things (classes, functions, etc.) will exist (i.e. they’ve been declared) - Implementations are defined in source files - The linker resolves all of this - Forward declared and included functions and classes are found 4. The Linker - The linker probably doesn’t sound very exciting - However, it will yell at you if you don’t know how it works - Or it can take a long time to execute - Learning do’s and don’t’s is important - Oh hey, guess what section’s up next? 5. Linker Errors - “Oh I think I’ll include this header file inside this other header file” - Prefer forward declaration until you must include - Important exception: Always include Standard Library headers. Forward declaring is undefined behavior - *gasp* - The std namespace is reserved 5. Linker Errors - What is a forward declaration? - Rather than typing #include <Foo.h> in your .h file Type class Foo; instead and put the include in the .cpp file - This signifies that the class Foo will exist and be defined later - This is all you need to say - Including the header file imports code and dependencies - This says what you need to say, but says too much more 5. Linker Errors - When can you forward declare in class header files? - When a function takes an object as an argument or returns an instance of it - Pointer and reference versions of the above - This includes smart pointers* - When a member variable is only a pointer or reference to an object *The smart pointer headers themselves must be included 5. Linker Errors - When must you include a header file inside another? - When extending a base class - When an object is a direct (non-reference, nonpointer) member of a class - When you want something from the Standard Library 5. Linker Errors - “Or what?” - Your code will take much longer to link - You will almost certainly create circular dependencies - Your code will fail QA and/or your boss will put on his/her disappointment face 5.a. Link times - If your code has lots of coupling, the Linker has more symbols to resolve and more places to look for them - Forward declaring is one statement for one class or function - Including shoves everything else about those functions and classes into the mix as well 5.b. Circular Dependencies - A circular dependency occurs when two header files include one another - Header A includes header B - Header B includes header A - Header A includes itself - an infinite regression 5.b. Circular Dependencies - Common example: Two classes contain instances of one another - Class A has member Class B - Class B has member Class A - In order to fully define class B for inclusion in Class A (and determine storage), class A must be fully defined, which requires that class B be fully defined… - Solution:Use pointers instead of direct 6. Cleaning - The compiler and linker are smart and try to reduce their workload - They won’t rebuild things unless they need to - Sometimes you’ll change an aspect of your build process - Add compilation flag - Add #define macro - Have a weird time replacing a file - Can end up with mixed-build state 6. Cleaning - Solution: Clean your project - Qt->Build->Clean All - Removes all generated files - The next build will be forced to recompile everything - When in doubt, clean it out! - If you’re getting strange errors that don’t reflect the code, the code just may need to be cleaned Thanks for playing! If you want to learn even more C++ (and there is always more), then come to the Advanced C++ Help Session (currently TBD).