6
\$\begingroup\$

Recently I wanted to use the gtest library to unit test my program but for some reason, it's incredibly hard to make it work and configure (at least for me it was), so I decided to make my own, following the style Google uses.

I set a goal for this summer and it is to make a Linked List library similar to std::list. I think I am getting there slowly and making some good progress on the way. So far I've implemented about 20 methods and there is more to come. I decided to use the TEST class to test it and it worked well I think.

  • Currently, I am using bubble sort to sort my list which runs in O(n^2), so an implementation of an O(nlogn) is on the todo list
  • I am also using recursion in some places which will probably gobble up all my space for larger inputs. I am working on an iterative solution and comparing results to see if it's even worth it... (recursive code looks cleaner IMHO)
  • I am also missing iterators to traverse the list but when I thought about implementing them I saw they exceed my knowledge of C++ (for now)
  • No restrictions, this is a personal project

I would like to know where I can improve my code and make it faster for larger input. I would also like to know if there are some major design flaws I should be aware of before proceeding, some code is repetitive but that's because I don't fully understand how std::function() works.

Is there a better way to write Makefiles? I have been CmdC + CmdV the same exact one since I started working with classes in C++. (not a joke)

I built the project in XCode but I also wrote a MakeFile so it should compile. For me, it compiles with [compiler]: Apple LLVM 14.0.3 (clang-1403.0.22.14.1)

P.S.:

  • I know std::list is a doubly linked list...mine is not and I am planning on making one later on, that's why I used a namespace to distinguish them. Please let me know if this is not the right approach and if some other OOP method would be a better fit. I eventually want to avoid namespace pollution since most of the methods would have the same name. Perhaps making all of the methods in one class static? (doesn’t sound productive but could it work?)

  • There are some #include "___.cpp" which might come as cringe to some more experienced programmers but I had some linking issues and kept getting an assembly error, including .cpp instead of .h fixed my issue. I am aware that grabbing and pasting the whole code base is considered unpractical.

Edit: I see my question hasn't been answered for a while, is it perhaps not specific enough? I set a bounty hoping it would help. main.cpp

//
//  main.cpp
//  linked_list_xcode
//
//  Created by Jakob Balkovec on 13/06/2023.
//

#include <iostream>
#include "single_l.h"
#include "test.h"

// {
// Un-comment test macro to run all tests
// }
// #define TEST

// {
// Un-comment example macro to run example
// }
// #define EXAMPLE


// {
//  {
//  Simple LL problem implemented in the example function
//  }
// Problem Name: Merge Two Sorted Lists

// You are given two sorted linked lists, list1 and list2, where each list is sorted 
// in non-decreasing order. Your task is to merge these two lists into a single 
// sorted linked list and return the head of the merged list.

// Example:
// Input:
// list1: 1 -> 2 -> 3
// list2: 4 -> 5 -> 6
// Output: 1 -> 2 -> 3 -> 4 -> 5 -> 6

// Write a function mergeSortedLists that takes two sorted linked lists list1 and list2 
// as parameters and returns the new merged list.

// You can assume the following:

// The linked lists are non-empty.
// The nodes of each list contain data of type T.
// The lists are already sorted in non-decreasing order.
// Note: You should not modify the original lists; instead, 
// you should create a new list with the merged elements.
// }

/** {
 * Displays information about the last compilation of the file.
 * @return void
    } **/
static inline void show_last_compiled() {
  std::cout << "\n[file]: " << __FILE__ << '\n'
    << "[compiled on]: " << __DATE__ << " at " << __TIME__ << '\n'
    << "[compiler]: " << __VERSION__ << '\n'
    << "[timestamp]: " << __TIMESTAMP__ << '\n';
}

/**
 * The main function of the C++ program. It runs either the tests or the example
 * depending on the macros defined. If no macros are defined, it prints a message
 * to the console. 
 *
 * @param argc the number of command line arguments
 * @param argv an array of the command line argument strings
 *
 * @return integer value EXIT_SUCCESS (0) upon successful execution
 *
 * @throws None
 */
int main(int argc, const char * argv[]) {
    show_last_compiled();
    #ifdef TEST
    bool all_passed = run_tests();
    if(all_passed) 
        std::cout << "\n\n[all tests passed]\n\n";
    else
        std::cout << "\n\n[some tests failed, check and see which]\n\n"
    
    #endif
    
    #ifdef EXAMPLE
    run_example();
    #endif
        
    #ifndef TEST
    #ifndef EXAMPLE
        std::cout << "\n[Un-comment a macro to run]\n\n";
    #endif
    #endif
    return EXIT_SUCCESS;
}

single_l.cpp

//
//  single_l.cpp
//  linked_list_xcode
//
//  Created by Jakob Balkovec on 13/06/2023.
//

#include <iostream>
#include <unordered_set>
#include <stdexcept>
#include "single_l.h"

namespace single {
  /**
   * Creates a new single linked list
   * @throws None
   */
  template <class T>
  single_l<T>::single_l() : head(nullptr) {}

  /**
   * Creates a new single linked list with the elements from the
   * specified initializer list.
   * @param initList  the initializer list containing the elements to add
   * @throws None
   */
  template <class T>
  single_l<T>::single_l(std::initializer_list<T> initList)
    : head(nullptr) {
    for (const T& value : initList) {
      push_back(value);
    }
  }
  /**
   * Destructor for the single_l class.
   * @param None
   * @return None
   * @throws None
   */
  template <class T>
  single_l<T>::~single_l() {
    clear();
  }

  /**
 * Constructs a new single linked list by copying all elements of another
 * linked list.
 * @param other The linked list to copy from.
 * @throws None
 */
  template <class T>
  single_l<T>::single_l(const single_l& other) : head(nullptr) {
    const single_l<T>::node* current = other.head;
    while (current != nullptr) {
      push_back(current->data_);
      current = current->next_;
    }
  }

  /**
   * Constructs a single linked list by moving the contents of another single
   * linked list, setting its head pointer to null afterwards.
   * @param other the single linked list to move from
   * @return none
   * @throws none
   */
  template <class T>
  single_l<T>::single_l(single_l&& other) : head(nullptr) {
    head = other.head;
    other.head = nullptr;
  }

  /**
   * @brief Assignment operator for the single_l class.
   * @tparam T The type of elements in the list.
   * @param other The list to be assigned.
   * @return Reference to the modified list.
   */
  template <class T>
  single_l<T>& single_l<T>::operator=(const single_l& other) {
    if (this != &other) {
      clear();

      const single_l<T>::node* current = other.head;
      while (current != nullptr) {
        push_back(current->data_);
        current = current->next_;
      }
    }
    return *this;
  }
  /**
   * @brief Move assignment operator for the single_l class.
   * @tparam T The type of elements in the list.
   * @param other The list to be moved.
   * @return Reference to the modified list.
   */
  template <class T>
  single_l<T>& single_l<T>::operator=(single_l&& other) {
    if (this != &other) {
      clear();

      head = other.head;
      other.head = nullptr;
    }
    return *this;
  }

  /**
   * Adds a new node with the given data to the end of the linked list.
   * @param data the data to be added to the linked list
   * @return void
   * @throws none
   */
  template <class T>
  void single_l<T>::push_back(const T& data) {
    single_l<T>::node* new_node = new single_l<T>::node{ data, nullptr };

    if (head == nullptr) {
      head = new_node;
    }
    else {
      single_l<T>::node* current = head;
      while (current->next_ != nullptr) {
        current = current->next_;
      }
      current->next_ = new_node;
    }
    return;
  }

  /**
   * Adds a new node to the front of the linked list.
   * @param data The data to be stored in the new node.
   * @return void.
   * @throws None.
   */
  template <class T>
  void single_l<T>::push_front(const T& data) {
    single_l<T>::node* new_node = new single_l<T>::node{ data, head };
    head = new_node;
    return;
  }

  /**
   * Removes the last element of the single linked list. Throws a runtime error
   * if the list is empty.
   * @throws std::runtime_error [pop_back()]  [list is empty]
   */
  template <class T>
  void single_l<T>::pop_back() {
    try {
      if (head == nullptr) {
        throw std::runtime_error("[pop_back()]  [list is empty]\n");
      }
      else if (head->next_ == nullptr) {
        delete head;
        head = nullptr;
      }
      else {
        single_l<T>::node* current = head;
        while (current->next_->next_ != nullptr) {
          current = current->next_;
        }
        delete current->next_;
        current->next_ = nullptr;
      }
    }
    catch (const std::exception& e) {
      std::cerr << "{error}: " << e.what() << std::endl;
    }
    return;
  }

  /**
   * Pops the first element from the list.
   * @param None
   * @return None
   * @throws std::runtime_error if the list is empty
   */
  template <class T>
  void single_l<T>::pop_front() {
    try {
      if (head == nullptr) {
        throw std::runtime_error("[pop_front()]  [list is empty]\n");
      }
      single_l<T>::node* temp = head;
      head = head->next_;
      delete temp;
    }
    catch (const std::exception& e) {
      std::cerr << "{error}: " << e.what() << std::endl;
    }
    return;
  }

  /**
   * Recursively prints the linked list in reverse order, starting from the given node.
   * @param current pointer to the current node being processed
   * @return void
   * @throws none
   */
  template <class T>
  void single_l<T>::print_reverse_helper(single_l<T>::node* current) {
    if (current == nullptr) {
      return;
    }
    print_reverse_helper(current->next_);
    std::cout << current->data_ << " ";
  }

