C++

advertisement
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).
Download