MTRN2500

Week 3 Lab01

WEDNESDAY 4PM - 6PM

THURSDAY 4PM - 6PM
 

Slides by Alvin Cherk (z5311001)

Info

These slides are based upon the lab document on Teams or on GitLab.com

Today

  • Storage duration.
  • Dynamic memory allocation.
  • Dangers of raw pointers.
  • Smart pointers (unique and shared).
  • make_unique and make_shared
  • Passing-by-pointer
  • References.
  • Passing-by-reference.

Heap vs Stack Memory

Array Memory Layout

Array Memory Layout

An array is a linear data structure that stores a collection of similar data types at contiguous locations in a computer's memory

Array Memory Layout

The address of std::array is the same as the address of its first element.

#include <array>
#include <iostream>

int main() {
    std::array<int, 4> arr{1, 2, 3, 4};
    std::cout << "Address: " << &arr << " == " << arr.data() << std::endl;
    // Address: 0x7ffcf7562b70 == 0x7ffcf7562b70
}

Array Memory Layout

The elements of std::array are contiguous

#include <array>
#include <iostream>

int main() {
    std::array<int, 4> arr{1, 2, 3, 4};
    for (std::size_t i{0}; i < arr.size(); i++) {
        std::cout << i << ": " << &arr.at(i) << std::endl;
    }
    // Actual values differs from each run
    // 0: 0x7ffdc616b400
    // 1: 0x7ffdc616b404
    // 2: 0x7ffdc616b408
    // 3: 0x7ffdc616b40c
}

Vector Memory Layout

Vector

Dynamically sized array that doubles in size when it reaches capacity.

It is one of the most common and most used STL container.

You can probably do everything in a vector, but that doesn't mean you should

int main() {
    // Creates a vector of integers using an initialiser list
    std::vector<int> a = {0, 1, 2, 3, 4};

    // Creates a vector of length 3, with all value defaulted at 2. Using constructor
    std::vector<int> b(3, 2);

    // Vector with {1, 2} as elements. Using constructor
    std::vector<int> c{1, 2};

    // Creating a copy. Using constructor
    std::vector<int> d = a;

    // Assignment {2, 2, 2}
    c = b;
}

When declaring a std::vector, a size is not needed, only the type.

How am I supposed to understand all the constructors or member functions/methods?

Read the C++ documentation on cppreference.com

 

cppreference generally better than cplusplus

Vector Memory Layout

The address of std::vector is not the same address of its first element.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec{1, 2, 3, 4};
    std::cout << "Address: " << &vec << " != " << vec.data() << std::endl;
    // Address: 0x7ffea4df2110 != 0x55e7531c8eb0
}

Vector Memory Layout

The elements of std::vector are contiguous

int main() {
    std::vector<int> vec{1, 2, 3, 4};
    std::cout << vec.capacity() << std::endl;
    for (std::size_t i{0}; i < vec.size(); i++) {
        std::cout << i << ": " << vec.data() + i << std::endl;
    }
    // 0: 0x55ba8abe3eb0
    // 1: 0x55ba8abe3eb4
    // 2: 0x55ba8abe3eb8
    // 3: 0x55ba8abe3ebc
}

Vector Memory Layout

How is a std::vector represented in memory

        --- --- --- ---
Stack  | b | e | c | a |
        --- --- --- ---
         |   |   |
         |   --------
         |           |
         v           v
        --- --- --- ---
Heap   | 1 | 2 | 3 |   |
        --- --- --- ---

The vector itself will be allocated on the stack i.e.:

  • b: Beginning address of the vector.
  • e: One-past-the-end address of the vector.
  • c: Capacity of the vector.
  • a: Allocator class (how the vector allocates its elements in memory).

Vector Fun Fact

std::vector stores 1 type differently

How many bits/bytes are needed to store a single boolean?

1 bit. 0 => false, 1 => true

But the smallest data we can store is 1 byte (like an ascii character)

Not course content (I think)