  /**
   * Recursively prints the data of each node in a single linked list, starting from
   * the given node.
   * @param current the node to start printing from
   * @return void
   */
  template <class T>
  void single_l<T>::print_helper(single_l<T>::node* current) {
    if (current == nullptr) {
      return;
    }
    std::cout << current->data_ << " ";
    print_helper(current->next_);
  }

  /**
   * Prints the contents of the list in reverse order.
   * @throws std::runtime_error if the list is empty
   */
  template <class T>
  void single_l<T>::print_reverse() {
    try {
      if (head == nullptr) {
        throw std::runtime_error("[print_reverse()]  [list is empty]");
      }
      std::cout << "{ ";
      print_reverse_helper(head);
      std::cout << "}";
      std::cout << std::endl;
    }
    catch (const std::exception& e) {
      std::cerr << "{error}: " << e.what() << std::endl;
    }
  }

  /**
   * Prints the elements of the single linked list.
   * @throws std::runtime_error if the list is empty.
   */
  template <class T>
  void single_l<T>::print() {
    try {
      if (head == nullptr) {
        throw std::runtime_error("[print()]  [list is empty]");
      }
      std::cout << "{ ";
      print_helper(head);
      std::cout << "}";
      std::cout << std::endl;
    }
    catch (const std::exception& e) {
      std::cerr << "{error}: " << e.what() << std::endl;
    }
  }

  /**
   * Clears the linked list by deleting all nodes. Starts from the head of the
   * linked list and deletes each node until the list is empty.
   * @param None
   * @return None
   * @throws None
   */
  template <class T>
  void single_l<T>::clear() {
    while (head != nullptr) {
      single_l<T>::node* temp = head;
      head = head->next_;
      delete temp;
    }
  }

  /**
   * Swaps the head of this list with another list's head.
   * @param other The other list to swap heads with.
   * @throws None
   */
  template <class T>
  void single_l<T>::swap(single_l& other) {
    std::swap(head, other.head);
  }

  /**
   * Determines whether the single_l<T> object is empty.
   * @return true if the object is empty, false otherwise
   */
  template <class T>
  bool single_l<T>::empty() {
    return (head == nullptr) ? true : false;
  }

  /**
   * Returns the number of nodes in the linked list.
   * @param None
   * @return size_t The number of nodes in the linked list.
   * @throws None
   */
  template <class T>
  size_t single_l<T>::size() {
    size_t count = 0;
    single_l<T>::node* current = head;
    while (current != nullptr) {
      count++;
      current = current->next_;
    }
    return count;
  }

  /**
   * @brief Returns a reference to the first element in the list.
   * @tparam T The type of elements in the list.
   * @return Reference to the first element.
   * @throw std::runtime_error If the list is empty.
   */
  template <class T>
  T& single_l<T>::front() {
    try {
      if (head == nullptr) {
        throw std::runtime_error("[front()]  [list is empty]");
      }
      return head->data_;
    }
    catch (const std::exception& e) {
      std::cerr << "{error}: " << e.what() << std::endl;
    }
    static T default_value;
    return default_value;
  }

  /**
   * @brief Returns a reference to the last element in the list.
   * @tparam T The type of elements in the list.
   * @return Reference to the last element.
   * @throw std::runtime_error If the list is empty.
   */
  template <class T>
  T& single_l<T>::back() {
    try {
      if (head == nullptr) {
        throw std::runtime_error("[back()]  [list is empty]");
      }
      single_l<T>::node* current = head;
      while (current->next_ != nullptr) {
        current = current->next_;
      }
      return current->data_;
    }
    catch (const std::exception& e) {
      std::cerr << "{error}: " << e.what() << std::endl;
    }
    static T default_value;
    return default_value;
  }

  /**
   * @brief Returns a const reference to the first element in the list.
   * @tparam T The type of elements in the list.
   * @return Const reference to the first element.
   * @throw std::runtime_error If the list is empty.
   */
  template <class T>
  const T& single_l<T>::front() const {
    try {
      if (head == nullptr) {
        throw std::runtime_error("[front() cosnt]  [list is empty]");
      }
      return head->data_;
    }
    catch (const std::exception& e) {
      std::cerr << "{error}: " << e.what() << std::endl;
    }
    static T default_value;
    return default_value;
  }

  /**
   * @brief Returns a const reference to the last element in the list.
   * @tparam T The type of elements in the list.
   * @return Const reference to the last element.
   * @throw std::runtime_error If the list is empty.
   */
  template <class T>
  const T& single_l<T>::back() const {
    try {
      if (head == nullptr) {
        throw std::runtime_error("[back() cosnt]  [list is empty]");
      }
      single_l<T>::node* current = head;
      while (current->next_ != nullptr) {
        current = current->next_;
      }
      return current->data_;
    }
    catch (const std::exception& e) {
      std::cerr << "{error}: " << e.what() << std::endl;
    }
    static T default_value;
    return default_value;
  }

  /**
   * @brief Accesses the element at the specified index in the list.
   * @tparam T The type of elements in the list.
   * @param index The index of the element to access.
   * @return Reference to the element at the specified index.
   * @throw std::runtime_error If the list is empty.
   * @throw std::out_of_range If the index is out of range.
   */
  template <class T>
  T& single_l<T>::operator[](size_t index) {
    try {
      if (head == nullptr) {
        throw std::runtime_error("[operator[]]  [list is empty]");
      }

      single_l<T>::node* current = head;
      size_t count = 0;
      while (current != nullptr) {
        if (count == index) {
          return current->data_;
        }
        current = current->next_;
        count++;
      }

      throw std::out_of_range("[operator[]]  [index out of range]");
    }
    catch (const std::exception& e) {
      std::cerr << "{error}: " << e.what() << std::endl;
    }
    static T default_value;
    return default_value;
  }

  /**
   * @brief Accesses the element at the specified index in the list (const version).
   * @tparam T The type of elements in the list.
   * @param index The index of the element to access.
   * @return Const reference to the element at the specified index.
   * @throw std::runtime_error If the list is empty.
   * @throw std::out_of_range If the index is out of range.
   */
  template <class T>
  const T& single_l<T>::operator[](size_t index) const {
    try {
      if (head == nullptr) {
        throw std::runtime_error("[operator[] const]  [list is empty]");
      }

      single_l<T>::node* current = head;
      size_t count = 0;
      while (current != nullptr) {
        if (count == index) {
          return current->data_;
        }
        current = current->next_;
        count++;
      }

      throw std::out_of_range("[operator[] const]  [index out of range]");
    }
    catch (const std::exception& e) {
      std::cerr << "{error}: " << e.what() << std::endl;
    }
  }

  /**
   * @brief Accesses the element at the specified index in the list.
   * @tparam T The type of elements in the list.
   * @param index The index of the element to access.
   * @return Reference to the element at the specified index.
   * @throw std::runtime_error If the list is empty.
   * @throw std::out_of_range If the index is out of range.
   */
  template <class T>
  T& single_l<T>::at(size_t index) {
    try {
      if (head == nullptr) {
        throw std::runtime_error("[at()]  [list is empty]");
      }

      single_l<T>::node* current = head;
      size_t count = 0;
      while (current != nullptr) {
        if (count == index) {
          return current->data_;
        }
        current = current->next_;
        count++;
      }

      throw std::out_of_range("[at()]  [index out of range]");
    }
    catch (const std::exception& e) {
      std::cerr << "{error}: " << e.what() << std::endl;
    }
    static T default_value;
    return default_value;
  }

  /**
   * @brief Deletes the element at the specified index in the list.
   * @tparam T The type of elements in the list.
   * @param index The index of the element to delete.
   * @throw std::runtime_error If the list is empty.
   * @throw std::out_of_range If the index is out of range.
   */
  template <class T>
  void single_l<T>::delete_at(size_t index) {
    try {
      if (head == nullptr) {
        throw std::runtime_error("[delete_at()]  [list is empty]");
      }

      if (index == 0) {
        pop_front();
        return;
      }

      single_l<T>::node* current = head;
      single_l<T>::node* previous = nullptr;
      size_t count = 0;
      while (current != nullptr) {
        if (count == index) {
          if (previous != nullptr) {
            previous->next_ = current->next_;
          }
          else {
            head = current->next_;
          }
          delete current;
          return;
        }
        previous = current;
        current = current->next_;
        count++;
      }

      throw std::out_of_range("[delete_at()]  [index out of range]");
    }
    catch (const std::exception& e) {
      std::cerr << "{error}: " << e.what() << std::endl;
      throw; // rethrow the exception to propagate it
    }
  }

  /**
   * @brief Deletes the first occurrence of the specified value in the list.
   * @tparam T The type of elements in the list.
   * @param data The value to delete.
   * @throw std::runtime_error If the list is empty or the value is not found.
   */
  template <class T>
  void single_l<T>::delete_value(const T& data) {
    try {
      if (head == nullptr) {
        throw std::runtime_error("[delete_value()]  [list is empty]");
      }

      single_l<T>::node* current = head;
      single_l<T>::node* previous = nullptr;

      while (current != nullptr) {
        if (current->data_ == data) {
          if (previous == nullptr) {
            head = current->next_;
          }
          else {
            previous->next_ = current->next_;
          }
          delete current;
          return;
        }

        previous = current;
        current = current->next_;
      }

      throw std::runtime_error("[delete_value()]  [value not found]");
    }
    catch (const std::exception& e) {
      std::cerr << "{error}: " << e.what() << std::endl;
      throw; // rethrow the exception to propagate it
    }
  }

