CS374: Programming Language Principles - Modern Language Features
Activity Goals
The goals of this activity are:- To utilize modern language features for working with memory
- To explain the concept of scope
- To explain the purpose of object references
- To time and compare different architectural approaches
The Activity
Directions
Consider the activity models and answer the questions provided. First reflect on these questions on your own briefly, before discussing and comparing your thoughts with your group. Appoint one member of your group to discuss your findings with the class, and the rest of the group should help that member prepare their response. Answer each question individually from the activity, and compare with your group to prepare for our whole-class discussion. After class, think about the questions in the reflective prompt and respond to those individually in your notebook. Report out on areas of disagreement or items for which you and your group identified alternative approaches. Write down and report out questions you encountered along the way for group discussion.Model 1: C++ Unique Pointers
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | #include <iostream> #include <memory> // Resource class with additional fields class Resource { public : int data; std::string name; Resource( int d, std::string n) : data(d), name(n) { std::cout << "Resource acquired: " << name << "\n" ; } ~Resource() { std::cout << "Resource destroyed: " << name << "\n" ; } }; // Function that manipulates the resource via a reference to the unique_ptr void update_resource(std::unique_ptr<Resource> &res) { std::cout << "Manipulating resource: " << res->name << "\n" ; res->data += 10; // Modify the data field std::cout << "Updated data: " << res->data << "\n" ; res->name = "Updated " + res->name; // Modify the name field std::cout << "Updated name: " << res->name << "\n" ; } int main() { // Create a unique_ptr to a Resource object std::unique_ptr<Resource> resPtr = std::make_unique<Resource>(42, "TestResource" ); // Pass the unique_ptr by reference to a function that manipulates it // if you'd like resPtr to become deallocated and null after this call, you // can pass std::move(resPtr) instead, and not pass by reference; this // transfers ownership of the resource to the function update_resource(resPtr); // The resource can still be accessed in main std::cout << "Resource in main after update:\n" ; std::cout << "Name: " << resPtr->name << ", Data: " << resPtr->data << "\n" ; return 0; } |
Questions
- What does this code do?
- What does this code have in common with traditional pointers? What would you have to do differently if you were creating the pointers yourself?
- What do you think
std::move
does? - What benefits does
std::unique_ptr
provide?
Model 2: C++ Shared Pointers
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | #include <iostream> #include <memory> // Resource class with additional fields class Resource { public : int data; std::string name; Resource( int d, std::string n) : data(d), name(n) { std::cout << "Resource acquired: " << name << "\n" ; } ~Resource() { std::cout << "Resource destroyed: " << name << "\n" ; } }; // Function that takes a shared_ptr and updates the resource void update_resource_data(std::shared_ptr<Resource> res, int value) { std::cout << "Updating resource data in function 1...\n" ; res->data += value; // Modify the data field std::cout << "Data after update: " << res->data << "\n" ; } // Function that takes a shared_ptr and modifies the name void update_resource_name(std::shared_ptr<Resource> res, const std::string &newName) { std::cout << "Updating resource name in function 2...\n" ; res->name = newName; // Modify the name field std::cout << "Name after update: " << res->name << "\n" ; } int main() { std::shared_ptr<Resource> resPtr1; { // Block scope to show shared_ptr behavior std::shared_ptr<Resource> resPtr2 = std::make_shared<Resource>(42, "SharedResource" ); resPtr1 = resPtr2; // Now resPtr1 shares ownership with resPtr2 // Pass resPtr2 to a function that updates the resource data update_resource_data(resPtr2, 10); // Block ends here, but resource is still alive because resPtr1 also owns it } // resPtr1 is still valid, even though resPtr2 has gone out of scope update_resource_name(resPtr1, "UpdatedResourceName" ); // Print the final state of the resource in main std::cout << "Final Resource state in main:\n" ; std::cout << "Name: " << resPtr1->name << ", Data: " << resPtr1->data << "\n" ; return 0; } |
Questions
- What is the purpose of the extra curly braces inside
main
? - When
resPtr1
goes out of scope, why is the underlying memory still accessible? - In your own words, what is the benefit of a
std::shared_ptr
?
Model 3: Vectors and the C++ Standard Template Library
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | #include <iostream> #include <memory> #include <vector> class Widget { public : Widget( int id) : id(id) { std::cout << "Widget " << id << " created.\n" ; } ~Widget() { std::cout << "Widget " << id << " destroyed.\n" ; } void display() const { std::cout << "Widget ID: " << id << "\n" ; } private : int id; }; int main() { // Create a vector to store shared_ptrs to Widgets std::vector<std::shared_ptr<Widget>> widgets; // Adding new Widgets to the vector using shared_ptr widgets.push_back(std::make_shared<Widget>(1)); widgets.push_back(std::make_shared<Widget>(2)); widgets.push_back(std::make_shared<Widget>(3)); // Displaying all Widgets std::cout << "Displaying Widgets:\n" ; for ( const auto &widgetPtr : widgets) { widgetPtr->display(); } // Simulate shared ownership by copying a shared_ptr from the vector std::shared_ptr<Widget> anotherWidgetPtr = widgets[1]; std::cout << "Removing the second Widget from the vector.\n" ; widgets.erase(widgets.begin() + 1); // Despite removing from the vector, the Widget object is still alive std::cout << "Widget in anotherWidgetPtr is still alive:\n" ; anotherWidgetPtr->display(); std::cout << "Remaining Widgets in the vector:\n" ; for ( const auto &widgetPtr : widgets) { widgetPtr->display(); } // When main exits, all remaining Widgets will be destroyed as their // shared_ptr reference counts drop to zero. return 0; } |
Questions
- What do you think the
auto
type means? How do you think it works? - Why is
anotherWidgetPtr
still accessible after removing it from thevector
?
Model 4: C++ Constant Expressions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | #include <chrono> #include <iostream> // note: set -fconstexpr-steps= in CXXFLAGS to set the number of constexpr // compile steps, and -O0 to disable other compiler optimizations // Regular function to calculate binomial coefficient unsigned long long binomial_coeff_non_constexpr( int n, int k) { return (k == 0 || k == n) ? 1 : binomial_coeff_non_constexpr(n - 1, k - 1) + binomial_coeff_non_constexpr(n - 1, k); } // Constexpr function to calculate binomial coefficient constexpr unsigned long long binomial_coeff_constexpr( int n, int k) { return (k == 0 || k == n) ? 1 : binomial_coeff_constexpr(n - 1, k - 1) + binomial_coeff_constexpr(n - 1, k); } int main() { int n = 25; int k = 15; constexpr int n_constexpr = 25; constexpr int k_constexpr = 15; // Timing non-constexpr version std::cout << "Testing non-constexpr version...\n" ; auto start_non_constexpr = std::chrono::high_resolution_clock::now(); unsigned long long result_non_constexpr = binomial_coeff_non_constexpr(n, k); auto end_non_constexpr = std::chrono::high_resolution_clock::now(); std::chrono::duration< double > elapsed_non_constexpr = end_non_constexpr - start_non_constexpr; std::cout << "Binomial Coefficient (non-constexpr) of " << n << " choose " << k << " is " << result_non_constexpr << "\n" ; std::cout << "Time taken: " << elapsed_non_constexpr.count() << " seconds\n" ; // Displaying constexpr result std::cout << "\nDisplaying constexpr result...\n" ; constexpr unsigned long long result_constexpr = binomial_coeff_constexpr( n_constexpr, k_constexpr); // Computed at compile-time std::cout << "Binomial Coefficient (constexpr) of " << n << " choose " << k << " is " << result_constexpr << "\n" ; std::cout << "Time taken: 0.0 seconds (computed at compile-time)\n" ; return 0; } |
Questions
- Try this code for various values of
n
andk
. Which program is faster? - What do you notice when compiling this program? Why do you think this is? What is the compromise?
- What, in your own words, does
constexpr
do? - What do you think the compiler flag
constexpr-steps
means?
Smart Pointer Examples: Before and After
Example 1: Using unique_ptr
instead of Raw Pointers
Before: Raw Pointers | After: unique_ptr |
---|---|
|
|
Example 2: Using shared_ptr
instead of Raw Pointers
Before: Raw Pointers | After: shared_ptr |
---|---|
|
|