Storage Duration

also known as Object Lifetimes

Storage Duration

Three different types of storage durations in C++

  1. Static: Object persists throughout program life time
  2. Automatic: Object persists within scope life time
  3. Dynamic: Object life time defined by us
int variable_1{10};

int main() {
    int* variable_2{new int};
    delete variable_2;

    {
        int variable_3{3};
    }
}
  • variable_1: Static
  • variable_2: Dynamic
  • variable_3: Automatic

Dynamic Memory Allocation

Dynamic Memory Allocation

What are the disadvantages of using C-style dynamic memory allocation?

#include <iostream>

int main() {
    int* int_ptr{(int*)malloc(sizeof(int))};
    *int_ptr = 3;
    std::cout << *int_ptr << std::endl;
    free(int_ptr);
}
  • malloc is verbose and requires the user to give the number of bytes to allocate.
  • malloc needs to be type-cast to the assigned pointer.
  • Pointer is not initialised upon creation.

Code Demo

 cpp_memory_allocation.cpp 

Dynamic Memory Allocation

Convert the program you created to use C++-style dynamic memory allocation i.e. new and delete.

What are the advantages?

#include <iostream>

int main() {
    int* int_ptr{new int{42}};
    std::cout << *int_ptr << std::endl;
    delete int_ptr;
}
  • new just requires the datatype and does not require the number of bytes to allocate.
  • new returns an int* directly so there is no need to type-cast.
  • The pointer can be initialised upon creation. If we don't want to initialise the pointer then:
int* int_ptr{new int};  // int_ptr is a nullptr.
#include <iostream>

int main() {
    int* int_ptr{(int*)malloc(sizeof(int))};
    *int_ptr = 3;
    std::cout << *int_ptr << std::endl;
    free(int_ptr);
}

Pointers are Dangerous

Pointers are Dangerous

Why are pointers dangerous?

Wild Pointers: Pointers used before initialisation

int *ptr;
std::cout << *ptr << std::endl;  // Using uninitialised pointer.

Dangling Pointers: Pointers whose memory that it is pointing to has been freed.

int *ptr1{new int{4}};
int *ptr2{ptr1};
delete ptr1;  // ptr2 becomes a dangling pointer.
delete ptr2;  // This line results in double free.

Memory Leaks: Pointers that are never freed so memory usage increases over time.

int *ptr{new int};
// do ptr stuff // Forget to free.

References

References

Why are pointers bad?

They are dangerous. They can cause runtime errors if you are not careful.

Solution?

C++ introduces references (&) that sort-of replaces pointers.

A reference is an alias for another object. You can use it just as you would the original object.

Similar to a pointer, but:

  • Don't need to use -> to access elements
  • Can't be null
  • Can't change what a variable is referenced to after initialisation

References

#include <iostream>

auto swap(int* x, int* y) -> void {
    int temp = *x;
    *x = *y;
    *y = temp;
}

auto main() -> int {
    int x = 100;
    int y = 0;
    std::cout << "Before (" << x << ", " << y << ")\n";
    // Before (100, 0)

    swap(&x, &y);
    std::cout << "After (" << x << ", " << y << ")\n";
    // After (0, 100)

    return 0;
}

Simple swap() function that swaps the values passed in the arguments using pointers

References

#include <iostream>

auto swap(int& x, int& y) -> void {
    int temp = x;
    x = y;
    y = temp;
}

auto main() -> int {
    int x = 100;
    int y = 0;
    std::cout << "Before (" << x << ", " << y << ")\n";
    // Before (100, 0)

    swap(x, y);
    std::cout << "After (" << x << ", " << y << ")\n";
    // After (0, 100)

    return 0;
}

Simple swap() function that swaps the values passed in the arguments using references

References

#include <iostream>

int main() {
    int a{3};
    int& b{a};
    std::cout << a << " " << b << std::endl;
    a = 4;
    std::cout << a << " " << b << std::endl;
}