  /**
   * Searches for the given data element in the single linked list and returns
   * the index of the first occurrence. If the element is not found, it returns
   * -1.
   * @param data  The element to search in the linked list.
   *
   * @return The index of the first occurrence of the element in the linked list.
   *         If the element is not found, it returns -1.
   * @throws None
   */
  template <class T>
  size_t single_l<T>::search(const T& data) {
    size_t index = 0;
    single_l<T>::node* current = head;
    while (current != nullptr) {
      if (current->data_ == data) {
        return index;
      }
      current = current->next_;
      index++;
    }
    return static_cast<size_t>(-1); // return -1 if value not found for clarity
  }

  /**
   * Inserts a new node with given data at the specified position in the linked list.
   * @param position_data the data to be inserted at the specified position
   * @param position the position at which the new node is to be inserted
   * @throws std::runtime_error if the list is empty
   * @throws std::out_of_range if the position is out of range
   */
  template <class T>
  void single_l<T>::at_position(const T& position_data, size_t position) {
    try {
      if (head == nullptr) {
        throw std::runtime_error("[at_position()]  [list is empty]");
      }

      single_l<T>::node* current = head;
      single_l<T>::node* previous = nullptr;
      size_t count = 0;

      while (current != nullptr) {
        if (count == position) {
          single_l<T>::node* new_node = new single_l<T>::node;
          new_node->data_ = position_data;
          new_node->next_ = current;

          if (previous == nullptr) {
            head = new_node;
          }
          else {
            previous->next_ = new_node;
          }

          return;
        }

        previous = current;
        current = current->next_;
        count++;
      }

      throw std::out_of_range("[at_position()]  [position out of range]");
    }
    catch (const std::exception& e) {
      std::cerr << "{error}: " << e.what() << std::endl;
      throw; // rethrow the exception to propagate it
    }
  }

  /**
   * Reverses the order of the nodes in a singly linked list.
   * @param none
   * @return void
   * @throws none
   */
  template <class T>
  void single_l<T>::reverse() {
    if (head == nullptr || head->next_ == nullptr) {
      return;
    }

    single_l<T>::node* current = head;
    single_l<T>::node* previous = nullptr;
    single_l<T>::node* next = nullptr;

    while (current != nullptr) {
      next = current->next_;
      current->next_ = previous;
      previous = current;
      current = next;
    }

    head = previous;
  }

  /**
   * Sorts the linked list in ascending order using bubble sort algorithm.
   * @param None.
   * @return None.
   * @throws None.
   */
  template <class T>
  void single::single_l<T>::sort() {
    if (head == nullptr || head->next_ == nullptr) {
      return;
    }

    bool swapped;
    single_l<T>::node* current;
    single_l<T>::node* last = nullptr;

    do {
      swapped = false;
      current = head;

      while (current->next_ != last) {
        if (current->data_ > current->next_->data_) {
          std::swap(current->data_, current->next_->data_);
          swapped = true;
        }
        current = current->next_;
      }

      last = current;
    } while (swapped);
  }

  /**
   * Removes duplicates from a singly linked list.
   * @param None
   * @return None
   * @throws None
   */
  template <class T>
  void single_l<T>::remove_duplicates() {
    if (head == nullptr || head->next_ == nullptr) {
      return;
    }

    std::unordered_set<T> unique_value;
    single_l<T>::node* current = head;
    single_l<T>::node* previous = nullptr;

    while (current != nullptr) {
      if (unique_value.count(current->data_) > 0) {
        previous->next_ = current->next_;
        delete current;
        current = previous->next_;
      }
      else {
        unique_value.insert(current->data_);
        previous = current;
        current = current->next_;
      }
    }
  }

  /**
   * Resizes the list to contain n elements.
   * @param n the new size of the list
   * @return void
   * @throws None
   */
  template <class T>
  void single::single_l<T>::resize(size_t n) {
    if (n == 0) {
      clear();
    }
    else if (n > size()) {
      size_t additional_nodes = n - size();

      if (head == nullptr) {
        head = new node;
        head->next_ = nullptr;
        additional_nodes--;
      }

      node* current = head;
      while (current->next_ != nullptr) {
        current = current->next_;
      }

      for (size_t i = 0; i < additional_nodes; i++) {
        node* new_node = new node;
        new_node->next_ = nullptr;
        current->next_ = new_node;
        current = new_node;
      }
    }
    else if (n < size()) {
      node* current = head;
      node* previous = nullptr;

      for (size_t i = 0; i < n; i++) {
        previous = current;
        current = current->next_;
      }

      while (current != nullptr) {
        node* next_node = current->next_;
        delete current;
        current = next_node;
      }

      previous->next_ = nullptr;
    }
  }
} //namespace single

single_l.h

//
//  single_l.h
//  linked_list_xcode
//
//  Created by Jakob Balkovec on 13/06/2023.
//

#ifndef single_l_h
#define single_l_h

#include <iostream>


namespace single {
  template <class T>
  class single_l final {
  private:
    struct node {
      T data_;
      node* next_;
    };
    struct node* head;
    
  public:
    single_l();
    single_l(std::initializer_list<T>_init_);
    ~single_l();
    single_l(const single_l&);
    single_l(single_l&&);
    single_l& operator=(const single_l&);
    single_l& operator=(single_l&&);
    void push_back(const T&);
    void push_front(const T&);
    void pop_back();
    void pop_front();
    void print();
    void print_reverse();
    void print_helper(single_l<T>::node* current);
    void print_reverse_helper(single_l<T>::node* current);
    void clear();
    void swap(single_l&);
    bool empty();
    size_t size();
    T& front();
    T& back();
    const T& front() const;
    const T& back() const;
    T& operator[](size_t);
    const T& operator[](size_t) const;
    T& at(size_t index); //inserts at index
    void delete_at(size_t index);
    void delete_value(const T &data);
    size_t search(const T &data);
    void at_position(const T& data, size_t position); //inserts at specific spot
    void reverse();
    void sort();
    void remove_duplicates();
    void resize(size_t n);
  };
} //namespace single

#endif /* single_l_h */

test_file.h

//
//  test_file.h
//  linked_list_xcode
//
//  Created by Jakob Balkovec on 13/06/2023.
//

#ifndef test_file_h
#define test_file_h

#include <iostream>
#include <string>

template <class T>
class TEST {
private:
  /**
   * Prints a test passing message with an optional function name.
   * @param func_name an optional name of the function being tested
   * @return void
   * @throws none
   */
  static void PRINT_PASS(const std::string& func_name = "") {
    std::cout << "[TEST " << func_name << " PASSED]" << std::endl;
  }
  
  /**
   * Prints a failure message for a test, including an optional message and function name.
   * @param message the message to include in the failure message (optional)
   * @param func_name the name of the function being tested (optional)
   * @return void
   * @throws none
   */
  static void PRINT_FAIL(const std::string& message, const std::string& func_name = "") {
    std::cout << "[TEST " << func_name << " FAILED]";
    if (!message.empty()) {
      std::cout << "  " << message;
    }
    std::cout << std::endl;
  }
  
public:
  /**
   * A function that checks a boolean condition, prints a pass message if the condition is true,
   * otherwise prints a fail message with an optional custom message and function name.
   * @param condition the boolean condition to check
   * @param message an optional custom message to print in the fail message
   * @param func_name an optional function name to print in the fail message
   * @throws no exceptions thrown
   */
  static void ASSERT(bool condition, const std::string& message = "", const std::string& func_name = "") {
    if (condition) {
      PRINT_PASS(func_name);
    } else {
      PRINT_FAIL(message, func_name);
    }
  }
  
  /**
   * Compares the expected and actual values and prints a pass or fail 
   * message with an optional custom message along with the expected and actual values.
   * @tparam T the data type of the expected and actual values
   * @param expected the expected value
   * @param actual the actual value
   * @param message an optional message to print on failure
   * @param func_name an optional function name to print in the pass or fail message
   * @return void
   */
  static void ASSERT_EQUAL(const T& expected, const T& actual, const std::string& message = "", const std::string& func_name = "") {
    if (expected == actual) {
      PRINT_PASS(func_name);
    } else {
      PRINT_FAIL(message, func_name);
      std::cout << "\n[expected]: " << expected << " ->  [actual]: " << actual << std::endl;
    }
  }
  
  /**
   * Compares the given actual and expected values and prints a pass message if they are not equal.
   * Otherwise, prints a fail message and the actual and not expected values.
   * @param notExpected the expected value
   * @param actual the actual value to be compared with the expected value
   * @param message an optional message to print if the comparison fails
   * @param func_name an optional name of the function for debugging purposes
   * @throws none
   */
  static void ASSERT_NOT_EQUAL(const T& notExpected, const T& actual, const std::string& message = "", const std::string& func_name = "") {
    if (notExpected != actual) {
      PRINT_PASS(func_name);
    } else {
      PRINT_FAIL(message, func_name);
      std::cout << "\n[not expected]: " << notExpected << " ->  [actual]: " << actual << std::endl;
    }
  }
  
  /**
   * A function that asserts whether a condition is true. If the condition is true,
   * prints a pass message; otherwise, prints a fail message.
   * @param condition a boolean representing the condition to be tested
   * @param message optional message to be printed in the event of a failure
   * @param func_name optional name of the function being tested
   * @throws none
   */
  static void ASSERT_TRUE(bool condition, const std::string& message = "", const std::string& func_name = "") {
    if (condition) {
      PRINT_PASS(func_name);
    } else {
      PRINT_FAIL(message, func_name);
    }
  }
  
