I'm implementing a single linked list as close as it could be to std::forward_list
. I would welcome a review and also more suggestions on what to test additionally.
#ifndef LINKED_LIST_HPP
#define LINKED_LIST_HPP
#include <iostream>
template <typename T>
class LinkedList {
private:
struct Node {
Node* next;
T data;
Node(T data): data(data), next(nullptr){
}
};
public:
struct iterator {
// this should probably be const or sth
Node* iterNode;
// Default constructor
iterator(): iterNode(nullptr)
{
}
// Constructor from a node
iterator(Node* n) : iterNode(n)
{
}
// Dereferences and gets the value of the node
const T& operator*() const
{
return iterNode->data;
}
// Prefix Increment points to the next element in the LinkedList
iterator* operator++()
{
iterNode = iterNode->next;
return this;
}
// Compares the two nodes of each iterator
bool operator !=(const iterator& other) const
{
return iterNode!= other.iterNode;
}
// Is the one nodes of the iterator equal to other iterator's node
bool operator ==(const iterator& other) const
{
return iterNode== other.iterNode;
}
// postFix increment returns an iterator to the previous node
// but points to the next element in the LinkedList
iterator operator++(int)
{
Node* previous = iterNode;
iterNode = iterNode->next;
return iterator(previous);
}
/*
iterator& operator--();
iterator operator--(int);
value_type& operator*() const;
*/
};
void push_back(const T& value)
{
Node* newNode = new Node(value);
if (!headNode)
{
std::cout << "!headNode" << std::endl;
headNode = newNode;
}
else
{
auto tmp = headNode;
Node* prevTmp = nullptr;
std::cout << "tmp" << std::endl;
while (tmp)
{
prevTmp = tmp;
tmp = tmp->next;
}
prevTmp->next = newNode;
}
size++;
}
void insert(iterator it, const T& value)
{
Node* newNode = new Node(value);
auto tmp = it.iterNode->next;
it.iterNode->next = newNode;
newNode->next = tmp;
}
iterator begin()
{
return iterator(headNode);
}
iterator end()
{
return iterator(nullptr);
}
iterator erase(iterator it)
{
// if deleting the first node then if this is the only element
// mark headNode as nullptr and delete the element else make
// the headNode point to the next element and delete the previous one.
if (headNode == it.iterNode)
{
std::cout << "headNode == it.iterNode" << std::endl;
auto nodeToDelete = it;
if (size == 1)
{
std::cout << "size == 1 " << std::endl;
headNode = nullptr;
}
else
{
std::cout << "size != 1 " << std::endl;
headNode = (++it)->iterNode;
}
std::cout << "deleting" << nodeToDelete.iterNode->data << std::endl;
delete nodeToDelete.iterNode;
}
else
{
auto prev = begin();
for (auto itr = begin(); itr!=end();itr++)
{
if (itr == it)
{
prev.iterNode->next = (++itr)->iterNode;
delete it.iterNode;
break;
}
prev = itr;
}
}
}
public:
LinkedList() : size(0), headNode(nullptr){
}
~LinkedList()
{
Node* current = headNode;
while (current)
{
Node* next = current->next;
delete current;
current = next;
}
}
private:
int size;
Node* headNode;
};
#endif //LINKED_LIST_HPP
This is testing using the g++ framework
#include "linkedlist.hpp"
#include <gtest/gtest.h>
TEST(LinkedListTest, PushBackTest)
{
LinkedList<int> myList;
LinkedList<int>::iterator it;
for (int i=1; i<=5; ++i) myList.push_back(i);
it = myList.begin();
ASSERT_EQ(*it, 1);
auto counter = 1;
for (auto itr = myList.begin(); itr!=myList.end();++itr)
{
ASSERT_EQ(*itr, counter);
counter++;
}
}
TEST(LinkedListTest, EraseTest)
{
LinkedList<int> myList;
LinkedList<int>::iterator it;
myList.push_back(1);
it = myList.begin();
// Deleting when is the only node and on the first place
myList.erase(it);
for (int i=2; i<=5; ++i) myList.push_back(i);
it = myList.begin();
auto counter = 2;
// Ensure values are as expected
for (auto itr = myList.begin(); itr!=myList.end();++itr)
{
ASSERT_EQ(*itr, counter);
counter++;
}
// Deleting when is not the only node but is on the first place
it = myList.begin();
std::cout << "Failing case" << std::endl;
myList.erase(it);
counter = 3;
// Ensure values are as expected
for (auto itr = myList.begin(); itr!=myList.end();++itr)
{
ASSERT_EQ(*itr, counter);
counter++;
}
it = myList.begin();
it++;
// Deleting an element from the middle and ensure everything is connected correctly
myList.erase(it);
counter = 3;
for (auto itr = myList.begin(); itr!=myList.end();++itr)
{
if (counter == 4) counter++;
ASSERT_EQ(*itr, counter);
counter++;
}
}
4 Answers 4
Remember the rule of three. The implicitly declared copy- and move- ctor and assignment are unsuitable for
LinkedList
.Two-space indentation doesn't really stand out. Consider doubling it.
If you add trace-output to your code, the least you can do is keeping it out of standard output. Error / Log stream is the proper destination. Also, design it for easy disabling and / or removal. Yes, that means using the preprocessor here.
Avoid
std::endl
. The manual flush is nearly always superfluous, just tanking performance. If you actually need it, be explicit and usestd::flush
.
See "What is the C++ iostreamendl
fiasco? ".If you use aggregate-initialization for your nodes, you need no custom ctor. Emplacing, move-constructing and copy-constructing would otherwise already be three ctors, or a templated one.
Don't make data-members public without good reason.
friend
can be used to selectively let some other code in.Use in-class-initialization for all members you can. It simplifies your ctors.
As a bonus, you can more often define them= default;
, making the type trivial, which has its own benefits.Comments are for describing your reasoning, the overarching design, and point out subtleties.
Don't bore your readers to tears by having them read the same thing twice, once in code and once in comment.Doc-comments which are extracted to create basic documentation must stand without the code, so should be treated a bit more lenient in this regard.
// this should probably be const or sth
No, really not. It is exactly as it should be. Or it would be, if you returnedT&
fromoperator*
.Currently, you are using the specific type half the time without rhyme or reason, consider leaning more on
auto
.If a function cannot throw by design, mark it
noexcept
. Others can pick it up and take advantage of the guarantee.Don't fear pointing at pointers.
It can significantly simplify some code:
template <class... Xs> void emplace_back(Xs&&...xs) { auto p = &headNode; while (*p) p = &p[0]->next; *p = new Node{{}, T(std::forward<Xs>(xs)...)}; // Using modified Node ++size; }
Expand the iterators, and the list-class itself. Currently, you just have the bare basics to have something move, but it is just a beginning.
Upgrade to C++20:
!=
will be synthesised from==
, so no need to define it.- Most any of your members can be made
constexpr
, allowing use at compile-time.
-
\$\begingroup\$ Don't make data-members public without good reason. friend can be used to selectively let some other code in. So you're suggesting to make the struct iterator a class and have a friend getter ? or make struct Node a class and have friend getters? Or both ? \$\endgroup\$user3638488– user36384882021年11月03日 21:07:34 +00:00Commented Nov 3, 2021 at 21:07
-
\$\begingroup\$ No, I mean that you can make your list a
friend
.Node
should be a private member of the list, containing only data-members. \$\endgroup\$Deduplicator– Deduplicator2021年11月03日 21:37:35 +00:00Commented Nov 3, 2021 at 21:37
What happens when you copy a LinkedList
object? GCC warns
269711.cpp:6:7: warning: ‘class LinkedList<int>’ has pointer data members [-Weffc++]
6 | class LinkedList {
| ^~~~~~~~~~
269711.cpp:6:7: warning: but does not declare ‘LinkedList<int>(const LinkedList<int>&)’ [-Weffc++]
269711.cpp:6:7: warning: or ‘operator=(const LinkedList<int>&)’ [-Weffc++]
269711.cpp:165:9: note: pointer member ‘LinkedList<int>::headNode’ declared here
165 | Node* headNode;
| ^~~~~~~~
And I don't see any tests that involve copying.
Some other warnings worth fixing:
269711.cpp:36:15: warning: prefix ‘LinkedList<T>::iterator* LinkedList<T>::iterator::operator++()’ should return ‘LinkedList<T>::iterator&’ [-Weffc++]
36 | iterator* operator++()
| ^~~~~~~~
269711.cpp: In instantiation of ‘LinkedList<T>::iterator LinkedList<T>::erase(LinkedList<T>::iterator) [with T = int]’:
269711.cpp:199:15: required from here
269711.cpp:147:3: warning: no return statement in function returning non-void [-Wreturn-type]
147 | }
| ^
269711.cpp: In instantiation of ‘LinkedList<T>::Node::Node(T) [with T = int]’:
269711.cpp:71:21: required from ‘void LinkedList<T>::push_back(const T&) [with T = int]’
269711.cpp:179:44: required from here
269711.cpp:10:7: warning: ‘LinkedList<int>::Node::data’ will be initialized after [-Wreorder]
10 | T data;
| ^~~~
269711.cpp:9:11: warning: ‘LinkedList<int>::Node* LinkedList<int>::Node::next’ [-Wreorder]
9 | Node* next;
| ^~~~
269711.cpp:11:5: warning: when initialized here [-Wreorder]
11 | Node(T data): data(data), next(nullptr){
| ^~~~
I really recommend enabling more warnings when you compile.
You need to publish iterator traits so you can use your class with standard algorithms! Your test code did not try passing your list's iterators to std::find
etc. Also ensure that you can use a LinkedList
in a range-based for
loop.
Don't write unnecessary or implicit type conversions.
return iterator(headNode);
does not need the explicit construction since the iterator
constructor is not marked explicit
. That is,
return headNode;
means the same thing.
Your destructor should just call clear
and not have to do the work directly.
The guard name LINKED_LIST_HPP
is not specific enough or unique. It could easily clash if this was used in a project with many libraries and nested library usage. If you must use such guards (#pragma once
is supported by all the major compilers) always use a UUID or the like.
Keep up the good work!
-
\$\begingroup\$ Considering that
.clear()
is the more rarely used one, shouldn't rather.clear()
be defined using the dtor? \$\endgroup\$Deduplicator– Deduplicator2021年11月04日 16:12:39 +00:00Commented Nov 4, 2021 at 16:12 -
1\$\begingroup\$ See what zwol has to say about
#pragma once
. \$\endgroup\$Deduplicator– Deduplicator2021年11月04日 16:18:28 +00:00Commented Nov 4, 2021 at 16:18 -
1\$\begingroup\$ How so, @deduplicator? The only way I could imagine is to invoke the destructor and then use in-place construction to reinstate the object. \$\endgroup\$uli– uli2021年11月04日 17:30:34 +00:00Commented Nov 4, 2021 at 17:30
-
1\$\begingroup\$ @uli
*this = LinkedList();
after defining the move-ctor. But yes, manual call to dtor +in-place-new using default-ctor would also work, as no virtual bases/functions to complicate inheritance. \$\endgroup\$Deduplicator– Deduplicator2021年11月04日 17:38:41 +00:00Commented Nov 4, 2021 at 17:38 -
\$\begingroup\$ Interesting, @Deduplicator, I hadn't thought of that. \$\endgroup\$uli– uli2021年11月04日 17:48:32 +00:00Commented Nov 4, 2021 at 17:48
Having Tests
Big plus! I have often wished that code I worked on would have more tests or any tests at all.
Advancing to last element
This locates the last node of the list in prevTmp
(not a speaking name, btw) and is unnecessarily complicated:
auto tmp = headNode;
Node* prevTmp = nullptr;
while (tmp)
{
prevTmp = tmp;
tmp = tmp->next;
}
One thing is that you already established that headNode
is not null (in code not shown here), so the first iteration of while (tmp)
will always execute the loop body. With that in mind, try this instead:
auto tmp = headNode;
while (tmp->next) {
tmp = tmp->next;
}
After that, you have an element where tmp->next
is null, which is almost self-documenting a free place at the end of the linked list.
Rule of Five
Your linked list has implicitly defined copy-ctor and assignment operator, which it shouldn't have. Disable them or implement them properly.
push_back()
Method
Why have this, which is the most inefficient way to use a singly-linked list? If you look at the STL (the origin of the C++ standard library), you won't find a push_front()
method in its vector
container either, exactly because it is inefficient! Same for your class, just don't implement it, but implement a push_front()
and pop_front()
instead!
insert()
Behaviour
I believe that insertion in the STL specifies the position before (!) which you insert, so you can call container.insert(value, container.end())
. Your version does it differently.
In any case, make sure to consider inserting into an empty container at positions container.begin()
and container.end()
in your tests!
-
1\$\begingroup\$ "insert() Behaviour: I believe that insertion in the STL specifies the position before (!) which you insert" Which is why the forward_list instead implements
.insert_after()
. Actually, it has many changes to accommodate only having forward-iterators without sacrificing efficiency. \$\endgroup\$Deduplicator– Deduplicator2021年11月04日 19:33:14 +00:00Commented Nov 4, 2021 at 19:33
Explore related questions
See similar questions with these tags.