Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Splitting the smartPointers exercise into 5 problems. #530

Merged
merged 11 commits into from
Sep 30, 2024
2 changes: 2 additions & 0 deletions exercises/ExerciseSchedule_AdvancedCourse.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ Day 3

### Atomicity (directory: [`atomic`](atomic), [cheatSheet](ExercisesCheatSheet.md#atomicity-directory-atomic))

### Condition variables (directory: [`condition_variable`](condition_variable), [cheatSheet](ExercisesCheatSheet.md#condition-variables-directory-condition_variable))

### Generic programming / templates (directory: [`templates`](templates), [cheatSheet](ExercisesCheatSheet.md#generic-programming--templates-directory-templates))
As a prerequisite for variadic templates, and in case it was not covered in day 2 session

Expand Down
7 changes: 5 additions & 2 deletions exercises/ExercisesCheatSheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,21 +137,24 @@ The goal is to use STL algorithms. I would advise to start in this order :

### Smart pointers (directory: [`smartPointers`](smartPointers))

Here we have four code snippets that will benefit from using smart pointers.
Here we have five code snippets that will benefit from using smart pointers.
**Essentials**: Work on part 1 and 2
**Advanced**: Try all parts

- `problem1` is a simple case of usage of `make_unique` with an observer pattern where the raw pointer should be used.
- `problem2` is an example of a collection of pointers. Move semantic has to be used to transfer ownership of newly created objects to the container (alternatively, `emplace_back`).
- `problem3` is an example of shared ownership where `std::shared_pointer` should be used.
- `problem4` demonstrates the usage of `shared_ptr` as class members. It has a second part where a `weak_ptr` can be used, but can be skipped if not enough time.
- `problem4` demonstrates the usage of `shared_ptr` as class members.
- `problem5` demonstrates the usage of `weak_ptr` can be used, but can be skipped if not enough time.

### std::optional (directory: [`optional`](optional))

Use std::optional to signify disallowed values in a computation.
1. Use std::optional as return value of the mysqrt function. Use `nullopt_t` for negative arguments. Note that `return {}` will create a `nullopt_t` automatically.
2. Given that the return type changes, modify the square function accordingly to take into account cases where a `nullopt_t` is given as input. Note that `std::optional` can be directly used in an if statement as if it would be a boolean to check whether is value is present

### std::variant (directory: [`variant`](variant))

Use the variant as an alternative to inheritance. The goal is to understand
1. That the base class is unnecessary when variant is used
2. That no dynamic allocations and polymorphism are necessary because the variant can directly be pushed into the vector
Expand Down
Empty file removed exercises/code/memcheck/README.md
Empty file.
16 changes: 12 additions & 4 deletions exercises/smartPointers/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@ include( "${CMAKE_CURRENT_SOURCE_DIR}/../common.cmake" )
set( CMAKE_CXX_STANDARD 20 )

# Create the user's executable.
add_executable( smartPointers "smartPointers.cpp" )
add_executable( problem1 "problem1.cpp" )
add_executable( problem2 "problem2.cpp" )
add_executable( problem3 "problem3.cpp" )
add_executable( problem4 "problem4.cpp" )
add_executable( problem5 "problem5.cpp" )

# Create the "solution executable".
add_executable( smartPointers.sol EXCLUDE_FROM_ALL "solution/smartPointers.sol.cpp" )
add_dependencies( solution smartPointers.sol )
# Create the "solution executables".
add_executable( problem1.sol EXCLUDE_FROM_ALL "solution/problem1.sol.cpp" )
add_executable( problem2.sol EXCLUDE_FROM_ALL "solution/problem2.sol.cpp" )
add_executable( problem3.sol EXCLUDE_FROM_ALL "solution/problem3.sol.cpp" )
add_executable( problem4.sol EXCLUDE_FROM_ALL "solution/problem4.sol.cpp" )
add_executable( problem5.sol EXCLUDE_FROM_ALL "solution/problem5.sol.cpp" )
add_dependencies( solution problem1.sol problem2.sol problem3.sol problem4.sol problem5.sol )
6 changes: 3 additions & 3 deletions exercises/smartPointers/Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
all: smartPointers
solution: smartPointers.sol
all: problem1 problem2 problem3 problem4 problem5
solution: problem1.sol problem2.sol problem3.sol problem4.sol problem5.sol

clean:
rm -f *o *so smartPointers *~ smartPointers.sol
rm -f *o *so *~ problem? problem?.sol

% : %.cpp
$(CXX) -g -std=c++20 -Wall -Wextra -o $@ $<
Expand Down
23 changes: 11 additions & 12 deletions exercises/smartPointers/README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
# Writing leak-free C++.