  /**
   * Checks if a given condition is false and prints either a pass message or a fail 
   * message with a given function name and message.
   * @param condition boolean expression to check
   * @param message optional message to print if the condition is true
   * @param func_name optional function name to print with the fail message
   * @throws none
   */
  static void ASSERT_FALSE(bool condition, const std::string& message = "", const std::string& func_name = "") {
    if (!condition) {
      PRINT_PASS(func_name);
    } else {
      PRINT_FAIL(message, func_name);
    }
  }
  
  /**
   * Runs a statement and catches any exceptions of type ExceptionType.
   * If an exception of type ExceptionType is caught, the function will print a pass message.
   * If another type of exception is caught, the function will print a fail message 
   * and the type of exception thrown.
   * If no exception is thrown, the function will print a fail message.
   * @param statement the statement to run
   * @param message an optional message to print if the statement fails
   * @param func_name an optional name of the function being tested
   *
   * @throws ExceptionType if the statement throws an exception of this type
   */
  template <typename ExceptionType>
  static void ASSERT_THROW(const std::function<void()>& statement, const std::string& message = "", const std::string& func_name = "") {
    try {
      statement();
      PRINT_FAIL(message, func_name);
    } catch (const ExceptionType&) {
      PRINT_PASS(func_name);
    } catch (...) {
      PRINT_FAIL(message, func_name);
      std::cout << "[expected exception type]: " << typeid(ExceptionType).name() << std::endl;
    }
  }
  
  /**
   * Method that takes in a std::function object, 
   * a string message, and a string function name. It attempts to execute the provided statement,
   * and if an exception of type ExceptionType is thrown, it prints a failed message and an 
   * optional error message and function name. If any other type of exception is thrown, it 
   * prints a failed message and an optional error message and function name.
   * @param statement The std::function object that is to be executed.
   * @param message Optional string error message to print alongside the failed message.
   * @param func_name Optional string function name to print alongside the failed message.
   * @throws ExceptionType If this type of exception is thrown by the statement.
   */
  template <typename ExceptionType>
  static void ASSERT_NO_THROW(const std::function<void()>& statement, const std::string& message = "", const std::string& func_name = "") {
    try {
      statement();
      PRINT_PASS(func_name);
    } catch (const ExceptionType&) {
      PRINT_FAIL(message, func_name);
      std::cout << "[unexpected exception type]: " << typeid(ExceptionType).name() << std::endl;
    } catch (...) {
      PRINT_FAIL(message, func_name);
    }
  }
  
  /**
   * Compares the given value with a given threshold and prints a pass 
   * or fail message based on the comparison.
   * @param value the value to be compared
   * @param threshold the threshold value to compare against
   * @param message an optional message to be printed in case of a fail
   * @param func_name the name of the calling function
   * @throws None
   */
  static void GREATER(const T& value, const T& threshold, const std::string& message = "", const std::string& func_name = "") {
    if (value > threshold) {
      PRINT_PASS(func_name);
    } else {
      PRINT_FAIL(message, func_name);
      std::cout << "[value]: " << value << " -> [threshold]: " << threshold << std::endl;
    }
  }
  
  /**
   * Compares the given value with the given threshold and prints a message based on the result.
   * @param value the value to compare with the threshold
   * @param threshold the threshold to compare the value with
   * @param message an optional message to print if the comparison fails
   * @param func_name an optional function name to print in the pass/fail message
   * @throws N/A
   */
  static void SMALLER(const T& value, const T& threshold, const std::string& message = "", const std::string& func_name = "") {
    if (value < threshold) {
      PRINT_PASS(func_name);
    } else {
      PRINT_FAIL(message, func_name);
      std::cout << "[value]: " << value << " -> [threshold]: " << threshold << std::endl;
    }
  }
};


#endif /* test_file_h */

test_file.cpp

//
//  test_file.cpp
//  linked_list_xcode
//
//  Created by Jakob Balkovec on 13/06/2023.
//
#include "single_l.cpp"
#include "test_file.h"
#include "test.h"
#include <string>
#include <iostream>

// {
// Follow the arrange, act, assert principle
// }

/**
 * Tests the default constructor of the 'single_l' template class using 'TEST' assertions.
 * @param None
 * @return Void
 * @throws None
 */
void test_constructor() {
  single::single_l<int> list1;
  
  TEST<bool>::ASSERT_TRUE(list1.empty(), "[list should be empty]","DEF_CONSTRUCTOR {1}");
  TEST<bool>::ASSERT_EQUAL(0, list1.size(), "[list size should be 0]", "DEF_CONSTRUCTOR {2}");
}

/**
 * This function tests the destructor of single_l<int> class. It creates a 
 * single_l<int> object, adds two elements to it and then calls the clear() 
 * method which in turn calls the destructor. It then uses the TEST macro to 
 * check that the list is empty and its size is 0.
 * @param None
 * @return None
 * @throws None
 */
void test_destructor() {
  single::single_l<int> list1;
  list1.push_back(10);
  list1.push_back(20);
  
  list1.clear(); //destructor calls clear
  
  TEST<bool>::ASSERT_TRUE(list1.empty(), "[list should be empty]", "DESTRUCTOR {1}");
  TEST<bool>::ASSERT_TRUE(list1.size() == 0, "[size should be 0]", "DESTRUCTOR {2}");
}

/**
 * Tests the copy constructor of the single_l class in the single namespace.
 * @param None
 * @return None
 * @throws None
 */
void test_copy_constructor() {
  single::single_l<int> list1;
  list1.push_back(10);
  list1.push_back(20);
  
  single::single_l<int> list2(list1);
  
  TEST<bool>::ASSERT_FALSE(list2.empty(), "[list should not be empty]", "CPY_CONSTRUCTOR {1}");
  TEST<bool>::ASSERT_EQUAL(2, list2.size(), "[list size should be 2]", "CPY_CONSTRUCTOR {2}");
  TEST<bool>::ASSERT_EQUAL(list1.front(), list2.front(), "[front elements should be equal]", "CPY_CONSTRUCTOR {3}");
  TEST<bool>::ASSERT_EQUAL(list1.back(), list2.back(), "[back elements should be equal]", "CPY_CONSTRUCTOR {4}");
}

/**
 * Tests the move constructor of a single linked list.
 * @param None
 * @return None
 * @throws None
 */
void test_move_constructor() {
  single::single_l<int> list1;
  list1.push_back(10);
  list1.push_back(20);
  
  single::single_l<int> list2(std::move(list1));
  
  TEST<bool>::ASSERT_TRUE(list1.empty(), "[source list should be empty]", "MV_CONSTRUCTOR {1}");
  TEST<bool>::ASSERT_FALSE(list2.empty(), "[destination list should not be empty]", "MV_CONSTRUCTOR {2}");
  TEST<bool>::ASSERT_EQUAL(2, list2.size(), "[list size should be 2]", "MV_CONSTRUCTOR {3}");
  TEST<bool>::ASSERT_EQUAL(10, list2.front(), "[front element should be 10]", "MV_CONSTRUCTOR {4}");
  TEST<bool>::ASSERT_EQUAL(20, list2.back(), "[back element should be 20]", "MV_CONSTRUCTOR {5}");
}

/**
 * Tests the copy assignment operator of the single_l class.
 * @param None
 * @return None
 * @throws None
 */
void test_copy_assignment_operator() {
  single::single_l<int> list1;
  list1.push_back(10);
  list1.push_back(20);
  
  single::single_l<int> list2;
  list2.push_back(30);
  
  list2 = list1;
  
  TEST<bool>::ASSERT_FALSE(list2.empty(), "[list should not be empty]", "CPY_ASSGN_OP {1}");
  TEST<bool>::ASSERT_EQUAL(2, list2.size(), "[list size should be 2]", "CPY_ASSGN_OP {2}");
  TEST<bool>::ASSERT_EQUAL(list1.front(), list2.front(), "[front elements should be equal]", "CPY_ASSGN_OP {3}");
  TEST<bool>::ASSERT_EQUAL(list1.back(), list2.back(), "[back elements should be equal]", "CPY_ASSGN_OP {4}");
}

/**
 * Test the move assignment operator of a single linked list.
 * @param None
 * @return None
 * @throws None
 */
void test_move_assignment_operator() {
  single::single_l<int> list1;
  list1.push_back(10);
  list1.push_back(20);
  
  single::single_l<int> list2;
  list2.push_back(30);
  
  list2 = std::move(list1);
  
  TEST<bool>::ASSERT_TRUE(list1.empty(), "[source list should be empty]", "MV_ASSGN_OP {1}");
  TEST<bool>::ASSERT_FALSE(list2.empty(), "[destination list should not be empty]", "MV_ASSGN_OP {2}");
  TEST<bool>::ASSERT_EQUAL(2, list2.size(), "[list size should be 2]", "MV_ASSGN_OP {3}");
  TEST<bool>::ASSERT_EQUAL(10, list2.front(), "[front element should be 10]", "MV_ASSGN_OP {4}");
  TEST<bool>::ASSERT_EQUAL(20, list2.back(), "[back element should be 20]", "MV_ASSGN_OP {5}");
}

/**
 * Tests the push_back() function of a single linked list implementation
 * @param None
 * @return None
 * @throws None
 */
