CS374: Programming Language Principles - Modern Language Features

Activity Goals

The goals of this activity are:
  1. To utilize modern language features for working with memory
  2. To explain the concept of scope
  3. To explain the purpose of object references
  4. 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

  1. What does this code do?
  2. What does this code have in common with traditional pointers? What would you have to do differently if you were creating the pointers yourself?
  3. What do you think std::move does?
  4. 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

  1. What is the purpose of the extra curly braces inside main?
  2. When resPtr1 goes out of scope, why is the underlying memory still accessible?
  3. 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

  1. What do you think the auto type means? How do you think it works?
  2. Why is anotherWidgetPtr still accessible after removing it from the vector?

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

  1. Try this code for various values of n and k. Which program is faster?
  2. What do you notice when compiling this program? Why do you think this is? What is the compromise?
  3. What, in your own words, does constexpr do?
  4. 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
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
    void say_hello() const { std::cout << "Hello from Resource\n"; }
};

void raw_pointer_example() {
    Resource* res = new Resource();
    res->say_hello();

    // Simulate transferring ownership (forgetting to delete might cause a memory leak)
    Resource* res2 = res;
    res = nullptr;

    // Resource cleanup
    delete res2; // If we forget this, it causes a memory leak.
}

int main() {
    raw_pointer_example();
    return 0;
}
#include <iostream>
#include <memory>  // For std::unique_ptr

class Resource {
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
    void say_hello() const { std::cout << "Hello from Resource\n"; }
};

void unique_pointer_example() {
    std::unique_ptr<Resource> res = std::make_unique<Resource>();
    res->say_hello();

    // Transferring ownership with std::move
    std::unique_ptr<Resource> res2 = std::move(res);

    // No need to manually delete, as unique_ptr handles this automatically
    // Resource will be destroyed when res2 goes out of scope
}

int main() {
    unique_pointer_example();
    return 0;
}

Example 2: Using shared_ptr instead of Raw Pointers

Before: Raw Pointers After: shared_ptr
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
    void say_hello() const { std::cout << "Hello from Resource\n"; }
};

void raw_pointer_shared_example() {
    Resource* res = new Resource();

    // Multiple "owners" of the same resource
    Resource* res1 = res;
    Resource* res2 = res;

    res1->say_hello();
    res2->say_hello();

    // Manually managing cleanup
    delete res1;  // Deleting res1 here makes res2 a dangling pointer
    // delete res2;  // If uncommented, this would cause undefined behavior (double delete)
}

int main() {
    raw_pointer_shared_example();
    return 0;
}
#include <iostream>
#include <memory>  // For std::shared_ptr

class Resource {
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
    void say_hello() const { std::cout << "Hello from Resource\n"; }
};

void shared_pointer_example() {
    std::shared_ptr<Resource> res = std::make_shared<Resource>();

    // Multiple shared_ptr instances share ownership of the same resource
    std::shared_ptr<Resource> res1 = res;
    std::shared_ptr<Resource> res2 = res;

    res1->say_hello();
    res2->say_hello();

    // No need to manually delete; the resource is automatically deleted
    // when the last shared_ptr (res, res1, or res2) goes out of scope
}

int main() {
    shared_pointer_example();
    return 0;
}

Submission

I encourage you to submit your answers to the questions (and ask your own questions!) using the Class Activity Questions discussion board. You may also respond to questions or comments made by others, or ask follow-up questions there. Answer any reflective prompt questions in the Reflective Journal section of your OneNote Classroom personal section. You can find the link to the class notebook on the syllabus.