Difference between revisions of "Intermediate C++ Game Programming Tutorial 21"
(→Source Code) |
(→Video Timestamp Index) |
||
(79 intermediate revisions by 2 users not shown) | |||
Line 1: | Line 1: | ||
− | Smart pointers. I have the smartest pointers. | + | Smart pointers. I know pointers, I have the smartest pointers. In this video we learn about <code>std::unique_ptr<></code>, which is by far the most frequently-used and important smart pointer. <code>std::shared_ptr<></code> can go suck on an egg. |
== Topics Covered == | == Topics Covered == | ||
− | * std::unique_ptr<> | + | * <code>std::unique_ptr<></code> |
+ | * How to pass (and not to pass) smart pointers | ||
+ | * Custom deleters | ||
== Video Timestamp Index == | == Video Timestamp Index == | ||
− | + | [https://youtu.be/WCTiFVlQFZU Tutorial 21] | |
+ | <div class="mw-collapsible mw-collapsed"><br /> | ||
+ | * Main risk factors for leaking memory when using pointers and (dynamically allocated) heap memory [https://youtu.be/WCTiFVlQFZU?t=0m30s 0:30] | ||
+ | <div class="mw-collapsible-content"> | ||
+ | ** Forgetting to delete memory | ||
+ | ** Copy constructing objects that contain pointers to dynamically allocated memory: upon leaving scope, your program will try to delete the same memory twice, leading to an Access Violation exception | ||
+ | ** Calling a function that returns a pointer to dynamically allocated memory without assigning it, i.e. "dropping" that pointer (it is not being owned anymore) | ||
+ | </div> | ||
+ | * Smart pointers / unique pointers can help avert these inadvertant memory leaks [https://youtu.be/WCTiFVlQFZU?t=2m39s 2:39] | ||
+ | <div class="mw-collapsible-content"> | ||
+ | ** They are RAII enabled, i.e. they take care of memory management (garbage collection) for you | ||
+ | ** No need to call <code>delete</code> on them, they will free the memory of the objects that they own in their destructors, which get called when they go out of scope | ||
+ | ** You <code>#include <memory></code> and create a unique pointer through <code>std::unique_ptr<T> p( new T );</code>. However, from now on, we avoid the use of <code>new</code> in our code | ||
+ | </div> | ||
+ | * Creating unique pointer in modern C++: <code>std::unique_ptr<T> p = std::make_unique<T>()</code> [https://youtu.be/WCTiFVlQFZU?t=3m57s 3:57] | ||
+ | <div class="mw-collapsible-content"> | ||
+ | ** This can be shortened to <code>auto p = std::make_unique<T>()</code> | ||
+ | ** To create a unique pointer to an array of size n: <code>auto p = std::make_unique<T[]>( n )</code>. The unique pointer will auto destroy all objects of type T when leaving scope | ||
+ | ** Unique pointers can be dereferenced <code>*p</code>, or dereferenced to access a member <code>p->someMember</code> | ||
+ | ** If you want to call a function by the underlying raw (/naked) pointer, use <code>SomeFunc( p.get() );</code>. Unique pointer does not implicitly convert to a raw pointer. This is to prevent you from accidently passing it to a function that might want to accept ownership of that pointer | ||
+ | </div> | ||
+ | * Example: functions in a class to transfer ownership of a resource through smart pointers [https://youtu.be/WCTiFVlQFZU?t=7m00s 7:00] | ||
+ | <div class="mw-collapsible-content"> | ||
+ | ** Instead of implementing the member function <code>T* Take()</code> and <code>void Give(T*)</code> | ||
+ | ** We implement <code>std::unique_ptr<T> Take()</code> and <code>void Give( std::unique_ptr<T> )</code> and avoid the risk of unintended memory leaks | ||
+ | ** When you are transferring an object via function, always pass unique pointers by value | ||
+ | </div> | ||
+ | * You control ownership of the resource object by using move semantics [https://youtu.be/WCTiFVlQFZU?t=9m12s 9:12] | ||
+ | <div class="mw-collapsible-content"> | ||
+ | ** You cannot copy a unique pointer; you have to use <code>objectHolder.Give( std::move( p ) );</code> where <code>p</code> is a unique pointer to a resource. | ||
+ | ** When implementing with smart pointers, you don't need to define destructors as this is taken care of by the smart pointer class: Rule-of-0 | ||
+ | ** If you use unique pointers correctly, you're not going to get memory leaks. It leads to: safer code, less code, and speed of execution (oh yeah, triple whammy) | ||
+ | ** If you have a unique pointer in your class, the compiler won't let you copy construct (copy constructor is deleted) / shallow copy, as this would generate two pointers to the same memory block | ||
+ | ** The default move constructor will still work | ||
+ | </div> | ||
+ | * Using a factory pattern (creating a factory function) to create unique pointers [https://youtu.be/WCTiFVlQFZU?t=11m47s 11:47] | ||
+ | <div class="mw-collapsible-content"> | ||
+ | ** Example: <code>std::unique_ptr<T> MakeT() { return std::make_unique<T>(); }</code> | ||
+ | ** As MakeT() will return an rvalue, you don't need to call <code>std::move</code> on it, and you can write <br /> <code>objectHolder.Give( MakeT() );</code> | ||
+ | </div> | ||
+ | * Implementing smart pointers in the MemeFighter game [https://youtu.be/WCTiFVlQFZU?t=13m58s 13:58] | ||
+ | <div class="mw-collapsible-content"> | ||
+ | ** Use <code>std::vector<std::unique_ptr<MemeFighter>> team;</code> to represent the teams of MemeFighters | ||
+ | ** Use <code>team.emplace_back(...)</code> to add elements to the vector, as you cannot use <code>= {.. , .. , ..}</code> to initialize a vector with unique pointers | ||
+ | ** Rewrite the predicate function as <code>[](const std::unique_ptr<MemeFighter>& mf){return mf->IsAlive();}</code> | ||
+ | ** Rewrite the ownership of a weapon to be represented by <code>std::unique_ptr<Weapon> pWeapon</code> | ||
+ | ** Remember to pass unique pointers by value when you want to transfer ownership inside a function | ||
+ | </div> | ||
+ | * When to pass smart pointers in function calls [https://youtu.be/WCTiFVlQFZU?t=17m47s 17:47] | ||
+ | <div class="mw-collapsible-content"> | ||
+ | ** Just because you work with smart pointers, doesn't mean that everything you pass into functions has to be passed in with a smart pointer | ||
+ | ** The only time you should be passing in smart pointers is when you are transfering ownership inside the function | ||
+ | ** Otherwise, just pass in with a normal reference to the object (or a normal pointer sometimes) | ||
+ | ** One exception here are functors for std <algorithm>s, see above | ||
+ | </div> | ||
+ | * Implementing smart pointers in the Surface class used for sprite loading & drawing [https://youtu.be/WCTiFVlQFZU?t=18m52s 18:52] | ||
+ | * Going over the documentation for <code>unique_ptr</code> on cppreference.com [https://youtu.be/WCTiFVlQFZU?t=21m55s 21:55] | ||
+ | * Review of main lessons [https://youtu.be/WCTiFVlQFZU?t=25m23s 25:23] | ||
+ | </div> | ||
== Bonus Video == | == Bonus Video == | ||
− | + | The addition of smart pointers to <code>Surface</code> has allowed us to reduce the amount of code we need to write for <code>Surface</code>, making it simpler and correct by default. But we still need to write logic for those pesky copy operations... What if we used a container like <code>std::vector</code>! Then all that copying bullshit will be taken care of for us. | |
+ | |||
+ | This is what we do in the bonus video. The only problem is, although adding <code>std::vector</code> will not make our release build any slower (the compiler optimizes out all of the extra abstraction for us), it does make the debug build slow. Usually, we don't worry about performance under debug, but for stuff like <code>PutPixel</code>, since we're calling it potentially millions of times per second, if our debug build is too slow it will become unusable for development and testing. | ||
+ | |||
+ | So the second goal of this bonus video is to optimize the debug build so that <code>PutPixel</code>-related operation run very fast, regardless of the fact that we are using <code>std::vector</code> to manage the array of pixels. The end result is that we can achieve rendering speeds which are over 100x faster than without the optimized debug configuration. | ||
+ | |||
+ | * [https://youtu.be/8VrOe512YIw Bonus Video Link] | ||
+ | |||
+ | == Notes == | ||
+ | Although Chili uses <code>std::unique_ptr<></code> in <code>Surface</code>, which allows us to <code>= default</code> the move members and leave the destructor undeclared, the astute student will realize that <code>Surface</code> should actually be setting the <code>width</code> and <code>height</code> of the donor surface to <code>0</code> when pilfering, so we actually still need to declare move members in this case. This is done in the Bonus Video where we perfect <code>Surface</code>. | ||
+ | |||
+ | <span style="color:red">Also note one major mistake</span>: when we convert <code>MemeFighter</code> to use <code>std::unique_ptr</code> for owning its <code>Weapon</code>, we remove the destructor (because we no longer need to manually <code>delete</code> the heap <code>Weapon</code> object). However, we still need to mark the destructor as <code>virtual</code> so that the proper derived destructor gets called in polymorphic situations. The correct course should have been to change the destructor to <code>virtual ~MemeFighter() = default;</code> (a commit has been pushed after the fact that fixes this issue). | ||
== Source Code == | == Source Code == | ||
* [https://github.com/planetchili/Inheritance Inheritance (Memefighter) Github Repository]<br /> | * [https://github.com/planetchili/Inheritance Inheritance (Memefighter) Github Repository]<br /> | ||
− | * [https://github.com/planetchili/Sprite Sprite Github Repository (for <code>Surface</code> code)] | + | * [https://github.com/planetchili/Sprite Sprite Github Repository (for <code>Surface</code> code)] |
== See also == | == See also == | ||
* [[Intermediate C++ Game Programming Tutorial 22|Next in series (Tutorial 22)]] | * [[Intermediate C++ Game Programming Tutorial 22|Next in series (Tutorial 22)]] | ||
* [[Intermediate C++ Game Programming Series]] | * [[Intermediate C++ Game Programming Series]] |
Latest revision as of 05:02, 3 November 2019
Smart pointers. I know pointers, I have the smartest pointers. In this video we learn about std::unique_ptr<>
, which is by far the most frequently-used and important smart pointer. std::shared_ptr<>
can go suck on an egg.
Topics Covered
-
std::unique_ptr<>
- How to pass (and not to pass) smart pointers
- Custom deleters
Video Timestamp Index
- Main risk factors for leaking memory when using pointers and (dynamically allocated) heap memory 0:30
- Forgetting to delete memory
- Copy constructing objects that contain pointers to dynamically allocated memory: upon leaving scope, your program will try to delete the same memory twice, leading to an Access Violation exception
- Calling a function that returns a pointer to dynamically allocated memory without assigning it, i.e. "dropping" that pointer (it is not being owned anymore)
- Smart pointers / unique pointers can help avert these inadvertant memory leaks 2:39
- They are RAII enabled, i.e. they take care of memory management (garbage collection) for you
- No need to call
delete
on them, they will free the memory of the objects that they own in their destructors, which get called when they go out of scope - You
#include <memory>
and create a unique pointer throughstd::unique_ptr<T> p( new T );
. However, from now on, we avoid the use ofnew
in our code
- Creating unique pointer in modern C++:
std::unique_ptr<T> p = std::make_unique<T>()
3:57
- This can be shortened to
auto p = std::make_unique<T>()
- To create a unique pointer to an array of size n:
auto p = std::make_unique<T[]>( n )
. The unique pointer will auto destroy all objects of type T when leaving scope - Unique pointers can be dereferenced
*p
, or dereferenced to access a memberp->someMember
- If you want to call a function by the underlying raw (/naked) pointer, use
SomeFunc( p.get() );
. Unique pointer does not implicitly convert to a raw pointer. This is to prevent you from accidently passing it to a function that might want to accept ownership of that pointer
- This can be shortened to
- Example: functions in a class to transfer ownership of a resource through smart pointers 7:00
- Instead of implementing the member function
T* Take()
andvoid Give(T*)
- We implement
std::unique_ptr<T> Take()
andvoid Give( std::unique_ptr<T> )
and avoid the risk of unintended memory leaks - When you are transferring an object via function, always pass unique pointers by value
- Instead of implementing the member function
- You control ownership of the resource object by using move semantics 9:12
- You cannot copy a unique pointer; you have to use
objectHolder.Give( std::move( p ) );
wherep
is a unique pointer to a resource. - When implementing with smart pointers, you don't need to define destructors as this is taken care of by the smart pointer class: Rule-of-0
- If you use unique pointers correctly, you're not going to get memory leaks. It leads to: safer code, less code, and speed of execution (oh yeah, triple whammy)
- If you have a unique pointer in your class, the compiler won't let you copy construct (copy constructor is deleted) / shallow copy, as this would generate two pointers to the same memory block
- The default move constructor will still work
- You cannot copy a unique pointer; you have to use
- Using a factory pattern (creating a factory function) to create unique pointers 11:47
- Example:
std::unique_ptr<T> MakeT() { return std::make_unique<T>(); }
- As MakeT() will return an rvalue, you don't need to call
std::move
on it, and you can write
objectHolder.Give( MakeT() );
- Example:
- Implementing smart pointers in the MemeFighter game 13:58
- Use
std::vector<std::unique_ptr<MemeFighter>> team;
to represent the teams of MemeFighters - Use
team.emplace_back(...)
to add elements to the vector, as you cannot use= {.. , .. , ..}
to initialize a vector with unique pointers - Rewrite the predicate function as
[](const std::unique_ptr<MemeFighter>& mf){return mf->IsAlive();}
- Rewrite the ownership of a weapon to be represented by
std::unique_ptr<Weapon> pWeapon
- Remember to pass unique pointers by value when you want to transfer ownership inside a function
- Use
- When to pass smart pointers in function calls 17:47
- Just because you work with smart pointers, doesn't mean that everything you pass into functions has to be passed in with a smart pointer
- The only time you should be passing in smart pointers is when you are transfering ownership inside the function
- Otherwise, just pass in with a normal reference to the object (or a normal pointer sometimes)
- One exception here are functors for std <algorithm>s, see above
Bonus Video
The addition of smart pointers to Surface
has allowed us to reduce the amount of code we need to write for Surface
, making it simpler and correct by default. But we still need to write logic for those pesky copy operations... What if we used a container like std::vector
! Then all that copying bullshit will be taken care of for us.
This is what we do in the bonus video. The only problem is, although adding std::vector
will not make our release build any slower (the compiler optimizes out all of the extra abstraction for us), it does make the debug build slow. Usually, we don't worry about performance under debug, but for stuff like PutPixel
, since we're calling it potentially millions of times per second, if our debug build is too slow it will become unusable for development and testing.
So the second goal of this bonus video is to optimize the debug build so that PutPixel
-related operation run very fast, regardless of the fact that we are using std::vector
to manage the array of pixels. The end result is that we can achieve rendering speeds which are over 100x faster than without the optimized debug configuration.
Notes
Although Chili uses std::unique_ptr<>
in Surface
, which allows us to = default
the move members and leave the destructor undeclared, the astute student will realize that Surface
should actually be setting the width
and height
of the donor surface to 0
when pilfering, so we actually still need to declare move members in this case. This is done in the Bonus Video where we perfect Surface
.
Also note one major mistake: when we convert MemeFighter
to use std::unique_ptr
for owning its Weapon
, we remove the destructor (because we no longer need to manually delete
the heap Weapon
object). However, we still need to mark the destructor as virtual
so that the proper derived destructor gets called in polymorphic situations. The correct course should have been to change the destructor to virtual ~MemeFighter() = default;
(a commit has been pushed after the fact that fixes this issue).