void test_push_back() {
  single::single_l<int> list1;
  int values[] = {10, 20, 30, 40, 50};
  
  for (int i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
    list1.push_back(values[i]);
  }
  
  TEST<bool>::ASSERT_FALSE(list1.empty(), "[list not should be empty]", "PUSH_BACK {1}");
  TEST<bool>::ASSERT_TRUE(list1[0] == 10, "[first element should be 10]", "PUSH_BACK {2}");
  TEST<bool>::ASSERT_TRUE(list1[1] == 20, "[second element should be 20]", "PUSH_BACK {3}");
  TEST<bool>::ASSERT_TRUE(list1[2] == 30, "[third element should be 30]", "PUSH_BACK {4}");
  TEST<bool>::ASSERT_TRUE(list1[3] == 40, "[fourth element should be 40]", "PUSH_BACK {5}");
  TEST<bool>::ASSERT_TRUE(list1[4] == 50, "[fifth element should be 50]", "PUSH_BACK {6}");
  TEST<bool>::ASSERT_TRUE(list1.size() == 5, "[size should be 5]", "PUSH_BACK {7}");
}

/**
 * Tests the push_front method of the single_l list implementation.
 * @param None
 * @return None
 * @throws None
 */
void test_push_front() {
  single::single_l<int> list1;
  int values[] = {10, 20, 30, 40, 50};
  
  for (int i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
    list1.push_front(values[i]);
  }
  
  TEST<bool>::ASSERT_FALSE(list1.empty(), "[list not should be empty]", "PUSH_FRONT {1}");
  TEST<bool>::ASSERT_TRUE(list1[0] == 50, "[first element should be 50]", "PUSH_FRONT {2}");
  TEST<bool>::ASSERT_TRUE(list1[1] == 40, "[second element should be 40]", "PUSH_FRONT {3}");
  TEST<bool>::ASSERT_TRUE(list1[2] == 30, "[third element should be 30]", "PUSH_FRONT {4}");
  TEST<bool>::ASSERT_TRUE(list1[3] == 20, "[fourth element should be 20]", "PUSH_FRONT {5}");
  TEST<bool>::ASSERT_TRUE(list1[4] == 10, "[fifth element should be 10]", "PUSH_FRONT {6}");
  TEST<bool>::ASSERT_TRUE(list1.size() == 5, "[size should be 5]", "PUSH_FRONT {7}");
}

/**
 * Tests the pop_back method of a single linked list implementation by removing
 * elements from the end of the list and asserting the expected values and sizes.
 * @param none
 * @return void
 * @throws none
 */
void test_pop_back() {
  single::single_l<int> list1;
  int values[] = {10, 20, 30, 40, 50};
  
  for (int i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
    list1.push_back(values[i]);
  }
  
  list1.pop_back();
  TEST<bool>::ASSERT_EQUAL(40, list1[3], "[fourth element should be 40]", "POP_BACK {1}");
  TEST<bool>::ASSERT_FALSE(list1.size() == 5, "[size shoudl be 4]", "POP_BACK {2}");
  
  list1.pop_back();
  TEST<bool>::ASSERT_EQUAL(30, list1[2], "[fourth element should be 40]", "POP_BACK {1}");
  TEST<bool>::ASSERT_FALSE(list1.size() == 4, "[size shoudl be 4]", "POP_BACK {2}");
  
  list1.pop_back();
  list1.pop_back();
  list1.pop_back();
  TEST<bool>::ASSERT_TRUE(list1.empty(), "[list should be empty]", "POP_BACK {5}");
  TEST<bool>::ASSERT_TRUE(list1.size() == 0, "[size should be 0]", "POP_BACK {6}");
}

/**
 * Tests the pop_front() method of the single_l class by creating a list of 
 * integers and removing the first element multiple times while checking 
 * the state of the list after each removal.
 * @param None
 * @return None
 * @throws None
 */
void test_pop_front() {
  single::single_l<int> list1;
  int values[] = {10, 20, 30, 40, 50};
  
  for (int i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
    list1.push_back(values[i]);
  }
  
  list1.pop_front();
  TEST<bool>::ASSERT_FALSE(list1[0] == 10, "[first element should not be 10]", "POP_FRONT {1}");
  TEST<bool>::ASSERT_TRUE(list1[0] == 20, "[first element should be 20]", "POP_FRONT {2}");
  
  list1.pop_front();
  TEST<bool>::ASSERT_FALSE(list1[0] == 20, "[first element should not be 40]", "POP_FRONT {3}");
  TEST<bool>::ASSERT_TRUE(list1[0] == 30, "[first element should be 30]", "POP_FRONT {4}");
  
  list1.pop_front();
  list1.pop_front();
  list1.pop_front();
  TEST<bool>::ASSERT_TRUE(list1.empty(), "[list should be empty]", "POP_FRONT {5}");
  TEST<bool>::ASSERT_TRUE(list1.size() == 0, "[size should be 0]", "POP_FRONT {6}");
}

/**
 * Tests the clear() method, the method clears the content of a single linked list and checks if the size is 0 and
 * the list is empty.
 * @param None
 * @return None
 * @throws None
 */
void test_clear() {
  single::single_l<int> list1;
  int values[] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
  
  for (int i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
    list1.push_back(values[i]);
  }
  
  list1.clear();
  TEST<bool>::ASSERT_EQUAL(0, list1.size(), "[size should be 0]", "CLEAR {1}");
  TEST<bool>::ASSERT_EQUAL(true, list1.empty(), "[list should be empty]", "CLEAR {2}");
}

/**
 * Executes a test swapping two linked lists of integers. 
 * @param None
 * @return None
 * @throws None
 */
void test_swap() {
  single::single_l<int> list1;
  int values1[] = {10, 20, 30, 40, 50};
  for (int i = 0; i < sizeof(values1) / sizeof(values1[0]); i++) {
    list1.push_back(values1[i]);
  }
  TEST<bool>::ASSERT_EQUAL(10, list1[0], "[list1 first element should be 10]", "SWAP {1}");
  TEST<bool>::ASSERT_EQUAL(20, list1[1], "[lsit1 second element should be 20]", "SWAP {1}");
  
  single::single_l<int> list2;
  int values2[] = {60, 70, 80, 90, 100};
  for (int i = 0; i < sizeof(values2) / sizeof(values2[0]); i++) {
    list2.push_back(values2[i]);
  }
  TEST<bool>::ASSERT_EQUAL(60, list2[0], "[list2 first element should be 60]", "SWAP {3}");
  TEST<bool>::ASSERT_EQUAL(70, list2[1], "[list2 second element should be 70]", "SWAP {4}");
  //swaps two lists
  list1.swap(list2);
  
  TEST<bool>::ASSERT_EQUAL(60, list1[0], "[list1 first element should be 60]", "SWAP {5}");
  TEST<bool>::ASSERT_EQUAL(70, list1[1], "[list1 second element should be 70]", "SWAP {6}");
  TEST<bool>::ASSERT_EQUAL(10, list2[0], "[list2 first element should be 10]", "SWAP {7}");
  TEST<bool>::ASSERT_EQUAL(20, list2[1], "[list2 second element should be 20]", "SWAP {8}");
  
  TEST<bool>::ASSERT(list1.size() == list2.size(), "[list1 and list2 size should be equal]", "SWAP {9}");
}

/**
 * Tests if the given single linked list is empty.
 * @param None
 * @return void
 * @throws None
 */
void test_empty() {
  single::single_l<int> list1;
  int values1[] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
  for (int i = 0; i < sizeof(values1) / sizeof(values1[0]); i++) {
    list1.push_back(values1[i]);
  }
  TEST<bool>::ASSERT_FALSE(list1.empty(), "[list should not be empty]", "EMPTY {1}");
  list1.clear();
  TEST<bool>::ASSERT_TRUE(list1.empty(), "[list should be empty]", "EMPTY {2}");
}

void test_size() {
  single::single_l<int> list1;
  int values1[] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
  for (int i = 0; i < sizeof(values1) / sizeof(values1[0]); i++) {
    list1.push_back(values1[i]);
  }
  TEST<bool>::ASSERT_EQUAL(static_cast<size_t>(10), static_cast<int>(const_cast<single::single_l<int>&>(list1).size()), "[size should be 10]", "SIZE {1}");
  list1.pop_back();
  TEST<bool>::ASSERT_EQUAL(static_cast<size_t>(9), static_cast<int>(const_cast<single::single_l<int>&>(list1).size()), "[size should be 9]", "SIZE {2}");
}

/**
 * Tests whether the front element of a single linked list is correct after 
 * several push and pop operations.
 * @return void
 * @throws None
 */
void test_front() {
  single::single_l<int> list1;
  int values1[] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
  for (int i = 0; i < sizeof(values1) / sizeof(values1[0]); i++) {
    list1.push_back(values1[i]);
  }
  
  TEST<bool>::ASSERT_EQUAL(10, list1.front(), "[front should be 10]", "FRONT {1}");
  list1.pop_back();
  TEST<bool>::ASSERT_EQUAL(10, list1.front(), "[front should be 10]", "FRONT {2}");
  list1.pop_front();
  TEST<bool>::ASSERT_EQUAL(20, list1.front(), "[front should be 20]", "FRONT {3}");
  
  for(auto i = 0; i < 7; i++) {
    list1.pop_front();
  }
  TEST<bool>::ASSERT_EQUAL(90, list1.front(), "[front should be 90]", "FRONT {4}");
}

/**
 * Tests the back() method of the single_l<int> class.
 * @param None
 * @return None
 * @throws None
 */
void test_back() {
  single::single_l<int> list1;
  int values1[] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
  for (int i = 0; i < sizeof(values1) / sizeof(values1[0]); i++) {
    list1.push_back(values1[i]);
  }
  
  TEST<bool>::ASSERT_EQUAL(100, list1.back(), "[back should be 100]", "BACK {1}");
  list1.pop_front();
  TEST<bool>::ASSERT_EQUAL(100, list1.back(), "[back should be 100]", "BACK {2}");
  list1.pop_back();
  TEST<bool>::ASSERT_EQUAL(90, list1.back(), "[back should be 90]", "BACK {3}");
  
  for(auto i = 0; i < 7; i++) {
    list1.pop_back();
  }
  TEST<bool>::ASSERT_EQUAL(20, list1.back(), "[back should be 20]", "BACK {4}");
}