What is the output of the following program?

3 3
4 4

References

References can also be dangerous (*if you do dumb things)

References can dangle (i.e. the resource is destroyed while the reference is valid). Attempting to access a dangling reference is undefined behaviour which may work, may give some unexpected value, or may crash.

#include <iostream>

int& foo() {
    int var{42};
    return var;
}

int main() {
    int& var_2{foo()};
    // This is undefined behaviour.
    std::cout << var_2 << std::endl;
}
  • In foo(), var is an automatic managed variable (at the end of the scope, its deleted)
  • As such, foo() is returning a reference to a deleted variable

References

Pass by Value / Reference

Pass by Value

By default in C++, all arguments passed into functions are copied into the parameters.

auto swap_value(int x, int y) -> void {
    auto const tmp = x;
    x = y;
    y = tmp;
}

auto main() -> int {
    auto i = 1;
    auto j = 2;
    std::cout << i << ' ' << j << '\n';
    // 1 2
    swap_value(i, j);
    std::cout << i << ' ' << j << '\n';
    // 1 2 (not swapped)
}

The actual value of the argument is copied into the memory being used to hold the formal parameters value during the function call/execution

Pass by Reference

Sometimes, we don't want to copy the variable (as it may not be copyable, or it is expensive to copy it). The solution is to use references.

auto swap_reference(int& x, int& y) -> void {
    auto const tmp = x;
    x = y;
    y = tmp;
}

auto main() -> int {
    auto i = 1;
    auto j = 2;
    std::cout << i << ' ' << j << '\n';
    // 1 2
    swap_reference(i, j);
    std::cout << i << ' ' << j << '\n';
    // 2 1 (swapped)
}

Code Demo

pass_by_references.cpp

Code Demo

What is the output?

#include <iostream>

void foo(int& arg1, int arg2) {
    arg1++;
    arg2++;
}

int main() {
    int a{3};
    int& b{a};
    int c{4};
    std::cout << a << " " << b << " " << c << std::endl;
    foo(a, b);
    std::cout << a << " " << b << " " << c << std::endl;
    foo(c, a);
    std::cout << a << " " << b << " " << c << std::endl;
}

3 3 4
4 4 4
4 4 5

Loops Revisited

Loops Revisited

So what are some actual usages of references in C++?

auto loop() -> void {
    auto numbers = std::vector<int>{1, 2, 3, 4, 5};
    for (auto number : numbers) {
        number = 0;
    }

    for (auto number : numbers) {
        std::cout << number << std::endl;
    }
    // {1, 2, 3, 4, 5}
}

No you can't, as the value in the array is copied into a local variable number. Any changes to this variable is not reflected in the actual vector itself

What if I want to use a for-range loop and I want to change the values in a vector?

Loops Revisited

What if I want to use a for-range loop and I want to change the values in a vector?

References can be used as they are an alias for the actual value in the vector

auto loop_4() -> void {
    auto numbers = std::vector<int>{1, 2, 3, 4, 5};
    for (auto& number : numbers) {
        number = 0;
    }

    for (auto const& number : numbers) {
        std::cout << number << std::endl;
    }
    // {0, 0, 0, 0, 0}
}

Loops Revisited

I have a 2D vector and I am looping through it.

auto loop_5() -> void {
    auto matrix = std::vector<std::vector<int>>{{1, 2, 3, 4, 5}, {0, 0, 0, 0, 0}};
    for (auto row : matrix) {
        //
    }
}

Since the for-range loop copies the elements of the vector into the row variable, we are effectively creating redundant copies.

This is a performance and memory issue.

We can use references to "point" to the vector elements instead of copying them.

auto loop_6() -> void {
    auto matrix = std::vector<std::vector<int>>{{1, 2, 3, 4, 5}, {0, 0, 0, 0, 0}};
    for (auto& row : matrix) {
        // row is a std::vector<int> reference that can be changed
    }

    for (auto const& row : matrix) {
        // row is a const std::vector<int> reference that cannot be changed
    }
}

