diff --git a/exercises/ExerciseSchedule_AdvancedCourse.md b/exercises/ExerciseSchedule_AdvancedCourse.md index 4c3da299..c6c55249 100644 --- a/exercises/ExerciseSchedule_AdvancedCourse.md +++ b/exercises/ExerciseSchedule_AdvancedCourse.md @@ -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 diff --git a/exercises/ExercisesCheatSheet.md b/exercises/ExercisesCheatSheet.md index fa8c268e..752ee626 100644 --- a/exercises/ExercisesCheatSheet.md +++ b/exercises/ExercisesCheatSheet.md @@ -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 diff --git a/exercises/code/memcheck/README.md b/exercises/code/memcheck/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/exercises/smartPointers/CMakeLists.txt b/exercises/smartPointers/CMakeLists.txt index ed01281b..1d13482c 100644 --- a/exercises/smartPointers/CMakeLists.txt +++ b/exercises/smartPointers/CMakeLists.txt @@ -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 ) diff --git a/exercises/smartPointers/Makefile b/exercises/smartPointers/Makefile index 9e8f8ad0..59041117 100644 --- a/exercises/smartPointers/Makefile +++ b/exercises/smartPointers/Makefile @@ -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 $@ $< diff --git a/exercises/smartPointers/README.md b/exercises/smartPointers/README.md index 0cc9cd34..5c710bb9 100644 --- a/exercises/smartPointers/README.md +++ b/exercises/smartPointers/README.md @@ -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. diff --git a/exercises/smartPointers/problem1.cpp b/exercises/smartPointers/problem1.cpp new file mode 100644 index 00000000..1741aca7 --- /dev/null +++ b/exercises/smartPointers/problem1.cpp @@ -0,0 +1,72 @@ + + +#include +#include + + +/* -------------------------------------------------------------------------------------------- + * 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 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: "< +#include +#include + + +/* -------------------------------------------------------------------------------------------- + * 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 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 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: "< +#include +#include +#include +#include + + +/* -------------------------------------------------------------------------------------------- + * 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 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 & 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 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: "< +#include +#include + + +/* -------------------------------------------------------------------------------------------- + * Smart pointers as class members. + * + * Class members that are pointers can quickly become a problem. + * Firstly, if only raw pointers are used, the intended ownership is unclear. + * Secondly, it's easy to overlook that a member has to be deleted. + * Thirdly, pointer members usually require you to implement copy or move constructors + * and assignment operators (--> rule of 3, rule of 5). + * Since C++-11, one can solve a few of those problems using smart pointers. + * + * The class "Owner" owns some data, but it is broken. If you copy it like in + * doStuff(), you have two pointers pointing to the same data, but both instances + * think that they own the data. + * + * Tasks + * 1) It likely crashes. Verify this. You can also try running `valgrind ./problem4`, + * it should give you some hints as to what's happening. + * 2) Fix the Owner class by using a `std::shared_ptr` for its data, which we can + * copy as much as we want. Run the fixed program. + * Note: Once `std::shared_ptr` is in use, you can also use the default destructor. + * + * -------------------------------------------------------------------------------------------- + */ + +struct LargeObject { + + std::array data ; + +} ; + +class Owner { + + // MAKE YOUR CHANGES IN THIS CLASS + + public: + + Owner() : _largeObject( new LargeObject() ) {} + LargeObject * getLargeObject() { return _largeObject ; } + ~Owner() { delete _largeObject ; } + + private: + + LargeObject * _largeObject ; + +} ; + +void doStuff() { + + std::vector owners ; + + for ( int i = 0 ; i < 5 ; ++i ) { + Owner owner ; + // ... additional owner setup ... + owners.push_back(owner) ; + } + + /* Now we have a problem: + * We created Owner instances on the stack, and copied them into the vector. + * When the instances on the stack are destroyed, the memory is deallocated. + * All copies in the vector now point to the deallocated memory! + * We could fix this using copy constructors (but we don't want to copy the data), + * using move semantics or using shared_ptr. + * Here, we want to go for shared_ptr. + */ + +} + +int main() { + + doStuff() ; + +} diff --git a/exercises/smartPointers/problem5.cpp b/exercises/smartPointers/problem5.cpp new file mode 100644 index 00000000..8a6f9ea4 --- /dev/null +++ b/exercises/smartPointers/problem5.cpp @@ -0,0 +1,113 @@ + + +#include +#include +#include +#include + + +/* -------------------------------------------------------------------------------------------- + * Weak pointers. + * + * Let's construct some `std::weak_ptr` so to observe some `std::shared_ptr`. + * This weak pointers can be used to retreive the object pointed by the corresponding `std::shared_ptr`, + * but it does not increase the reference count of the objects, and does not prevent + * the deletion of the underlying objects if all shared pointers go out of scope. + * To *use* the observed data, one has to create a `std::shared_ptr` from the `std::weak_ptr`, + * so that it is guaranteed that the underlying object is alive. + * + * In our case, the `Observer` class wants to act on the data of the `Owner`, + * but it doesn't need to own it. To do this, we use a weak pointer. + * + * Tasks + * 1) Investigate the crash. Optionally use a debugger, run in valgrind, + * compile with -fsanitize=address ... + * 2) Rewrite the interface of Owner::getData() such that the observer can see the + * `std::shared_ptr` pointing to the large object. + * Review the class `Observer` such that it stores a `std::weak pointer`. + * In `setValue`and `getValue`, access the weak pointer, and use the data *only* if the memory is still alive. + * Note: What you need is weak_ptr::lock(). Check out the documentation and the example at the bottom: + * https://en.cppreference.com/w/cpp/memory/weak_ptr/lock + * + * -------------------------------------------------------------------------------------------- + */ + +struct LargeObject { + + std::array data ; + +} ; + +class Owner { + + // SOME CHANGES NEEDED IN THIS CLASS + + public: + + Owner() : _largeObject( new LargeObject() ) {} + LargeObject * getLargeObject() const { return _largeObject.get() ; } + + private: + + std::shared_ptr _largeObject ; + +} ; + +class Observer { + + // SOME CHANGES NEEDED IN THIS CLASS + + public: + + Observer( const Owner & owner ) : _largeObject(owner.getLargeObject()) {} + + void setValue( double v ) { + if (_largeObject) { _largeObject->data[0] = v ; } + else { _largeObject->data[0] = 0. ; } + } + + double getValue() const { + if (_largeObject) { return _largeObject->data[0] ; } + else { return -1. ; } + } + + private: + + LargeObject * _largeObject ; + +} ; + +void doStuff() { + + // Owners and observers + + std::vector owners(5) ; + std::vector observers ; + for ( auto & owner : owners ) { + observers.emplace_back(owner) ; + } + + // Write through observers + + for ( auto & observer : observers ) { + observer.setValue(1.) ; + } + + // Let's destroy the 2 last owners + + owners.resize(3) ; + + // Read through observers + + std::cout << "Values:"; + for ( auto const & observer : observers ) { + std::cout<<" "< -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -/* - * Please fix all memory leaks / ownership problems using smart pointers. - * (Verify by running the program with valgrind!) - * - * In the *essentials course*: - * - Work on problem1() and problem2() - * - You can have a look at problem3() if interested - * - * In the *advanced course*: - * - Work on problem1() to problem4() - * - * In main() at the bottom, comment in the different parts as you progress through the exercise. - * - * Remember that: - * - The unique ownership of data is expressed using unique_ptr. - * - "Observer" access without ownership is expressed using raw pointers, references or spans. - * - Shared ownership to data is expressed using shared_ptr. - */ - - -/* -------------------------------------------------------------------------------------------- - * 1: Always use smart pointers instead of new. - * - * A frequent source of leaks is a function that terminates in an unexpected way. - * - * - Fix the leak using a smart pointer. - * - The arguments of sumEntries() don't need to change, as it has only read access. - * -------------------------------------------------------------------------------------------- - */ - -// Note how we are using a span pointing to "double const" to ensure that the -// data can only be read. You don't need to change anything in this function. -double sumEntries(std::span range) { - // Simulate an error - throw std::invalid_argument("Error when summing over data."); - - return std::reduce(range.begin(), range.end()); -} - -// Often, data are owned by one entity, and merely used by others. In this case, we hand the data to -// sumEntries() for reading, so the ownership stays with this function. Unfortunately, something goes -// wrong and we didn't use smart pointers. -// Understand and fix the memory leak. -void doStuffWithData() { - auto data = new std::array{}; - - sumEntries(*data); - - delete data; -} - - -void problem1() { - try { - doStuffWithData(); - } catch (const std::exception& e) { - std::cerr << "problem1() terminated with exception: \"" << e.what() - << "\" Check for memory leaks.\n"; - } -} - - - -/* -------------------------------------------------------------------------------------------- - * 2: Storing unique_ptr in collections. - * - * Often, one has to store pointers to objects in collections. Fix the memory leaks using unique_ptr. - * - * Notes: - * - Factory functions should return objects either directly or using smart pointers. - * This is good practice, because it clearly shows who owns an object. Fix the return type of the factory function. - * - 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. - * Note that this works without shared_ptr! - * -------------------------------------------------------------------------------------------- - */ - -// This is a large object. We maybe shouldn't copy it, so using a pointer is advisable to pass it around. -struct LargeObject { - std::array fData; -}; - -// A factory function to create large objects. -LargeObject* createLargeObject() { - 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.fData[0] = 1.; -} - -void problem2() { - std::vector largeObjects; - - for (unsigned int i=0; i < 10; ++i) { - auto newObj = createLargeObject(); - largeObjects.push_back(newObj); - } - - for (const auto& obj : largeObjects) { - changeLargeObject(*obj); - } -} - - - -/* -------------------------------------------------------------------------------------------- - * 3: Shared ownership. - * - * Most of the time, ownership can be solved by having one owner (with 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. It leaks about 1/10 of the times - * it is invoked. - * - Verify this by running it in a loop using a command like: - * while true; do valgrind --leak-check=full --track-origins=yes ./smartPointers 2>&1 | grep -B 5 -A 5 problem3 && exit 1; done - * - Fix the ownership model using shared_ptr! - * - Convert the vectors to holding shared_ptr. - * - Fix the arguments of the functions. - * - Speed optimisation: - * Make sure that you don't create & destroy a shared_ptr in the for loop in problem3() and when calling processElement(). - * -------------------------------------------------------------------------------------------- - */ - -// This removes the element in the middle of the vector. -void removeMiddle(std::vector& collection) { - auto middlePosition = collection.begin() + collection.size()/2; - - // Must not delete element when erasing from collection, because it's also in the copy ... - collection.erase(middlePosition); -} - -// This removes a random element. -// Note that this leaks if the element happens to be the same -// that's removed above ... -void removeRandom(std::vector& collection) { - auto pos = collection.begin() + time(nullptr) % collection.size(); - - collection.erase(pos); -} - -// Do something with an element. -// Just a dummy function, for you to figure out how to pass an object -// managed by a shared_ptr to a function. -void processElement(const LargeObject* /*element*/) { } - - -// We have pointers to objects in two different collections. We work a bit with -// the collections, and then we try to terminate leak free. Without a shared ownership -// model, this becomes a mess. -void problem3() { - // Let's generate a vector with 10 pointers to LargeObject - std::vector objVector(10); - for (auto& ptr : objVector) { - ptr = new LargeObject(); - } - - // Let's copy it - std::vector objVectorCopy(objVector); - - - // Now we work with the objects: - removeMiddle(objVector); - removeRandom(objVectorCopy); - // ... - // ... - for (auto elm : objVector) { - processElement(elm); - } - - - // Now try to figure out what has to be deleted. It's a mess ... - // Fix using shared_ptr, so the following code becomes 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; - } - } -} - - - -/* -------------------------------------------------------------------------------------------- - * 4: Smart pointers as class members. - * - * Class members that are pointers can quickly become a problem. - * Firstly, if only raw pointers are used, the intended ownership is unclear. - * Secondly, it's easy to overlook that a member has to be deleted. - * Thirdly, pointer members usually require you to implement copy or move constructors and assignment - * operators (--> rule of 3, rule of 5). - * Since C++-11, one can solve a few of those problems using smart pointers. - * - * 4.1: - * The class "Owner" owns some data, but it is broken. If you copy it like in - * problem4_1(), you have two pointers pointing to the same data, but both instances think - * that they own the data. - * - * Tasks: - * - Comment in problem4_1() in main(). - * - It likely crashes. Verify this. You can also try running valgrind ./smartPointers, it should give you some hints as to - * what's happening. - * - Fix the Owner class by using a shared_ptr for its _largeObj, which we can copy as much as we want. - * - Run the fixed program. - * - Note: Once shared_ptr is in use, you can also use the default destructor. - * - * 4.2: **BONUS** - * Let's use a weak_ptr now to observe a shared_ptr. - * These are used to observe a shared_ptr, but unlike the shared_ptr, they don't prevent the deletion - * of the underlying object if all shared_ptr go out of scope. - * To *use* the observed data, one has to create a shared_ptr from the weak_ptr, so that it is guaranteed that - * the underlying object is alive. - * - * In our case, the observer class wants to observe the data of the owner, but it doesn't need to own it. - * To do this, we use a weak pointer. - * - * Tasks: - * - Comment in problem4_2() in main(). - * - Investigate the crash. Use a debugger, run in valgrind, compile with -fsanitize=address ... - * - Rewrite the interface of Owner::getData() such that the observer can see the shared_ptr to the large object. - * - Set up the Observer such that it stores a weak pointer that observes the large object. - * - In Observer::processData(), access the weak pointer, and use the data *only* if the memory is still alive. - * Note: What you need is weak_ptr::lock(). Check out the documentation and the example at the bottom: - * https://en.cppreference.com/w/cpp/memory/weak_ptr/lock - * -------------------------------------------------------------------------------------------- - */ - -class Owner { -public: - Owner() : - _largeObj(new LargeObject()) { } - - ~Owner() { - std::cout << "problem4(): Owner " << this << " is deallocating " << _largeObj << ".\n"; - delete _largeObj; - } - - const LargeObject* getData() const { - return _largeObj; - } - -private: - LargeObject* _largeObj; -}; - - -void problem4_1() { - std::vector owners; - - for (int i=0; i < 5; ++i) { - Owner owner; - owners.push_back(owner); - } - - /* Now we have a problem: - * We created Owner instances on the stack, and copied them into the vector. - * When the instances on the stack are destroyed, the memory is deallocated. - * All copies in the vector now point to the deallocated memory! - * We could fix this using copy constructors (but we don't want to copy the data), - * using move semantics or using shared_ptr. - * Here, we want to go for shared_ptr. - */ -} - - -class Observer { -public: - Observer(const Owner& owner) : - _largeObj(owner.getData()) { } - - double getValue() const { - if (_largeObj) { - return _largeObj->fData[0]; - } - - return -1.; - } - -private: - const LargeObject* _largeObj; // We don't own this. -}; - - - -void problem4_2() { - // We directly construct 5 owners inside the vector to get around problem4_1: - std::vector owners(5); - - // Let's now fill the other vector with observers: - std::vector observers; - observers.reserve(owners.size()); - for (const auto& owner : owners) { - observers.emplace_back(owner); - } - - // Now let's destroy a few of the data owners: - owners.resize(3); - - std::cout << "Values of the observers:\n\t"; - for (const auto& observer : observers) { - // Problem: We don't know if the data is alive ... - // TODO: Fix Observer! - // std::cout << observer.getValue() << " "; - } - std::cout << "\n"; -} - - -int main() { - problem1(); - // problem2(); - // problem3(); - // problem4_1(); - // problem4_2(); -} diff --git a/exercises/smartPointers/solution/problem1.sol.cpp b/exercises/smartPointers/solution/problem1.sol.cpp new file mode 100644 index 00000000..8fac5c6b --- /dev/null +++ b/exercises/smartPointers/solution/problem1.sol.cpp @@ -0,0 +1,42 @@ + +#include +#include +#include + +struct LargeObject { + + std::array 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-- ; } + +} ; + +void changeLargeObject( LargeObject & object ) { + + object.data[0] = 1. ; + throw std::invalid_argument("Error when changing object data.") ; + +} + +void doStuff() { + + auto obj = std::make_unique() ; + changeLargeObject(*obj) ; + +} + +int main() { + + try { + doStuff() ; + } catch ( const std::exception & e ) { + std::cerr<< "Terminated with exception: " << e.what() << "\n" ; + } + + std::cout<<"Leaked large objects: "< +#include +#include +#include + + +struct LargeObject { + + std::array 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-- ; } + +} ; + +std::unique_ptr newLargeObject() { + + auto object = std::make_unique() ; + // Imagine there is more setup steps of "object" here + // ... + return object ; +} + +void changeLargeObject( LargeObject & object ) { + + object.data[0] = 1. ; + +} + +void doStuff() { + + std::vector> largeObjects ; + + for ( unsigned int i = 0 ; i < 10 ; ++i ) { + largeObjects.push_back(newLargeObject()); + // ... additional largeObjects.back() setup ... + + // Alternatively, when the object is ready, + // one can "give up" newObj + // by moving it into the vector. + // auto newObj = createLargeObject() ; + // ... additional newObj setup ... + // largeObjects.push_back(std::move(newObj)); + + } + + for (const auto& obj : largeObjects) { + changeLargeObject(*obj); + } +} + +int main() { + doStuff() ; + std::cout<<"Leaked large objects: "< +#include +#include +#include +#include +#include + +struct LargeObject { + + std::array 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-- ; } + +} ; + +void removeRandom(std::vector>& collection, std::default_random_engine & engine) { + + auto pos = collection.begin() + engine() % collection.size(); + collection.erase(pos); + +} + +void changeLargeObject( LargeObject & object ) { + + object.data[0] = 1. ; + +} + +void doStuff() { + + // Prepare a non deterministic random engine + + std::random_device device ; + std::default_random_engine engine(device()) ; + + // Original collection + + std::vector> objVector(10); + for ( auto & ptr : objVector ) { + ptr = std::make_shared(); + } + + // Less copies : + // std::vector> objVector ; + // objVector.reserve(10); + // for ( unsigned int i = 0 ; i < 10 ; ++i ) { + // objVector.emplace_back(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 const & objPtr : objVector ) { + changeLargeObject(*objPtr) ; + } + +} + +int main() { + + doStuff() ; + std::cout<<"Leaked large objects: "< +#include +#include +#include + + +struct LargeObject { + + std::array data ; + +} ; + +class Owner { + + public: + + Owner() : _largeObject( new LargeObject() ) {} + LargeObject * getLargeObject() const { return _largeObject.get() ; } + + private: + + std::shared_ptr _largeObject ; + +} ; + +void doStuff() { + + std::vector owners ; + + for ( int i = 0 ; i < 5 ; ++i ) { + Owner owner ; + // ... additional owner setup ... + owners.push_back(owner) ; + } +} + +int main() { + + doStuff() ; + +} diff --git a/exercises/smartPointers/solution/problem5.sol.cpp b/exercises/smartPointers/solution/problem5.sol.cpp new file mode 100644 index 00000000..154af2fc --- /dev/null +++ b/exercises/smartPointers/solution/problem5.sol.cpp @@ -0,0 +1,83 @@ + +#include +#include +#include +#include + +struct LargeObject { + + std::array data ; + +} ; + +class Owner { + + public: + + Owner() : _largeObject( new LargeObject() ) {} + auto getLargeObject() const { return _largeObject ; } + + private: + + std::shared_ptr _largeObject ; + +} ; + +class Observer { + + public: + + Observer( const Owner & owner ) : _largeObject(owner.getLargeObject()) {} + + void setValue( double v ) { + std::shared_ptr wptr = _largeObject.lock(); + if (wptr) { wptr->data[0] = v ; } + else { wptr->data[0] = 0. ; } + } + + double getValue() const { + std::shared_ptr wptr = _largeObject.lock(); + if (wptr) { return wptr->data[0] ; } + else { return -1. ; } + } + + private: + + std::weak_ptr _largeObject ; + +} ; + +void doStuff() { + + // Owners and observers + + std::vector owners(5) ; + std::vector observers ; + for ( auto & owner : owners ) { + observers.emplace_back(owner) ; + } + + // Write through observers + + for ( auto & observer : observers ) { + observer.setValue(1.) ; + } + + // Let's destroy the 2 last owners + + owners.resize(3) ; + + // Read through observers + + std::cout << "Values:"; + for ( auto const & observer : observers ) { + std::cout<<" "< -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -/* - * Please fix all memory leaks / ownership problems using smart pointers. - * (Verify by running the program with valgrind!) - * - * In the *essentials course*: - * - Work on problem1() and problem2() - * - You can have a look at problem3() if interested - * - * In the *advanced course*: - * - Work on problem1() to problem4() - * - * In main() at the bottom, comment in the different parts as you progress through the exercise. - * - * Remember that: - * - The unique ownership of data is expressed using unique_ptr. - * - "Observer" access without ownership is expressed using raw pointers, references or spans. - * - Shared ownership to data is expressed using shared_ptr. - */ - - -/* -------------------------------------------------------------------------------------------- - * 1: Always use smart pointers instead of new. - * - * A frequent source of leaks is a function that terminates in an unexpected way. - * - * - Fix the leak using a smart pointer. - * - The arguments of sumEntries() don't need to change, as it has only read access. - * -------------------------------------------------------------------------------------------- - */ - -// Note how we are using a span pointing to "double const" to ensure that the -// data can only be read. You don't need to change anything in this function. -double sumEntries(std::span range) { - // Simulate an error - throw std::invalid_argument("Error when summing over data."); - - return std::reduce(range.begin(), range.end()); -} - -// Often, data are owned by one entity, and merely used by others. In this case, we hand the data to -// sumEntries() for reading, so the ownership stays with this function. Unfortunately, something goes -// wrong and we didn't use smart pointers. -// Understand and fix the memory leak. -void doStuffWithData() { - auto data = std::make_unique>(); - - sumEntries(*data); -} - - -void problem1() { - try { - doStuffWithData(); - } catch (const std::exception& e) { - std::cerr << "problem1() terminated with exception: \"" << e.what() - << "\" Check for memory leaks.\n"; - } -} - - - -/* -------------------------------------------------------------------------------------------- - * 2: Storing unique_ptr in collections. - * - * Often, one has to store pointers to objects in collections. Fix the memory leaks using unique_ptr. - * - * Notes: - * - Factory functions should return objects either directly or using smart pointers. - * This is good practice, because it clearly shows who owns an object. Fix the return type of the factory function. - * - 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. - * Note that this works without shared_ptr! - * -------------------------------------------------------------------------------------------- - */ - -// This is a large object. We maybe shouldn't copy it, so using a pointer is advisable to pass it around. -struct LargeObject { - std::array fData; -}; - -// A factory function to create large objects. -std::unique_ptr createLargeObject() { - auto object = std::make_unique(); - // 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.fData[0] = 1.; -} - -void problem2() { - std::vector> largeObjects; - - for (unsigned int i=0; i < 10; ++i) { - auto newObj = createLargeObject(); - largeObjects.push_back(std::move(newObj)); // Can only have one copy, so need to "give up" newObj by moving it into the vector. - - // Alternatively: - // largeObject.push_back(createLargeObject()); - } - - for (const auto& obj : largeObjects) { - changeLargeObject(*obj); - } -} - - - -/* -------------------------------------------------------------------------------------------- - * 3: Shared ownership. - * - * Most of the time, ownership can be solved by having one owner (with 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. It leaks about 1/10 of the times - * it is invoked. - * - Verify this by running it in a loop using a command like: - * while true; do valgrind --leak-check=full --track-origins=yes ./smartPointers 2>&1 | grep -B 5 -A 5 problem3 && exit 1; done - * - Fix the ownership model using shared_ptr! - * - Convert the vectors to holding shared_ptr. - * - Fix the arguments of the functions. - * - Speed optimisation: - * Make sure that you don't create & destroy a shared_ptr in the for loop in problem3() and when calling processElement(). - * -------------------------------------------------------------------------------------------- - */ - -// This removes the element in the middle of the vector. -void removeMiddle(std::vector>& collection) { - auto middlePosition = collection.begin() + collection.size()/2; - - // Must not delete element when erasing from collection, because it's also in the copy ... - collection.erase(middlePosition); -} - -// This removes a random element. -// Note that this leaks if the element happens to be the same -// that's removed above ... -void removeRandom(std::vector>& collection) { - auto pos = collection.begin() + time(nullptr) % collection.size(); - - collection.erase(pos); -} - -// Do something with an element. -// Just a dummy function, for you to figure out how to pass an object -// managed by a shared_ptr to a function. -void processElement(const LargeObject* /*element*/) { } - - -// We have pointers to objects in two different collections. We work a bit with -// the collections, and then we try to terminate leak free. Without a shared ownership -// model, this becomes a mess. -void problem3() { - // Let's generate a vector with 10 pointers to LargeObject - std::vector> objVector; - objVector.reserve(10); - for (unsigned int i = 0; i < 10; i++) { - objVector.emplace_back(new LargeObject()); - } - - // Let's copy it - std::vector> objVectorCopy(objVector); - - - // Now we work with the objects: - removeMiddle(objVector); - removeRandom(objVectorCopy); - // ... - // ... - for (const auto& elm : objVector) { - processElement(elm.get()); - } - - // Destruction happens automatically! -} - - - -/* -------------------------------------------------------------------------------------------- - * 4: Smart pointers as class members. - * - * Class members that are pointers can quickly become a problem. - * Firstly, if only raw pointers are used, the intended ownership is unclear. - * Secondly, it's easy to overlook that a member has to be deleted. - * Thirdly, pointer members usually require you to implement copy or move constructors and assignment - * operators (--> rule of 3, rule of 5). - * Since C++-11, one can solve a few of those problems using smart pointers. - * - * 4.1: - * The class "Owner" owns some data, but it is broken. If you copy it like in - * problem4_1(), you have two pointers pointing to the same data, but both instances think - * that they own the data. - * - * Tasks: - * - Comment in problem4_1() in main(). - * - It likely crashes. Verify this. You can also try running valgrind ./smartPointers, it should give you some hints as to - * what's happening. - * - Fix the Owner class by using a shared_ptr for its _largeObj, which we can copy as much as we want. - * - Run the fixed program. - * - Note: Once shared_ptr is in use, you can also use the default destructor. - * - * 4.2: **BONUS** - * Let's use a weak_ptr now to observe a shared_ptr. - * These are used to observe a shared_ptr, but unlike the shared_ptr, they don't prevent the deletion - * of the underlying object if all shared_ptr go out of scope. - * To *use* the observed data, one has to create a shared_ptr from the weak_ptr, so that it is guaranteed that - * the underlying object is alive. - * - * In our case, the observer class wants to observe the data of the owner, but it doesn't need to own it. - * To do this, we use a weak pointer. - * - * Tasks: - * - Comment in problem4_2() in main(). - * - Investigate the crash. Use a debugger, run in valgrind, compile with -fsanitize=address ... - * - Rewrite the interface of Owner::getData() such that the observer can see the shared_ptr to the large object. - * - Set up the Observer such that it stores a weak pointer that observes the large object. - * - In Observer::processData(), access the weak pointer, and use the data *only* if the memory is still alive. - * Note: What you need is weak_ptr::lock(). Check out the documentation and the example at the bottom: - * https://en.cppreference.com/w/cpp/memory/weak_ptr/lock - * -------------------------------------------------------------------------------------------- - */ - -class Owner { -public: - Owner() : - _largeObj(new LargeObject()) { } - - std::shared_ptr getData() const { - return _largeObj; - } - -private: - std::shared_ptr _largeObj; -}; - - -void problem4_1() { - std::vector owners; - - for (int i=0; i < 5; ++i) { - Owner owner; - owners.push_back(owner); - } - - // No problem now. Every object is deallocated only once. -} - - -class Observer { -public: - Observer(const Owner& owner) : - _largeObj(owner.getData()) { } - - double getValue() const { - if (auto data = _largeObj.lock()) { - return data->fData[0]; - } - - return -1.; - } - -private: - std::weak_ptr _largeObj; // We don't own this. -}; - - - -void problem4_2() { - // We directly construct 5 owners inside the vector to get around problem4_1: - std::vector owners(5); - - // Let's now fill the other vector with observers: - std::vector observers; - observers.reserve(owners.size()); - for (const auto& owner : owners) { - observers.emplace_back(owner); - } - - // Now let's destroy a few of the data owners: - owners.resize(3); - - std::cout << "Values of the observers:\n\t"; - for (const auto& observer : observers) { - std::cout << observer.getValue() << " "; - } - std::cout << "\n"; -} - - -int main() { - problem1(); - problem2(); - problem3(); - problem4_1(); - problem4_2(); -}