LMU ☀️ CMSI 3801
LANGUAGES AND AUTOMATA I
HOMEWORK #5 PARTIAL ANSWERS

Code

C

#include "string_stack.h"

#include <math.h>
#include <stdlib.h>
#include <string.h>

const int INITIAL_CAPACITY = 16;

struct _Stack {
    char** elements; // array of strings
    int capacity; // can grow and shrink
    int top; // index of next slot to fill, also the size
};

stack_response create() {
    stack s = malloc(sizeof(struct _Stack));
    if (s == nullptr) {
        return (stack_response) { out_of_memory, nullptr };
    }
    s->capacity = INITIAL_CAPACITY;
    s->top = 0;
    s->elements = malloc(INITIAL_CAPACITY * sizeof(char*));
    if (s->elements == nullptr) {
        return (stack_response) { out_of_memory, nullptr };
    }
    return (stack_response) { success, s };
}

int size(const stack s) {
    return s->top;
}

bool is_empty(const stack s) {
    return s->top == 0;
}

bool is_full(const stack s) {
    return s->top == MAX_CAPACITY;
}

response_code push(stack s, char* item) {
    if (is_full(s)) {
        return stack_full;
    }

    // The validation that the element is not too big should use the
    // awesome strnlen function. Not too many folks know about strnlen,
    // but it is important that we use it for efficiency!
    size_t item_length = strnlen(item, MAX_ELEMENT_BYTE_SIZE + 1);
    if (item_length > MAX_ELEMENT_BYTE_SIZE) {
        return stack_element_too_large;
    }

    if (s->top == s->capacity) {
        // Double the capacity, but don't exceed the maximum allowed. This
        // has to be done with a realloc and updates to the stack fields.
        int new_capacity = s->capacity * 2;
        if (new_capacity > MAX_CAPACITY) {
            new_capacity = MAX_CAPACITY;
        }
        char** new_elements = realloc(s->elements, new_capacity * sizeof(char*));
        if (new_elements == nullptr) {
            return out_of_memory;
        }
        s->elements = new_elements;
        s->capacity = new_capacity;
    }

    // Defensive copy! The stack is going to own a new copy of the
    // string, so that the caller can not manipulate the stack contents
    // through a back door. It is safe to use strdup here since we
    // previously checked its length. Because the stack itself is
    // taking ownership, it has to free the string when it is popped
    // or the stack is destroyed.
    s->elements[s->top++] = strdup(item);
    return success;
}

string_response pop(stack s) {
    if (is_empty(s)) {
        return (string_response) { stack_empty, nullptr };
    }

    // We can return the existing pointer (a move) to the string element
    // as long as we "unhook" it from the stack. This is more efficient
    // than making a copy, and it is also secure because the response object
    // is now the sole owner of the string (and will be responsible for
    // freeing it).
    char* popped_value = s->elements[--s->top];
    s->elements[s->top] = nullptr;

    if (s->top < s->capacity / 4) {
        // Shrink the array, but don't go below the initial capacity
        int new_capacity = s->capacity / 2;
        if (new_capacity < INITIAL_CAPACITY) {
            new_capacity = INITIAL_CAPACITY;
        }
        char** new_elements = realloc(s->elements, new_capacity * sizeof(char*));
        if (new_elements == nullptr) {
            return (string_response) { out_of_memory, nullptr };
        }
        s->elements = new_elements;
        s->capacity = new_capacity;
    }

    return (string_response) { success, popped_value };
}

void destroy(stack* s) {
    // Because the stack owns the strings, it has to free them before
    // freeing the array of pointers and the stack itself.
    for (int i = 0; i < (*s)->top; i++) {
        free((*s)->elements[i]);
    }
    free((*s)->elements);
    free(*s);

    // The caller should not use the stack after it has been destroyed,
    // so let's prevent dangling pointers with the usual set-to-nullptr.
    *s = nullptr;
}

C++

// A class for an expandable stack. There is already a stack class in the
// Standard C++ Library; this class serves as an exercise for students to
// learn the mechanics of building generic, expandable, data structures
// from scratch with smart pointers.

#include <stdexcept>
#include <string>
#include <memory>
using namespace std;

// A stack object wraps a low-level array indexed from 0 to capacity-1 where
// the bottommost element (if it exists) will be in slot 0. The member top is
// the index of the slot above the top element, i.e., the next available slot
// that an element can go into. Therefore if top==0 the stack is empty and
// if top==capacity it needs to be expanded before pushing another element.
// However for security there is still a super maximum capacity that cannot
// be exceeded.

// Restriction: The type T must have a default constructor, copy constructor,
// and copy assignment operator. This is because the stack will be copying
// elements around in the array, and it needs to know how to do so.

#define MAX_CAPACITY 32768
#define INITIAL_CAPACITY 16

template <typename T>
class Stack {
  // Allocate the data array with a smart pointer, so no destructor needed
  unique_ptr<T[]> elements;
  int capacity;
  int top;

  // Prohibit copying and assignment
  Stack(Stack&) = delete;
  Stack& operator=(Stack&) = delete;

public:
  Stack():
    top(0),
    elements(make_unique<T[]>(INITIAL_CAPACITY)),
    capacity(INITIAL_CAPACITY) {
  }

  int size() {return top;}

  bool is_empty() {return top == 0;}

  bool is_full() {return top == MAX_CAPACITY;}