Smart Pointers

Smart Pointers

Class wrapper of un-named heap objects (i.e., raw pointer) in named stack objects so that object lifetimes can be managed much easier (makes them automatic)

#include <memory>

int main() {
    { // ptr will only exist within these braces 
      // and will not require manual deallocation.
        std::unique_ptr<int> ptr{new int{3}};
    }
    // There are no memory leaks since ptr will free int(3).
}

Smart Pointers

There are 3 types of smart pointers:

  1. Unique pointers
  2. Shared pointers
  3. Weak pointers
Type Shared Ownership Take Ownership
std::unique_ptr<T> No Yes
raw pointer No No
std::shared_ptr<T> Yes Yes
std::weak_ptr<T> No No

Unique Pointer

  • Unique pointers owns the object
  • When the unique pointer is destructed, the underlying object is destructed too
  • No additional/very tiny overhead compared to raw pointer
  • Has no copy constructor/assignment
  • make_unique<T>() is safe for creating temporary variables and assigning them to a unique_ptr

Unique Pointer

#include <iostream>
#include <memory>

int main() {
    // 1 - Worst - you can accidentally own the resource multiple
    // times, or easily forget to own it.
    auto* silly_string = new std::string{"Hi"};
    auto up1 = std::unique_ptr<std::string>(silly_string);
    auto up11 = std::unique_ptr<std::string>(silly_string);

    // 2 - Not good - requires actual thinking about whether there's a leak.
    auto up2 = std::unique_ptr<std::string>(new std::string("Hello"));

    // 3 - Good - no thinking required.
    auto up3 = std::make_unique<std::string>("Hello");
}

Shared Pointer

  • Shared pointers allow several pointers to share ownership over an object
  • When a shared pointer is destructed, if its the only shared pointer left pointing at the object, then the object is destroyed 
#include <iostream>
#include <memory>

int main() {
    auto x = std::make_shared<int>(5);
    auto y = x; // Using copy constructor
    std::cout << "use count: " << x.use_count() << "\n"; // 2
    std::cout << "value: " << *x << "\n"; // 5
    x.reset(); // Memory still exists, due to y.
    std::cout << "use count: " << y.use_count() << "\n"; // 1
    std::cout << "value: " << *y << "\n"; // 5
    y.reset(); // Deletes the memory, since
    // no one else owns the memory
    std::cout << "use count: " << x.use_count() << "\n"; // 0
    // std::cout << "value: " << *y << "\n";
}

Weak Pointer

  • Weak pointers are used when:
    • You don't want to add to the reference count
    • You want to be able to check if the underlying data is valid before using it
#include <iostream>
#include <memory>

int main() {
    auto x = std::make_shared<int>(1);
    auto wp = std::weak_ptr<int>(x); // x owns the memory
    auto y = wp.lock(); // Creates a shared_ptr
    if (y != nullptr) { // x and y own the memory
        // Do something with y
        std::cout << "Attempt 1: " << *y << '\n';
    }
}

Extra info, not in course.

Weak Pointer

Weak pointers are also used to avoid circular references which can result in memory leaks.

Extra info, not in course.

Smart Pointers

When to use which?

  • Almost always want to use a unique pointer over shared
  • Use a shared pointer when:
    • An object has multiple owners, and you don't know which one will stay around the longest

Code Demo

Convert to Smart Pointers

Code Demo

Convert the following program so that raw pointers are replaced with smart pointers (prefer std::unique_ptr over std::shared_ptr when possible). The following program may or may not be correct.

int main() {
    int* ptr1{new int{1}};
    int* ptr2{new int{2}};
    int* ptr3{ptr2};
    int var{5};
    int* ptr4{new int{var}};
    int* ptr5{new int{var}};
    delete ptr1;
    delete ptr2;
    delete ptr3;
    delete ptr4;
    delete ptr5;
}
int main() {
    int* ptr1{new int{1}};
    int* ptr2{new int{2}};
    int* ptr3{ptr2};
    int var{5};
    int* ptr4{new int{var}};
    int* ptr5{new int{var}};
    delete ptr1;
    delete ptr2;
    delete ptr3;
    delete ptr4;
    delete ptr5;
}
#include <memory>