Here we have four code snippets that will benefit from using smart pointers.
By replacing every explicit `new` with `make_unique` or `make_shared`,
(alternatively by explicitly instantiating smart pointers) we will fix memory leaks,
and make most cleanup code unnecessary.
# Writing leak-free and fault-free C++

Here we have five code snippets that will benefit from using smart pointers. By replacing every explicit `new` with `make_unique` or `make_shared`, (alternatively by explicitly instantiating smart pointers) we will fix memory leaks, segmentation faults, and make most cleanup code unnecessary.

## Prerequisites

* Which pointer is used for what?
* Do you know which kind of pointer is used for what?
* Raw pointer
* [`std::unique_ptr`](https://en.cppreference.com/w/cpp/memory/unique_ptr)
* [`std::shared_ptr`](https://en.cppreference.com/w/cpp/memory/shared_ptr)
* C++-14 for `std::make_unique` / `std::make_shared`. Understand what these functions do.
* Helpful: Move semantics for `problem2()`, but can do without.
* Helpful: Move semantics for `problem2()`, but one can do without.

## Instructions

* Compile and run the program. It doesn't generate any output.
* Run with valgrind to check for leaks
* In the **essentials course**, work on `problem1` and `problem2`, and fix the leaks using smart pointers.
* In the **advanced course**, work on `problem1` to `problem5`. Skip `problem4` and `problem5` if you don't have enough time.
* Dedicated instructions are given in each cpp file.
* Each one is written so that you easily check if the problem is solved or not.
* If seen in course before, you are also advised to try external tools such as valgrind:
```
valgrind --leak-check=full --track-origins=yes ./smartPointers
valgrind --leak-check=full --track-origins=yes ./problem1
```
* In the **essentials course**, work on `problem1()` and `problem2()`, and fix the leaks using smart pointers.
* In the **advanced course**, work on `problem1()` to `problem4()`. Skip `problem4()` if you don't have enough time.
72 changes: 72 additions & 0 deletions exercises/smartPointers/problem1.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@


#include <iostream>
#include <array>


/* --------------------------------------------------------------------------------------------
* Unique ownership.
*
* Always use smart pointers instead of `new`. A frequent source of memory leaks is a function
* that terminates in an unexpected way.
*
* Tasks
* 1) Compile and run the code below. Notice that the final count is `1`,
* showing that the instance of LargeObject has not been deallocated.
* 2) Modify `doStuff()` (only) so to use a `std::unique_ptr` instead of a raw pointer.
* The final count should be `0`, and the memory leak solved.
* --------------------------------------------------------------------------------------------
*/


// The class LargeObject emulates a large object.
// One should avoid to copy it, and rather use
// a pointer to pass it around.

struct LargeObject {

std::array<double, 100000> data ;

// So to check for some potential memory leak,
// we count the constructions and destructions
inline static std::size_t count = 0;
LargeObject() { count++ ; }
~LargeObject() { count-- ; }

} ;

// A function to do something with a large object.
// Here we simulate that an error happens.

void changeLargeObject( LargeObject & object ) {

object.data[0] = 1. ;
throw std::invalid_argument("Error when changing object data.") ;

}

// Often, data are owned by one entity, and merely used by others.
// In this case, we hand the data to changeLargeObject(),
// and unfortunately, something goes wrong...

void doStuff() {

// MAKE YOUR CHANGES IN THIS FUNCTION

auto obj = new LargeObject ;
changeLargeObject(*obj) ;
delete obj ;

}

int main() {

try {
doStuff() ;
} catch ( const std::exception & e ) {
std::cerr<< "Terminated with exception: " << e.what() << "\n" ;
}

std::cout<<"Leaked large objects: "<<LargeObject::count<<std::endl ;

}
88 changes: 88 additions & 0 deletions exercises/smartPointers/problem2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@


#include <iostream>
#include <array>
#include <vector>


/* --------------------------------------------------------------------------------------------
* Collections of smart pointers.
*
* Often, one has to store pointers to objects in collections.
* Fix the memory leaks below by using `std::unique_ptr`.
*
* Tasks
* 1) Compile and run the code below. Notice that the final count is `10`,
* which is expected because the new objects are never deallocated.
* 2) Factory functions should better return smart pointers,
* because it clarifies who owns an object.
* Change the return type of the function `newLargeObject()` for a `std::unique_ptr()`.
* The vector should own the objects, so try to store them using smart pointers.
* Since the change function doesn't accept smart pointers, find a solution to pass the objects.
* Try to use `std::unique_ptr`, not `std::shared_ptr` !
* --------------------------------------------------------------------------------------------
*/


// The class LargeObject emulates a large object.
// One should avoid to copy it, and rather use
// a pointer to pass it around.

struct LargeObject {

std::array<double, 100000> data ;

// So to check for some potential memory leak,
// we count the constructions and destructions
inline static std::size_t count = 0;
LargeObject() { count++ ; }
~LargeObject() { count-- ; }

} ;

// A factory function to create large objects.

LargeObject * newLargeObject() {

// MAKE YOUR CHANGES IN THIS FUNCTION

auto object = new LargeObject() ;
// Imagine there is more setup steps of "object" here
// ...
return object ;

}

// A function to do something with the objects.
// Note that since we don't own the object,
// we don't need a smart pointer as argument.

void changeLargeObject( LargeObject & object ) {

object.data[0] = 1. ;

}

void doStuff() {

// MAKE YOUR CHANGES IN THIS FUNCTION

std::vector<LargeObject *> largeObjects ;

for ( unsigned int i = 0 ; i < 10 ; ++i ) {
auto newObj = newLargeObject() ;
// ... additional newObj setup ...
largeObjects.push_back(newObj) ;
}

for ( const auto & obj : largeObjects ) {
changeLargeObject(*obj) ;
}
}

int main() {

doStuff() ;
std::cout<<"Leaked large objects: "<<LargeObject::count<<std::endl ;

}
125 changes: 125 additions & 0 deletions exercises/smartPointers/problem3.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@


#include <iostream>
#include <array>
#include <vector>
#include <random>
#include <algorithm>


/* --------------------------------------------------------------------------------------------
* Shared ownership.
*
* Most of the time, ownership can be solved by having one owner (with `std::unique_ptr`) and
* one or more observers (raw pointers or references). Sometimes, we need to truly share data,
* though. Here is an example of a completely messed up ownership model, which could be
* fixed using shared_ptr.
*
* Tasks
* 1) Verify the mess by repeatedly running it using such a command like:
* `while true; do ./problem3 ; done`
* You should notice that the program regularly leaks.
* 2) Fix the ownership model using `std::shared_ptr` !
* - Convert the vectors to holding `std::shared_ptr`.
* - Fix the arguments of the functions.
* 3) Speed optimisation: make sure that you don't create & destroy useless `std::shared_ptr`,
* which is slow, for example in the for loop of `doStuff()` and when
* calling `changeLargeObject()`.
* --------------------------------------------------------------------------------------------
*/


// The class LargeObject emulates a large object.
// One should avoid to copy it, and rather use
// a pointer to pass it around.

struct LargeObject {

std::array<double, 100000> data ;

// So to check for some potential memory leak,
// we count the constructions and destructions
inline static std::size_t count = 0;
LargeObject() { count++ ; }
~LargeObject() { count-- ; }

} ;

// This removes an element from a non-owning vector,
// in a random place. Such elements can by known in
// several vectors, so they must not be deleted.

void removeRandom( std::vector<LargeObject *> & collection, std::default_random_engine & engine ) {

// MAKE YOUR CHANGES IN THIS FUNCTION

auto pos = collection.begin() + engine() % collection.size() ;
collection.erase(pos);

}

// A function to do something with a large object.
// Note that since we don't own the object,
// we don't need a smart pointer as argument.

void changeLargeObject( LargeObject & object ) {

object.data[0] = 1. ;

}

// Global stuff: we have pointers to objects duplicated in two different collections.
// We work a bit with the collections, and then we try to clean up without neither
// memory leak nor segmentation fault. Without a shared ownership model, this becomes a mess.

void doStuff() {

// MAKE YOUR CHANGES IN THIS FUNCTION

// Prepare a non deterministic random engine

std::random_device device ;
std::default_random_engine engine(device()) ;

// Original collection

std::vector<LargeObject*> objVector(10);
for ( auto & ptr : objVector ) {
ptr = new LargeObject();
}

// Let's copy the whole collection

auto objVectorCopy(objVector);

// Random work with the objects

removeRandom(objVector,engine);
removeRandom(objVectorCopy,engine);
removeRandom(objVectorCopy,engine);
// ...
for (auto objPtr : objVector ) {
changeLargeObject(*objPtr) ;
}

// ONCE YOU FIXED CODE ABOVE WITH SHARED POINTERS
// THE UGLY CODE BELOW SHOULD BECOME UNNECESSARY

for ( auto objPtr : objVector ) {
delete objPtr ;
}
for ( auto objPtr : objVectorCopy ) {
// If the element is in the original collection, it was already deleted.
if (std::find(objVector.begin(), objVector.end(), objPtr) == objVector.end()) {
delete objPtr;
}
}

}

int main() {

doStuff() ;
std::cout<<"Leaked large objects: "<<LargeObject::count<<std::endl ;

}
Loading