  void push(T item) {
    if (top == MAX_CAPACITY) {
      throw overflow_error("Stack has reached maximum capacity");
    }
    if (top == capacity) {
     // No more room, expand if we can
     reallocate(capacity * 2);
    }
    // Copy the item into the next available slot and increment top
    elements[top++] = item;
  }

  T pop() {
    if (is_empty()) {
      throw underflow_error("cannot pop from empty stack");
    }
    if (top < capacity / 4) {
      // Too much empty space, contract if we can
      reallocate(capacity / 2);
    }
    T poppedValue = elements[--top];
    // For security, blank out the cell holding the popped value so that the
    // old data does not hang around. "Blanking out" in this case just means
    // assigning the default value of the type T to the cell. The stack is not
    // holding pointers, so there is no need to delete the popped value.
    elements[top] = T();
    // Again, assume that the type T has a proper copy constructor and copy
    // assignment operator.
    return poppedValue;
  }

private:
  void reallocate(int new_capacity) {
      // Prevent capacity from going below initial or above maximum.
      capacity = min(max(new_capacity, INITIAL_CAPACITY), MAX_CAPACITY);

      // Allocate a new array with the new capacity.
      unique_ptr<T[]> new_elements = make_unique<T[]>(capacity);

      // Copy the elements in the old array to the new array.
      std::copy(elements.get(), elements.get() + top, new_elements.get());

      // The stack needs to point to the new array. The old array will be
      // automatically deleted when reassigned. But we can't just invoke
      // elements = new_elements; because elements is a unique_ptr, so we have to
      // use the move() function to transfer ownership of the new array
      // to the stack.
      elements = std::move(new_elements);
  }
};

Rust

pub struct Stack<T> {
    // stack items are private by default
    items: Vec<T>,
}

impl<T> Stack<T> {
    pub fn new() -> Self {
        Stack { items: Vec::new() }
    }

    pub fn push(&mut self, item: T) {
        self.items.push(item);
    }

    pub fn pop(&mut self) -> Option<T> {
        self.items.pop()
    }

    pub fn peek(&self) -> Option<&T> {
        self.items.last()
    }

    pub fn is_empty(&self) -> bool {
        self.items.is_empty()
    }

    pub fn len(&self) -> usize {
        self.items.len()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_push_and_pop() {
        let mut stack: Stack<i32> = Stack::new();
        assert!(stack.is_empty());
        stack.push(1);
        stack.push(2);
        assert_eq!(stack.len(), 2);
        assert_eq!(stack.pop(), Some(2));
        assert_eq!(stack.pop(), Some(1));
        assert_eq!(stack.pop(), None);
        assert!(stack.is_empty());
    }

    #[test]
    fn test_peek() {
        let mut stack: Stack<i32> = Stack::new();
        assert_eq!(stack.peek(), None);
        stack.push(3);
        assert_eq!(stack.peek(), Some(&3));
        stack.push(5);
        assert_eq!(stack.peek(), Some(&5));
    }

    #[test]
    fn test_is_empty() {
        let mut stack: Stack<String> = Stack::new();
        assert!(stack.is_empty());
        stack.push(String::from("hello"));
        assert!(!stack.is_empty());
        stack.pop();
        assert!(stack.is_empty());
    }

    #[test]
    fn test_stacks_cannot_be_cloned_or_copied() {
        let stack1: Stack<i32> = Stack::new();
        let _stack2: Stack<i32> = stack1;
        // Should get a compile error if next line uncommented
        // let _stack3: Stack<i32> = stack1; // Error: `stack1` has been moved
    }
}

Exercises

  1. Explain the meaning of each of the following C declarations.
    1. double *a[n];
    2. double (*b)[n];
    3. double (*c[n])();
    4. double (*d())[n];

    Answer: (a) an array of $n$ pointers to double; (b) a pointer to an array of $n$ doubles; (c) an array of $n$ pointers to functions with any number of args returning double; (d) a function of any number of args returning a pointer to an array of $n$ doubles.

  2. In C, when exactly do arrays decay to pointers?

    Answer: When arrays are passed to functions or assigned to pointers (in other words, when used in pointer contexts).

  3. Give a short description, under 10 words each, of the following, as they are understood in the context of the C language: (a) memory leak, (b) dangling pointer, (c) double free, (d) buffer overflow, (e) stack overflow, (f) wild pointer.

    Answer:

    1. failure to free memory
    2. use of a pointer to freed memory
    3. freeing same memory twice
    4. writing beyond allocated memory
    5. exceeding call stack size
    6. uninitialized pointer
  4. Explain why C++ move constructors and move assignment operators only make sense on r-values and not l-values. You can use a rough code fragment in your explanation.

    Answer: Moving transfers resources from temporary (r-value) objects, which are about to be destroyed, to new objects. L-values have persistent identities and should not have their resources stolen.

  5. Why does C++ even have moves, anyway?

    Answer: Moves improve performance by enabling resource transfer instead of (possible very expensive) copying.

  6. What is the rule-of-5 in C++?

    Answer: There are a few ways to state this, but the one I like the best is: For any class that desires move semantics, all of the following five must be defined: destructor, copy constructor, copy assignment operator, move constructor, move assignment operator.

  7. What are the three ownership rules of Rust?

    Answer: (1) Each value has exactly one owner (2) A value can only have one owner at a time (3) When the owner goes out of scope, the value is dropped (which means any associated memory is freed).

  8. What are the three borrowing rules of Rust?

    Answer: (1) At any given time, you can have either one mutable reference or any number of immutable references (2) References must always be valid, that is, they must refer to a value in memory (3) References cannot outlive the data they refer to.