patterncppMinor
A doubly linked list implementation using C++ smart pointers
Viewed 0 times
implementationsmartdoublyusingpointerslistlinked
Problem
I am new to "modern" C++ and decided to learn about smart pointers by implementing a doubly linked list.
I have a few problems with this code: Should the tail pointer be a weak pointer? Should the head pointer be a unique pointer (If yes, then the node after head will have a weak_ptr to the head node, causing a unique_ptr and a weak pointer pointing to the same object)?
```
#include
#include
template
class LinkedList {
public:
LinkedList();
int GetLength() const;
void Print() const;
// TODO: Implement and return an iterator.
void Append(T data);
// TODO: Implement and return an iterator.
void Prepend(T data);
bool Delete(T data);
private:
struct ListNode {
T data;
std::shared_ptr next;
std::weak_ptr prev;
ListNode(T data) : data(data) {}
};
int length;
std::shared_ptr head;
std::shared_ptr tail;
void Delete(std::shared_ptr ptr);
};
template
LinkedList::LinkedList()
: length(0) {}
template
int LinkedList::GetLength() const {
return length;
}
template
void LinkedList::Print() const {
std::shared_ptr h(head);
std::cout data next;
}
std::cout
void LinkedList::Append(T data) {
std::shared_ptr node_ptr(std::make_shared(data));
node_ptr->prev = tail;
if (tail) {
tail->next = node_ptr;
tail = std::move(node_ptr);
} else {
tail = std::move(node_ptr);
head = tail;
}
length++;
}
template
void LinkedList::Prepend(T data) {
std::shared_ptr node_ptr(std::make_shared(data));
node_ptr->next = head;
if (head) {
head->prev = node_ptr;
head = std::move(node_ptr);
} else {
head = std::move(node_ptr);
tail = head;
}
length++;
}
template
bool LinkedList::Delete(T data) {
std::shared_ptr ptr = head;
while (ptr) {
if (ptr->data == data) {
Delete(ptr);
return true;
}
ptr = ptr->next;
}
return false;
}
template
void LinkedList::Delete(std::shared_ptr ptr) {
auto shared_prev = (ptr->prev).lock();
if (head == ptr) {
I have a few problems with this code: Should the tail pointer be a weak pointer? Should the head pointer be a unique pointer (If yes, then the node after head will have a weak_ptr to the head node, causing a unique_ptr and a weak pointer pointing to the same object)?
```
#include
#include
template
class LinkedList {
public:
LinkedList();
int GetLength() const;
void Print() const;
// TODO: Implement and return an iterator.
void Append(T data);
// TODO: Implement and return an iterator.
void Prepend(T data);
bool Delete(T data);
private:
struct ListNode {
T data;
std::shared_ptr next;
std::weak_ptr prev;
ListNode(T data) : data(data) {}
};
int length;
std::shared_ptr head;
std::shared_ptr tail;
void Delete(std::shared_ptr ptr);
};
template
LinkedList::LinkedList()
: length(0) {}
template
int LinkedList::GetLength() const {
return length;
}
template
void LinkedList::Print() const {
std::shared_ptr h(head);
std::cout data next;
}
std::cout
void LinkedList::Append(T data) {
std::shared_ptr node_ptr(std::make_shared(data));
node_ptr->prev = tail;
if (tail) {
tail->next = node_ptr;
tail = std::move(node_ptr);
} else {
tail = std::move(node_ptr);
head = tail;
}
length++;
}
template
void LinkedList::Prepend(T data) {
std::shared_ptr node_ptr(std::make_shared(data));
node_ptr->next = head;
if (head) {
head->prev = node_ptr;
head = std::move(node_ptr);
} else {
head = std::move(node_ptr);
tail = head;
}
length++;
}
template
bool LinkedList::Delete(T data) {
std::shared_ptr ptr = head;
while (ptr) {
if (ptr->data == data) {
Delete(ptr);
return true;
}
ptr = ptr->next;
}
return false;
}
template
void LinkedList::Delete(std::shared_ptr ptr) {
auto shared_prev = (ptr->prev).lock();
if (head == ptr) {
Solution
This review will focus on
The constructor takes its argument by copy, whether the argument is a
Before:
After:
Additional remarks:
The
Currently, when
This is inefficient and limits the number of elements that your list can contain to the recursive limit that your stack allows; it is reason enough to remove ownership semantics by using raw pointers over
To deal with this change, you simply define a destructor for
Note that this will require you to provide the proper move and copy operations for your
From looking at your declaration (not your implementation), here's some points for:
ListNode.ListNodeThe constructor takes its argument by copy, whether the argument is a
char or a 1'000'000 element std::vector<>. Additionally, there is no exception specification.Before:
ListNode(T data) : data(data) {}
// ^^^^^^^^^^ another copy is made here
// ^^^^^^ a copy is made here- In order to prevent the first copy, you should pass it as
T const&.
- Use type traits to add an exception specification.
After:
#include // for the std::is_nothrow_xxx family of template traits
ListNode(T const& data) // the argument is now taken by const&
noexcept(std::is_nothrow_copy_constructible::value) // correct exception specification
: data(data) // now, only one copy is made: here.
{}Additional remarks:
- Consider adding a constructor that takes a
T&&for efficiency.
- Consider having only one constructor that directly initializes
databy using a variadic template and perfect forwarding.
The
std::unique_ptr<> and std::shared_ptr<> types model ownership semantics. Meaning that the smart pointer instance itself owns the memory it points to. In a linked list, the list owns the nodes and their values.Currently, when
ListNode's destructor is called, it will start a recursive chain of calls: freeing a node requires freeing its next data member, which requires freeing the next data member's next data member, and so on.This is inefficient and limits the number of elements that your list can contain to the recursive limit that your stack allows; it is reason enough to remove ownership semantics by using raw pointers over
std::shared_ptr<>.To deal with this change, you simply define a destructor for
LinkedList:~LinkedList() noexcept(std::is_nothrow_destructible::value)
{
for (ListNode* current = head, *next; current;)
{
next = current->next;
delete current;
current = next;
}
}Note that this will require you to provide the proper move and copy operations for your
LinkedList type by hand as well.From looking at your declaration (not your implementation), here's some points for:
LinkedList- Provide a
size_typealias for your container. Assuming I want to fill the linked list up to its theoretical maximum size, I need to know how many elements it can support. This is determined by the maximum value of thelengthdata type. The only way to guess at its type is by checkingGetLength()'s return type.
- The
Append(),Prepend()andDelete()member functions all take their arguments by copy just likeListNode's old constructor. Consider similar fixes without forgetting to adapt the exception specification according to the operations you do.
- Your data members comply with the requirements needed to mark your default constructor as
constexprand asnoexcept.
- Printing should not be a member function. Instead, provide
operator
- As Loki Astari mentioned, consider using sentinel nodes in order to avoid having to check for nullptr
values.
- Provide an exception specification for non-throwing functions such as GetLength()`.
Code Snippets
ListNode(T data) : data(data) {}
// ^^^^^^^^^^ another copy is made here
// ^^^^^^ a copy is made here#include <type_traits> // for the std::is_nothrow_xxx family of template traits
ListNode(T const& data) // the argument is now taken by const&
noexcept(std::is_nothrow_copy_constructible<T>::value) // correct exception specification
: data(data) // now, only one copy is made: here.
{}~LinkedList() noexcept(std::is_nothrow_destructible<T>::value)
{
for (ListNode* current = head, *next; current;)
{
next = current->next;
delete current;
current = next;
}
}Context
StackExchange Code Review Q#143021, answer score: 6
Revisions (0)
No revisions yet.