This is probably the last time I will post this data structure that I have been working on. I have added in an iterator and const_iterator class (although I do not use them probably where I should be). I also did not follow another persons advice to add emplace, emplace_front, and emplace_back simply because I have no idea how nor can I find any code to refer to online.
The idea of this post is to gain more insight in what I need to change in my class most likely with the additional classes I have added i.e. iterator and const_iterator. This is my first time implementing an iterator class and to be honest I am not sure where to change my code to use iterator and where not to.
I want to again thank the community for helping me I really appreciate the effort.
Here is my header file:
#ifndef SINGLELINKEDLIST_h
#define SINGLELINKEDLIST_h
template <class T>
class SingleLinkedList {
private:
struct Node {
T data;
std::unique_ptr<Node> next = nullptr;
// disable if noncopyable<T> for cleaner error msgs
explicit Node(const T& x, std::unique_ptr<Node>&& p = nullptr)
: data(x)
, next(std::move(p)) {}
// disable if nonmovable<T> for cleaner error msgs
explicit Node(T&& x, std::unique_ptr<Node>&& p = nullptr)
: data(std::move(x))
, next(std::move(p)) {}
};
std::unique_ptr<Node> head = nullptr;
Node* tail = nullptr;
void do_pop_front() {
head = std::move(head->next);
}
public:
// Constructors
SingleLinkedList() = default; // empty constructor
SingleLinkedList(SingleLinkedList const &source); // copy constructor
// Rule of 5
SingleLinkedList(SingleLinkedList &&move) noexcept; // move constructor
SingleLinkedList& operator=(SingleLinkedList &&move) noexcept; // move assignment operator
~SingleLinkedList();
// Overload operators
SingleLinkedList& operator=(SingleLinkedList const &rhs);
// Memeber functions
void swap(SingleLinkedList &other) noexcept;
bool empty() const { return head.get() == nullptr; }
int size() const;
void push_back(const T &theData);
void push_back(T &&theData);
void push_front(const T &theData);
void push_front(T &&theData);
void insert(int pos, const T &theData);
void clear();
void pop_front();
void pop_back();
void delete_specific(int delValue);
bool search(const T &x);
// Create an iterator class
class iterator;
iterator begin();
iterator end();
// Create const iterator class
class const_iterator;
const_iterator cbegin() const;
const_iterator cend() const;
const_iterator begin() const;
const_iterator end() const;
};
template <class T>
SingleLinkedList<T>::SingleLinkedList(SingleLinkedList<T> const &source) {
for(Node* loop = source.head.get(); loop != nullptr; loop = loop->next.get()) {
push_back(loop->data);
}
}
template <class T>
SingleLinkedList<T>::SingleLinkedList(SingleLinkedList<T>&& move) noexcept {
move.swap(*this);
}
template <class T>
SingleLinkedList<T>& SingleLinkedList<T>::operator=(SingleLinkedList<T> &&move) noexcept {
move.swap(*this);
return *this;
}
template <class T>
SingleLinkedList<T>::~SingleLinkedList() {
clear();
}
template <class T>
void SingleLinkedList<T>::clear() {
while (head) {
do_pop_front();
}
}
template <class T>
SingleLinkedList<T>& SingleLinkedList<T>::operator=(SingleLinkedList const &rhs) {
SingleLinkedList copy{ rhs };
swap(copy);
return *this;
}
template <class T>
void SingleLinkedList<T>::swap(SingleLinkedList &other) noexcept {
using std::swap;
swap(head, other.head);
swap(tail, other.tail);
}
template <class T>
int SingleLinkedList<T>::size() const {
int size = 0;
for (auto current = head.get(); current != nullptr; current = current->next.get()) {
size++;
}
return size;
}
template <class T>
void SingleLinkedList<T>::push_back(const T &theData) {
std::unique_ptr<Node> newNode = std::make_unique<Node>(theData);
if (!head) {
head = std::move(newNode);
tail = head.get();
}
else {
tail->next = std::move(newNode);
tail = tail->next.get();
}
}
template <class T>
void SingleLinkedList<T>::push_back(T &&thedata) {
std::unique_ptr<Node> newnode = std::make_unique<Node>(std::move(thedata));
if (!head) {
head = std::move(newnode);
tail = head.get();
}
else {
tail->next = std::move(newnode);
tail = tail->next.get();
}
}
template <class T>
void SingleLinkedList<T>::push_front(const T &theData) {
std::unique_ptr<Node> newNode = std::make_unique<Node>(theData);
newNode->next = std::move(head);
head = std::move(newNode);
if (!tail) {
tail = head.get();
}
}
template <class T>
void SingleLinkedList<T>::push_front(T &&theData) {
std::unique_ptr<Node> newNode = std::make_unique<Node>(std::move(theData));
newNode->next = std::move(head);
head = std::move(newNode);
if (!tail) {
tail = head.get();
}
}
template <class T>
void SingleLinkedList<T>::insert(int pos, const T &theData) {
if (pos < 0) {
throw std::out_of_range("The insert location is invalid.");
}
auto node = head.get();
int i = 0;
for (; node && node->next && i < pos; node = node->next.get(), i++);
auto newNode = std::make_unique<Node>(theData);
if (node) {
newNode->next = std::move(node->next);
if (!newNode->next) tail = newNode.get(); // created in case we insert after the current tail
node->next = std::move(newNode);
}
else {
head = std::move(newNode);
tail = head.get();
}
}
template <class T>
void SingleLinkedList<T>::pop_front() {
if (empty()) {
throw std::out_of_range("List is Empty!!! Deletion is not possible.");
}
do_pop_front();
}
template <class T>
void SingleLinkedList<T>::pop_back() {
if (!head) return;
auto current = head.get();
Node* previous = nullptr;
while (current->next) {
previous = current;
current = current->next.get();
}
if (previous) {
previous->next = nullptr;
}
else {
head = nullptr;
}
tail = previous;
previous->next = nullptr;
}
template <class T>
void SingleLinkedList<T>::delete_specific(int delValue) {
if (!head.get()) {
throw std::out_of_range("List is Empty!!! Deletion is not possible.");
}
auto temp1 = head.get();
Node* temp2 = nullptr;
while (temp1->data != delValue) {
if (temp1->next == nullptr) {
throw std::invalid_argument("Given node not found in the list!!!");
}
temp2 = temp1;
temp1 = temp1->next.get();
}
temp2->next = std::move(temp1->next);
}
template <class T>
bool SingleLinkedList<T>::search(const T &x) {
return std::find(begin(), end(), x) != end();
}
template <typename T>
std::ostream& operator<<(std::ostream &str, SingleLinkedList<T>& list) {
for (auto const& item : list) {
str << item << "\t";
}
return str;
}
template <class T>
class SingleLinkedList<T>::iterator {
Node* node = nullptr;
public:
using iterator_category = std::forward_iterator_tag;
using value_type = T;
using difference_type = std::ptrdiff_t;
using pointer = T * ;
using reference = T & ;
iterator(Node *node = nullptr) : node(node) {}
bool operator!=(const iterator& other) const { return node != other.node; }
bool operator==(const iterator& other) const { return node == other.node; }
T& operator*() const { return node->data; }
T& operator->() const { return node->data; }
iterator& operator++() { node = node->next.get(); return *this; }
};
template <class T>
class SingleLinkedList<T>::const_iterator {
Node* node = nullptr;
public:
using iterator_category = std::forward_iterator_tag;
using value_type = T;
using difference_type = std::ptrdiff_t;
using pointer = T * ;
using reference = T & ;
const_iterator(Node *node = nullptr) : node(node) {}
bool operator!=(const iterator& other) const { return node != other.node; }
bool operator==(const iterator& other) const { return node == other.node; }
const T& operator*() const { return node->data; }
const T& operator->() const { return node->data; }
const_iterator& operator++() { node = node->next.get(); return *this; }
};
template<class T>
typename SingleLinkedList<T>::iterator SingleLinkedList<T>::begin() {
return head.get();
}
template<class T>
typename SingleLinkedList<T>::iterator SingleLinkedList<T>::end() {
return {};
}
template <class T>
typename SingleLinkedList<T>::const_iterator SingleLinkedList<T>::begin() const {
return head.get();
}
template <class T>
typename SingleLinkedList<T>::const_iterator SingleLinkedList<T>::end() const {
return {};
}
template <class T>
typename SingleLinkedList<T>::const_iterator SingleLinkedList<T>::cbegin() const {
return head.get();
}
template <class T>
typename SingleLinkedList<T>::const_iterator SingleLinkedList<T>::cend() const {
return {};
}
#endif /* SingleLinkedList_h*/
Here is my main.cpp file:
#include <iostream>
#include <iterator>
#include <memory>
#include <utility>
#include <stdexcept>
#include <iosfwd>
#include <stdexcept>
#include <ostream>
#include "SingleLinkedList.h"
#include "DoubleLinkedList.h"
int main(int argc, const char * argv[]) {
///////////////////////////////////////////////////////////////////////
///////////////////////////// Single Linked List //////////////////////
///////////////////////////////////////////////////////////////////////
SingleLinkedList<int> obj;
obj.push_back(2);
obj.push_back(4);
obj.push_back(6);
obj.push_back(8);
obj.push_back(10);
std::cout<<"\n--------------------------------------------------\n";
std::cout<<"---------------displaying all nodes---------------";
std::cout<<"\n--------------------------------------------------\n";
std::cout << obj << "\n";
std::cout<<"\n--------------------------------------------------\n";
std::cout<<"----------------Inserting At Start----------------";
std::cout<<"\n--------------------------------------------------\n";
obj.push_front(50);
std::cout << obj << "\n";
std::cout<<"\n--------------------------------------------------\n";
std::cout<<"-------------inserting at particular--------------";
std::cout<<"\n--------------------------------------------------\n";
obj.insert(5,60);
std::cout << obj << "\n";
std::cout << "\n--------------------------------------------------\n";
std::cout << "-------------Get current size ---=--------------------";
std::cout << "\n--------------------------------------------------\n";
std::cout << obj.size() << "\n";
std::cout<<"\n--------------------------------------------------\n";
std::cout<<"----------------deleting at start-----------------";
std::cout<<"\n--------------------------------------------------\n";
obj.pop_front();
std::cout << obj << "\n";
std::cout<<"\n--------------------------------------------------\n";
std::cout<<"----------------deleting at end-----------------------";
std::cout<<"\n--------------------------------------------------\n";
obj.pop_back();
std::cout << obj << "\n";
std::cout<<"\n--------------------------------------------------\n";
std::cout<<"--------------Deleting At Particular--------------";
std::cout<<"\n--------------------------------------------------\n";
obj.delete_specific(4);
std::cout << obj << "\n";
obj.search(8) ? printf("yes"):printf("no");
std::cout << "\n--------------------------------------------------\n";
std::cout << "--------------Testing copy----------------------------";
std::cout << "\n--------------------------------------------------\n";
SingleLinkedList<int> obj1 = obj;
std::cout << obj1 << "\n";
std::cin.get();
}
Here is also the following post I made here
2 Answers 2
Design issues
An overload of
insert
that acceptsT&&
is missing.Now that there are iterators, I'd expect
insert
to take aconst_iterator
as parameter instead ofint pos
.This allows the
insert
operation itself to run in \$\mathcal{O}(1)\$ by delegating the iteration work to the caller (who might be iterating the list already, e.g. to find where the node should be inserted in the first place).Also, it would be nice if an
iterator
pointing to the newly inserted node would be returned (as this allows for easy chaining of insertions).If insertion at a specific ordinal position is needed, this could still be easily achieved by calling
list.insert(list.begin() + pos, value)
.The easiest way to let the
SingleLinkedList
have access toiterator::node
orconst_iterator::node
is to declare it as afriend class SingleLinkedList
insideiterator
andconst_iterator
.
Clarity issue: What does the
delValue
parameter ofdelete_specific
actually represent? And while I'm at it, what is the actual purpose ofdelete_specific
?My first guesses based on its definition would be that
delValue
is the position to be removed (likeint pos
ininsert
), or the value of which all copies should be removed (which then should be of typeconst T&
, notint
).In the first case, I'd suggest rewriting
void delete_specific(int)
intoiterator erase_after(const_iterator)
, to keep the naming from the standard library'sstd::forward_list
.In the latter case, I'd suggest rewriting
void delete_specific(int)
intovoid remove(const T&)
, again adhering to the standard library's choice of name.
Granted, there are some inconsistencies in the standard library itself, as the operations
std::forward_list::remove
andstd::list::remove
(removing all elements with a specific value) correspond toerase
member functions on other containers. Still, it might be easier to adopt usage ofSingleLinkedList
if the names are similar to thestd::forward_list
ones.However, that actual implementation of
delete_specific
does neither of those, instead it only removes the first element that is equal todelValue
(not all of them), and throws if none was found (which again would surprise me).
Implementation issues
Copy-constructing a
SingleLinkedList
has runtime complexity \$\mathcal{O}(n^2)\$. This could be reduced to \$\mathcal{O}(n)\$.There are multiple ways to achieve this, the easiest would be (assuming the above mentioned changes to
insert
have been made):SingleLinkedList(const SingleLinkedList& other) { auto insert_pos = begin(); for(auto&& obj : other) { // keep the position of the newly inserted node, as the next one will be inserted after it insert_pos = insert(insert_pos, obj); } }
Move assignment might extend the lifetime of the elements originally contained in
this
far longer than expected by swapping them intomove
. (Also, it might be unexpected thatmove
isn't empty after the move-assignment.)clear()
doesn't updatetail
(should benullptr
afterwards).pop_front()
doesn't updatetail
in case the last node was removed.
Iterator issues
The iterator classes need a post-increment operator to be fully adhering to the ForwardIterator requirements.
const_iterator::pointer
should beconst T*
andconst_iterator::reference
should beconst T&
, as the expectations of aconst_iterator
is that it doesn't allow modification (by anyone but the container) of the data it's refering to.Simple memorization help:
operator*
should return areference
andoperator->
should return apointer
.iterator::operator->
andconst_iterator::operator->
should return a pointer, i.e.return &node->data;
(also needs adjustment of the return type).Consider adding an constructor to
const_iterator
that allows implicit conversion from a normaliterator
. This would allow usingiterator
s in places where aconst_iterator
would be expected (e.g. in theinsert
member function).
-
\$\begingroup\$ I need some help if you are not too busy. \$\endgroup\$Snorrlaxxx– Snorrlaxxx2018年08月11日 18:11:45 +00:00Commented Aug 11, 2018 at 18:11
-
\$\begingroup\$ would the insert function be a void or an iterator type? \$\endgroup\$Snorrlaxxx– Snorrlaxxx2018年08月14日 00:22:29 +00:00Commented Aug 14, 2018 at 0:22
Make SingleLinkedList.h
self-sufficient
The file can be made self-sufficient by #include
ing the following header files:
<memory>
forstd::unique_ptr
andstd::make_unique
.<algorithm>
forstd::find
.<utility>
forstd::move
.<stdexcept>
forstd::out_of_range
andstd::invalid_argument
.<iterator>
forstd::forward_iterator_tag
.<cstddef>
forstd::ptrdiff_t
.
One trick to detect such issues is to make it the first #include
file in main.cpp.
-
2\$\begingroup\$
<string>
isn't explicitly needed, for 2 reasons: 1) those exception types can take aconst char*
parameter, and only raw string literals are used, and 2)<stdexcept>
already has to include<string>
to make it self-sufficient. (Also, I'm not sure on<utility>
,<memory>
might have to include it already.) \$\endgroup\$hoffmale– hoffmale2018年08月10日 06:35:51 +00:00Commented Aug 10, 2018 at 6:35 -
2\$\begingroup\$ You are right about (1). Not sure about (2). The constructors use
std::string const&
, which can be accomplished by forward declaration.<utility>
is used by many other standard headers. It is safer to#include
it explicitly. \$\endgroup\$R Sahu– R Sahu2018年08月10日 06:42:40 +00:00Commented Aug 10, 2018 at 6:42