/**
 * Tests the front() function of the single_l class with const objects.
 * @param None
 * @return None
 * @throws None
 */
void test_front_const() {
  const single::single_l<int> list1{10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
  TEST<bool>::ASSERT_EQUAL(10, list1.front(), "[front should be 10]", "FRONT CONST {1}");
  
  const single::single_l<int> list2{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  TEST<bool>::ASSERT_EQUAL(1, list2.front(), "[front should be 1]", "FRONT CONST {2}");
}

/**
 * Tests the back() method of the single::single_l class when the list is const.
 * @param None
 * @return None
 * @throws None
 */
void test_back_const() {
  const single::single_l<int> list1{10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
  TEST<bool>::ASSERT_EQUAL(100, list1.back(), "[front should be 100]", "BACK CONST {1}");
  
  const single::single_l<int> list2{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  TEST<bool>::ASSERT_EQUAL(10, list2.back(), "[front should be 10]", "BACK CONST {2}");
}

/**
 * Test the subscript operator of a single linked list.
 * @param None
 * @return None
 * @throws None
 */
void test_sub_operator() {
  single::single_l<int> list1{10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
  TEST<bool>::ASSERT_EQUAL(100, list1[list1.size()-1], "[should return 100]", "SUB_OP {1}");
  
  single::single_l<int> list2{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  TEST<bool>::ASSERT_EQUAL(4, list2[3], "[should return 4]", "SUP_OP {2}");
}

/**
 * Tests the at() function of a single_l<int> list. It asserts that the 
 * values returned by at() match the expected values at all positions.
 * @param None
 * @return None
 * @throws None
 */
void test_at() {
  single::single_l<int> list1{1, 2, 3, 4, 5};
  TEST<bool>::ASSERT_EQUAL(1, list1.at(0), "[first element should be 1]", "AT {1}");
  TEST<bool>::ASSERT_EQUAL(2, list1.at(1), "[first element should be 2]", "AT {2}");
  TEST<bool>::ASSERT_EQUAL(3, list1.at(2), "[first element should be 3]", "AT {3}");
  TEST<bool>::ASSERT_EQUAL(4, list1.at(3), "[first element should be 4]", "AT {4}");
  TEST<bool>::ASSERT_EQUAL(5, list1.at(4), "[first element should be 5]", "AT {5}");
}

/**
 * Tests the delete_at() method of a single_l list.
 * @param None
 * @return None
 * @throws None
 */
void test_delete_at() {
  single::single_l<int> list1{10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
    
  list1.delete_at(4);
  TEST<bool>::ASSERT_FALSE(list1.at(4) == 50, "[element should not be 50]", "DELETE_AT {1}");
  TEST<bool>::ASSERT_TRUE(list1.size() == 9, "[size should be 9]", "DELETE_AT {2}");
  
  list1.delete_at(3);
  TEST<bool>::ASSERT_FALSE(list1.at(3) == 40, "[element should not be 40]", "DELETE_AT {3}");
  TEST<bool>::ASSERT_TRUE(list1.size() == 8, "[size should be 8]", "DELETE_AT {4}");
  
  TEST<bool>::ASSERT_TRUE(list1.at(3) == 60, "[list1 at i = 3 should be 60]", "DELETE_AT {4}");
}

/**
 * Tests the delete_value method of the single_l class.
 * @param None
 * @return None
 * @throws None
 */
void test_delete_value() {
  single::single_l<int> list1{10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
  
  list1.delete_value(10);
  TEST<bool>::ASSERT_EQUAL(-1, list1.search(100), "[100 should not be in the list]", "DELETE_VAL {1}");
  
  list1.delete_value(90);
  TEST<bool>::ASSERT_EQUAL(-1, list1.search(90), "[90 should not be in the list]", "DELETE_VAL {2}");
  
  list1.delete_value(80);
  TEST<bool>::ASSERT_EQUAL(-1, list1.search(80), "[80 should not be in the list]", "DELETE_VAL {3}");
  
  TEST<bool>::ASSERT(list1.size() == 7, "[size should be 7]", "DELETE_VAL {4}");
}

/**
 * Tests the search method of the single_l class.
 * @return void
 * @throws None
 */
void test_search() {
  single::single_l<int> list1{10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
  TEST<bool>::ASSERT_EQUAL(0, list1.search(10), "[10 should be at i = 0]", "SEARCH {1}");
  TEST<bool>::ASSERT_EQUAL(5, list1.search(60), "[60 should be at i = 5]", "SEARCH {2}");
  TEST<bool>::ASSERT_EQUAL(-1, list1.search(120), "[120 should not be int the list]", "SEARCH {3}");
  TEST<bool>::ASSERT_EQUAL(-1, list1.search(8), "[8 should not be int the list]", "SEARCH {4}");
}

/**
 * Tests the at_position() function of the single_l class.
 * @param None
 * @return None
 * @throws None
 */
void test_at_position() {
  single::single_l<int> list1{10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
  TEST<bool>::ASSERT_EQUAL(1, list1.search(20), "[20 should be at i = 1]", "AT_POSITION {1}");
  TEST<bool>::ASSERT_EQUAL(2, list1.search(30), "[30 should be at i = 2]", "AT_POSITION {2}");
  
  list1.at_position(12, 2);
  TEST<bool>::ASSERT_EQUAL(2, list1.search(12), "[12 should be at i = 2]", "AT_POSITION {3}");
  TEST<bool>::ASSERT_EQUAL(1, list1.search(20), "[20 should be at i = 1]", "AT_POSITION {4}");
  TEST<bool>::ASSERT_EQUAL(3, list1.search(30), "[30 should be at i = 3]", "AT_POSITION {5}");
  TEST<bool>::ASSERT(list1.size() == 11, "[size should be 11]", "AT_POSITION {6}");
}

/**
 * Tests the reverse function of single_l class.
 * @param None
 * @return None
 * @throws None
 */
void test_reverse() {
  single::single_l<int> list1{10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
  TEST<bool>::ASSERT_EQUAL(10, list1[0], "[first element should be 10]", "REVERSE {1}");
  TEST<bool>::ASSERT_EQUAL(100, list1[list1.size()-1], "[last element should be 100]", "REVERSE {2}");
  
  list1.reverse();
  
  TEST<bool>::ASSERT_EQUAL(100, list1[0], "[first element should be 100]", "REVERSE {3}");
  TEST<bool>::ASSERT_EQUAL(10, list1[list1.size()-1], "[last element should be 10]", "REVERSE {4}");
}

/**
 * Tests if the single linked list is properly sorted.
 * @param None
 * @return None
 * @throws None
 */
void test_sort() {
  single::single_l<int> list1{95, 7, 86, 44, 43, 63, 46, 45, 13, 2, 19, 4, 84, 100, 5, 63, 53, 56, 85, 95, 48, 88, 9, 6, 65};
  list1.sort();
  
  TEST<int>::GREATER(list1[1], list1[0], "[list should be dsorted]", "SORT {1}");
  TEST<int>::GREATER(list1[2], list1[1], "[list should be dsorted]", "SORT {2}");
  TEST<int>::GREATER(list1[3], list1[2], "[list should be dsorted]", "SORT {3}");
  TEST<int>::GREATER(list1[4], list1[3], "[list should be dsorted]", "SORT {4}");
  
  TEST<int>::SMALLER(list1[5], list1[6], "[list should be sorted]", "SORT {5}");
  TEST<int>::SMALLER(list1[1], list1[10], "[list should be sorted]", "SORT {6}");
  TEST<int>::SMALLER(list1[18], list1[22], "[list should be sorted]", "SORT {7}");
  TEST<int>::SMALLER(list1[23], list1[24], "[list should be sorted]", "SORT {8}");
}

/**
 * Tests the functionality of removing duplicates from a single linked list.
 * @param None
 * @return None
 * @throws None
 */
void test_remove_duplicates() {
  single::single_l<int> list1{1, 1, 1, 1, 1, 1, 1, 3};
  list1.remove_duplicates();
  
  TEST<bool>::ASSERT(list1.size() == 2, "[size should be 2]", "REM_DUP {1}");
  TEST<bool>::ASSERT(list1[0] == 1, "[first element should be 1]", "REM_DUP {1}");
  
}

/**
 * Tests resizing of single_l list.
 * @param None
 * @return None
 * @throws None
 */
void test_resize() {
  single::single_l<int> list1;
  TEST<bool>::ASSERT(list1.size() == 0, "[size should be 0]", "RESIZE {1}" );
  
  list1.resize(5);
  TEST<bool>::ASSERT(list1.size() == 5, "[size should be 5]", "RESIZE {2}" );
  
  list1.resize(10);
  TEST<bool>::ASSERT(list1.size() == 10, "[size should be 10]", "RESIZE {3}" );
}

/**
 * Solves the problem from my textbook described in main.cpp
 * @param list1 first list to be merged
 * @param list2 second list to be merged
 * @return a new single linked list that contains all elements from the input
 * lists in non-decreasing order
 * @throws None
 */
template <typename T>
single::single_l<T> merge_lists(const single::single_l<T>& list1, const single::single_l<T>& list2) {
  single::single_l<T> merged_list;
  
  single::single_l<T> temp_list1 = list1;
  single::single_l<T> temp_list2 = list2;
  
  while (!temp_list1.empty() && !temp_list2.empty()) {
    if (temp_list1.front() <= temp_list2.front()) {
      merged_list.push_back(temp_list1.front());
      temp_list1.pop_front();
    } else {
      merged_list.push_back(temp_list2.front());
      temp_list2.pop_front();
    }
  }
  
  while (!temp_list1.empty()) {
    merged_list.push_back(temp_list1.front());
    temp_list1.pop_front();
  }
  
  while (!temp_list2.empty()) {
    merged_list.push_back(temp_list2.front());
    temp_list2.pop_front();
  }
  
  return merged_list;
}

/**
 * Runs an example function.
 * @param None
 * @return None
 * @throws None
 */
void run_example() {
  single::single_l<int> list1{1, 2, 3, 4, 5, 6};
  single::single_l<int> list2{7, 8, 9, 10, 11};
  
  std::cout << "\n[first list]: ";
  list1.print();
  
  std::cout << "\n[second list]: ";
  list2.print();
  single::single_l<int> merged_list = merge_lists(list1, list2);
  
  std::cout << "\n[merged list]: ";
  merged_list.print();
  std::cout << "\n";
}

/**
 * Runs a series of tests for the LinkedList class to ensure proper functionality.
 * @return true if all tests pass, false otherwise
 * @throws std::exception if any test fails
 */
bool run_tests() {
  try {
    test_constructor();
    std::cout << std::endl;
    test_destructor();
    std::cout << std::endl;
    test_move_constructor();
    std::cout << std::endl;
    test_move_assignment_operator();
    std::cout << std::endl;
    test_copy_constructor();
    std::cout << std::endl;
    test_copy_assignment_operator();
    std::cout << std::endl;
    test_push_back();
    std::cout << std::endl;
    test_push_front();
    std::cout << std::endl;
    test_pop_back();
    std::cout << std::endl;
    test_pop_front();
    std::cout << std::endl;
    test_clear();
    std::cout << std::endl;
    test_swap();
    std::cout << std::endl;
    test_empty();
    std::cout << std::endl;
    test_size();
    std::cout << std::endl;
    test_front();
    std::cout << std::endl;
    test_back();
    std::cout << std::endl;
    test_front_const();
    std::cout << std::endl;
    test_back_const();
    std::cout << std::endl;
    test_sub_operator();
    std::cout << std::endl;
    test_at();
    std::cout << std::endl;
    test_delete_at();
    std::cout << std::endl;
    test_delete_value();
    std::cout << std::endl;
    test_search();
    std::cout << std::endl;
    test_at_position();
    std::cout << std::endl;
    test_reverse();
    std::cout << std::endl;
    test_sort();
    std::cout << std::endl;
    test_remove_duplicates();
    std::cout << std::endl;
    test_resize();
    std::cout << std::endl;
    
  }catch (std::exception &e) {
    std::cerr << "{error}: " << e.what() << std::endl;
    return false;
  }
  return true;
}

test.h

//
//  test.h
//  linked_list_xcode
//
//  Created by Jakob Balkovec on 14/06/2023.
//

// {
// This file defines a series of test functions implemented in @file test_file.h
// }

#ifndef test_h
#define test_h

#include "single_l.h"

void test_constructor();
void test_destructor();
void test_move_constructor();
void test_move_assignment_operator();
void test_copy_constructor();
void test_copy_assignment_operator();
void test_push_back();
void test_push_front();
void test_pop_back();
void test_pop_front();
void test_clear();
void test_swap();
void test_empty();
void test_size();
void test_front();
void test_back();
void test_front_const();
void test_back_const();
void test_sub_operator();
void test_at();
void test_delete_at();
void test_delete_value();
void test_search();
void test_at_position();
void test_reverse();
void test_sort();
void test_remove_duplicates();
void test_resize();

template <typename T>
single::single_l<T> merge_lists();

void run_example();

bool run_tests();

#endif /* test_h */

MakeFile

CC = g++
CFLAGS = -std=c++17 -Wall

all: prog

prog: single_l.o main.o test_file.o
    $(CC) $(CFLAGS) $^ -o $@

single_l.o: single_l.cpp single_l.h
    $(CC) $(CFLAGS) -c $<

main.o: main.cpp single_l.h test_file.h test.h
    $(CC) $(CFLAGS) -c $<

test_file.o: test_file.cpp test_file.h single_l.h test.h
    $(CC) $(CFLAGS) -c $<

clean:
    rm -f *.o prog
\$\endgroup\$
8
  • 1
    \$\begingroup\$ Just FYI, 2000 lines of code takes a bit of time to review, don't expect an immediate answer. I am reviewing the code. \$\endgroup\$
    – pacmaninbw
    Commented Jun 17, 2023 at 12:54
  • \$\begingroup\$ @pacmaninbw: what? All the pull requests in our company are done in less than 15 minutes. Now, either you are doing it incorrectly or all employees in our company... Hmm.... I am not saying that you're doing it wrong... \$\endgroup\$ Commented Jun 17, 2023 at 14:08
  • 1
    \$\begingroup\$ @ThomasWeller Metrics for formal code reviews are about 200 lines an hour. This is based on research from SEI (Software Engineering Institute at CMU). \$\endgroup\$
    – pacmaninbw
    Commented Jun 17, 2023 at 14:43
  • 1
    \$\begingroup\$ @ThomasWeller Are programmers in your company sitting around all day waiting for code review requests to come in? And then they approve them without reviewing? I don’t see another way of doing them so quickly… \$\endgroup\$ Commented Jun 17, 2023 at 15:38
  • \$\begingroup\$ @pacmaninbw Thank you, I definitely wasn’t expecting an answer right away, and I’m sorry if it looked like that. I appreciate every answer I get \$\endgroup\$ Commented Jun 17, 2023 at 15:52

3 Answers 3

3
+50
\$\begingroup\$

General Observations

I am recommending that you take some time to follow the suggestion here and then post a follow up question. What should be handled immediately is the re-partitioning of the classes and files. You should also consider a rewrite of the TEST class.

In the main() function rather than force the user to make a choice at compile time, allow them to choose between TEST and EXAMPLE at run time. This would also allow you to create a menu to run each test individually if that is necessary.

C++ is generally a portable language, but __VERSION__ in main.cppis not portable, __cplusplus is portable.

static inline void show_last_compiled() {
    std::cout << "\n[file]: " << __FILE__ << '\n'
        << "[compiled on]: " << __DATE__ << " at " << __TIME__ << '\n'
        << "[compiler]: " << __cplusplus << '\n'
        << "[timestamp]: " << __TIMESTAMP__ << '\n';
}

It isn't clear why you are using the inline specification for the above function. If it is for optimization purposes then the -O3 switch can determine if the function should be inlined better than a programmer can.

It might be better if the merge_list() function was a member of the list class.

Rather than using XCODE it would be better to use gcc, since it is more portable and will compile on all systems the same way.

If you are just starting out with automating builds, prefer CMake over Makefiles, it will automate more of the process for you making the automation less work.

The file and class partitioning needs more design work.

File Partitioning and Naming

All of the Test member functions in test_file.h should be defined in a .cpp file. The function prototypes declared in test.h should be declared in test_file.h since the body of the functions are in test_file.cpp.

Missing Include Directive

The file test_file.h is missing the #include <functional> statement. On my computer using my compiler (not Apple or XCODE) this file does not compile. Adding the missing include directive allows the file to compile.

Use of std::endl versus use of "\n"

The code is inconsistent, sometimes it ends the output with "\n" such as this line in main.cpp

        std::cout << "\n\n[all tests passed]\n\n";

and sometimes it ends an output line with std::endl such as this function in test_file.h

    static void ASSERT_EQUAL(const T& expected, const T& actual, const std::string& message = "", const std::string& func_name = "") {
        if (expected == actual) {
            PRINT_PASS(func_name);
        }
        else {
            PRINT_FAIL(message, func_name);
            std::cout << "\n[expected]: " << expected << " ->  [actual]: " << actual << std::endl;
        }
    }

Throughout your code you should always be consistent, choose one or the other.

A second consideration to make is that std::endl will slow down your program each time you call it, because std:endl calls std::flush to flush the output buffer. The fail path of the function ASSERT_EQUAL calls std::flush twice because the function PRINT_FAIL() also contains an std::endl. This is unnecessary. If you need to flush the outout buffer after each test call std::flush at the end of each test, but that probably isn't necessary.

Naming Conventions

In the C programming language as well as the C++ programming language ALL CAPITALS is generally reserved for symbolic constants that are defined using #define or const (and constexpr). Class names generally start with a capital, function and variables start with lower case letters.

Short Example

//
//  test_file.h
//  linked_list_xcode
//
//  Created by Jakob Balkovec on 13/06/2023.
//

#ifndef test_file_h
#define test_file_h

#include <functional>
#include <iostream>
#include <string>

template <class T>
class Test {
private:
    /**
     * Prints a test passing message with an optional function name.
     * @param func_name an optional name of the function being tested
     * @return void
     */
    static void print_pass(const std::string& func_name = "") noexcept {
        std::cout << "[TEST " << func_name << " PASSED]" << std::endl;
    }

    /**
     * Prints a failure message for a test, including an optional message and function name.
     * @param message the message to include in the failure message (optional)
     * @param func_name the name of the function being tested (optional)
     * @return void
     */
    static void print_fail(const std::string& message, const std::string& func_name = "") noexcept {
        std::cout << "[TEST " << func_name << " FAILED]";
        if (!message.empty()) {
            std::cout << "  " << message;
        }
        std::cout << std::endl;
    }

...

} // End class Test

Static Member Methods

It isn't clear why the all of the methods in the TEST class are static methods, to be honest it isn't clear why any of the member functions of the TEST class are declared as static member functions. This might have been the cause of your linking errors. Static class members have a very special usage.

No Throw

Many of the functions comments contain @throws None. There is actually a C++ way to indicate this in the function declaration so that it doesn't need to be in the comments. You need the noexcept keyword in the function declarations.

/**
 * Tests resizing of single_l list.
 * @param None
 * @return None
 * @throws None
 */
void test_resize() noexcept {
    single::single_l<int> list1;
    Test<bool>::assert(list1.size() == 0, "[size should be 0]", "RESIZE {1}");

    list1.resize(5);
    Test<bool>::assert(list1.size() == 5, "[size should be 5]", "RESIZE {2}");

    list1.resize(10);
    Test<bool>::assert(list1.size() == 10, "[size should be 10]", "RESIZE {3}");
}

/**
 * Solves the problem from my textbook described in main.cpp
 * @param list1 first list to be merged
 * @param list2 second list to be merged
 * @return a new single linked list that contains all elements from the input
 * lists in non-decreasing order
 * @throws None
 */
template <typename T>
single::single_l<T> merge_lists(const single::single_l<T>& list1, const single::single_l<T>& list2) noexcept {
    single::single_l<T> merged_list;

    single::single_l<T> temp_list1 = list1;
    single::single_l<T> temp_list2 = list2;

    while (!temp_list1.empty() && !temp_list2.empty()) {
        if (temp_list1.front() <= temp_list2.front()) {
            merged_list.push_back(temp_list1.front());
            temp_list1.pop_front();
        }
        else {
            merged_list.push_back(temp_list2.front());
            temp_list2.pop_front();
        }
    }

    while (!temp_list1.empty()) {
        merged_list.push_back(temp_list1.front());
        temp_list1.pop_front();
    }

    while (!temp_list2.empty()) {
        merged_list.push_back(temp_list2.front());
        temp_list2.pop_front();
    }

    return merged_list;
}


\$\endgroup\$
3
  • \$\begingroup\$ Some really good points here! But I can’t agree with the Xcode vs GCC comment, Xcode is an IDE that uses Clang, which certainly is available on any system. \$\endgroup\$ Commented Jun 17, 2023 at 17:35
  • \$\begingroup\$ I followed a “guide to writing unit tests ” in my fat C++ book that I bought in the beginning of the school year, they made their methods static …didn’t really put much thought into it, but thanks for letting me know \$\endgroup\$ Commented Jun 17, 2023 at 20:01
  • \$\begingroup\$ I appreciate the feedback, also accepted your answer. \$\endgroup\$ Commented Jun 17, 2023 at 20:02
3
\$\begingroup\$

This is going to be a superficial review, I hope someone else will go into details on the C++ code.

I don’t see any issues with your makefile, and don’t understand why you would need to include single_l.cpp file in test_file.cpp. Including the source file in another source file, and then linking the two together, means that you define those functions twice in the program. The reason this works is because they are member functions, if they were free functions you should see a linker error (unless explicitly inlined).

You should consider defining some of the shorter class methods inline. For example the constructor for single::single_l really doesn’t need to be declared separately, it has no code! By declaring it in a separate file, the compiler won’t be able to inline it when you use it. A separate file is appropriate for the larger bits of code.

You write function documentation in the .cpp file. Typically it is more convenient to write it in the header file. For example, once compiled, your library users would have easy access to the header files (included by their sources), but not the source files (in a separate project). Their IDE would bring up the declaration of the function in the header file, that is where I’d want to read the documentation.

Your TEST class only has static functions. Make these free functions. There is no advantage to putting them in a class. A namespace is more appropriate in C++.


Looking at the code a bit more (still superficial, I won’t go into too many details because I’m typing this on a phone).

Please check out the copy-and-swap idiom. That should significantly simplify your copy and move constructors and assignment operators.

single_l<T>::pop_back() is documented to throw an exception. But it catches it and writes a message to stderr instead. pop_front and some other functions do the same thing. Obviously throwing an exception is the right behavior, writing a messsge to stderr is not. Because you must let your user (= the calling code, not the human user of the program that uses your library) know of the error, and you must let the user decide what to do with it. I’m assuming it’s left over from debugging?

print_helper() and print_reverse_helper() should be private member functions. Instead of a print function, consider overloading operator<<().


know std::list is a doubly linked list...mine is not and I am planning on making one later on, that's why I used a namespace to distinguish them. Please let me know if this is not the right approach and if some other OOP method would be a better fit. I eventually want to avoid namespace pollution since most of the methods would have the same name. Perhaps making all of the methods in one class static? (doesn’t sound productive but could it work?)

Namespace pollution is defining too many things in a namespace that is not yours. If you define your classes and functions within a unique namespace, then you’re not polluting anyone else’s namespaces. Declaring a class outside a namespace would be pollution.

You don’t need a separate namespace for each class. Your classes have different names, then they can be inside the same namespace. Member functions for different classes can have the same name. For example, name your namespace foo, then you have classes single_l and double_l. Both these classes can have a member pop_back(). One function will be foo::single_l::pop_back(), the other will be foo::double_l::pop_back().

This has nothing to do with member functions being static or not.

\$\endgroup\$
4
  • \$\begingroup\$ My answer is not as in depth as I would like. \$\endgroup\$
    – pacmaninbw
    Commented Jun 17, 2023 at 16:57
  • \$\begingroup\$ Yeah I thought about overloading the ostream operator but ran into an issue when trying to print the list in reverse...means I would have << to print it and then a weird sequence of chars to print it in reverse... for instance: <<< or >> which might be confusing. I appreciate the feedback, thanks! \$\endgroup\$ Commented Jun 17, 2023 at 20:10
  • \$\begingroup\$ @I_throw_but_dont_catch How about std::cout << list.reversed()? It’s not like this is a frequent operation, building a new temporary list is not a big deal. \$\endgroup\$ Commented Jun 17, 2023 at 21:04
  • \$\begingroup\$ Oh, I see, never thought of it that way...I was thinking about making the << operator print the list as in std::cout << list << std::ednl; I'll give it a shot \$\endgroup\$ Commented Jun 18, 2023 at 20:16
2
\$\begingroup\$

Try to make the interface simpler and more robust

Let's look at these two lines in your example usage of your TEST class:

TEST<bool>::ASSERT_TRUE(list1.empty(), "[list should be empty]","DEF_CONSTRUCTOR {1}");
TEST<bool>::ASSERT_EQUAL(0, list1.size(), "[list size should be 0]", "DEF_CONSTRUCTOR {2}");

The static member function ASSERT_TRUE() does not depend on T. So in the first line you could have replaced bool with any other type, and it will still do the same. I'd rather not have to specify anything in that case.

The first two parameters of ASSERT_EQUAL() are const T&. But notice how you wrote TEST<bool> in front of it, but list1.size() is a std::size_t, and 0 is an int. How can a reference to T accept those values? The answer is that the implicit conversion rules will cause temporary bool values to be created, and references to those temporaries will be passed to ASSERT_EQUAL(). Here it seems harmless, but later on you write:

TEST<bool>::ASSERT_EQUAL(2, list2.size(), "[list size should be 2]", "CPY_CONSTRUCTOR {2}");

Oops! Both 2 and list2.size() will be implicitly cast to bool. So a non-empty list of any size will pass this test! Having to specify a type for TEST<> manually is annoying, you probably just copy&pasted another test and modified the function parameters, but forgot about the template parameter. It would be much better if you did not have to specify the type yourself, just let it be deduced from the function parameters, by moving the function out of the class (as already suggested in the other answers), and make it a template:

template<typename T>
void ASSERT_EQUAL(const T& expected, const T& actual, …) {…}

If the two parameters do not have the same type, template type deduction will fail, as the compiler doesn't know which one to choose for T. This is what you want, but it forces you to be more precise in writing the test:

ASSERT_EQUAL(std::size_t(2), list2.size(), …);

Automatically getting the function name

Your ASSERT_*() functions take a func_name parameter. It sounds like that should be set to the name of the test function? But you pass it something different. I assume you just want something that allows you to quickly find which line in the source code of the tests triggered the assertion. Having to write some unique identifier yourself is annoying, and again it's easy to make a mistake, like copy&pasting another line and forgetting to modify the func_name argument, which you indeed did in your code.

Luckily, in C++20 there is a standard way of getting the function name, file name and line number where something is called, using std::source_location:

template<typename T>
void ASSERT_EQUAL(const T& expected, const T& actual,
                  const std::string& message = "",
                  const std::source_location location = std::source_location::current()) {
    …
    PRINT_FAIL(location);
    …
}

void PRINT_FAIL(const std::source_location& location) {
    std::cout << "[TEST " << location.function_name()
              << " line " << location.line()
              << " FAILED]";
    …
}
\$\endgroup\$
2
  • \$\begingroup\$ I didn’t know about std::source_location. That’s really cool! So far I’ve using macros for my assertions so I can use ` __FILE__` and __LINE__. \$\endgroup\$ Commented Jun 17, 2023 at 22:43
  • \$\begingroup\$ Thanks, I used template<class T> for the whole class so I can use the T param everywhere, I used the ExceptionType param to distinguish them... I will definitely remove that and template the functions separately \$\endgroup\$ Commented Jun 18, 2023 at 20:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.