int main() {
    std::unique_ptr<int> ptr1{new int{1}};
    std::shared_ptr<int> ptr2{new int{2}};
    std::shared_ptr<int> ptr3{ptr2};
    int var{5};
    std::unique_ptr<int> ptr4{new int{var}};
    std::unique_ptr<int> ptr5{new int{var}};
}
  • ptr1 is a std::unique_ptr<int>.
  • ptr2 and ptr3 are std::shared_ptr<int> because ptr3 points to prt2's resource.
  • ptr4 and ptr5 are std::unique_ptr because memory is allocated for var for both pointers.
  • delete is not used since the deletion of the resource given to smart pointer is managed automatically.

Code Demo

Convert to smart pointers 2

Code Demo

Convert the following program so that it uses smart pointers instead of raw pointers. 

#include <cstdlib>
#include <memory>

void foo(int* ptr) {
    return;
}

int main() {
    int* ptr{(int*)malloc(sizeof(int))};
    *ptr = 42;
    foo(ptr);
    free(ptr);
}

Code Demo

Convert the following program so that it uses smart pointers instead of raw pointers. 

#include <cstdlib>
#include <memory>

void foo(int* ptr) {
    return;
}

int main() {
    int* ptr{(int*)malloc(sizeof(int))};
    *ptr = 42;
    foo(ptr);
    free(ptr);
}
#include <memory>

void foo(std::shared_ptr<int>& ptr) {
    return;
}

int main() {
    auto ptr{std::make_shared<int>(42)};
    foo(ptr);
}

Note the reference in foo(...), we don't want to create a copy of the shared_ptr

Code Demo

Challenge: Implement a Shared Pointer

Code Demo

Implement a shared pointer to an int in a class called SharedPointer. The specification for SharedPointer is given below:

Spec link here

or look at the pdf on teams

Code Demo

#ifndef SHARED_POINTER_HPP
#define SHARED_POINTER_HPP

class SharedPointer {
  public:
    SharedPointer();

    SharedPointer(int* pointer);
    ~SharedPointer();

    int* get();
    int const* get() const;
    int& operator*();
    int const& operator*() const;
    long use_count() const;

  private:
    int* pointer_;
};

#endif // SHARED_POINTER_HPP

Note the const correctness

Code Demo

int main() {
    auto* ptr = new int{42}; // Malloc the memory for an integer with a value of 42
    auto sp_1 = SharedPointer(ptr); // Create an instance
    std::cout << "count: " << sp_1.use_count() << std::endl;
    auto sp_2 = SharedPointer{ptr};
    std::cout << "count: " << sp_1.use_count() << std::endl;

    {
        auto const sp_3 = SharedPointer{ptr};
        std::cout << "count: " << sp_1.use_count() << std::endl;
    } // sp_3 gets deleted

    std::cout << "count: " << sp_1.use_count() << std::endl;

    auto const sp_4 = SharedPointer{ptr};
    auto sp_4_ptr = sp_4.get(); // Gets me the pointer as a const

    std::cout << sp_4_ptr << " vs " << ptr << std::endl;

    auto& test2 = *sp_1; // Gets me a reference of the integer
    std::cout << test2 << std::endl;
    test2 = 100; // Change the value using the reference
    std::cout << *(sp_1.get()) << std::endl; // Get the address of the integer and deference to
                                             // print value

    auto const& test3 = *sp_4; // Get a const reference of integer
    std::cout << test3 << std::endl;
}

Feedback

MTRN2500 Week 3 Lab 1 22T3

By kuroson

MTRN2500 Week 3 Lab 1 22T3

  • 378