0

I am trying to use smart pointers (std::unique_ptr) to create a singly linked list. Here is an example of a singly linked list with raw pointer.

struct Node {
  int data;
  Node *next = nullptr;
  Node(int data) : data{data}, next{nullptr} {}
  ~Node() { std::cout << "Destroy node with data: " << data << '\n'; }
};

void print_list(Node *head) {
  while (head != nullptr) {
    cout << head->data << " --> ";
    head = head->next;
  }
  cout << "nullptr" << std::endl;
}

void insert(Node *&head, int data) {
  Node *new_node = new Node{data};
  new_node->next = head;
  head = new_node;
}

int main(int argc, char *argv[]) {
  Node *head = nullptr;
  for (int i = 0; i < 5; ++i) {
    insert(head, i);
  }
  print_list(head);
  return 0;
}

The output is:

4 --> 3 --> 2 --> 1 --> 0 --> nullptr

Apparently there is memory leak in the above code (destructor is not called). Now I want to use smart pointer to achieve the same thing:

struct Node {
  int data = 0;
  std::unique_ptr<Node> next;
  Node(int data) : data{data}, next{nullptr} {}
  ~Node() { std::cout << "Destroy node with data: " << data << '\n'; }
};

void print_list(std::unique_ptr<Node> head) {
  while (head != nullptr) {
    std::cout << head->data << " --> ";
    head = std::move(head->next);
  }
  std::cout << "nullptr" << std::endl;
}

void insert(std::unique_ptr<Node> &&head, int data) {
  std::unique_ptr<Node> new_node{std::make_unique<Node>(data)};
  new_node->next = std::move(head);
  head = std::move(new_node);
}

// g++ -std=c++17 -Wall 2_1.cpp && ./a.out
int main(int argc, char *argv[]) {
  std::unique_ptr<Node> head{nullptr};
  for (int i = 0; i < 5; ++i) {
    insert(std::move(head), i);
  }
  print_list(std::move(head));
  return 0;
}

The output is:

4 --> Destroy node with data: 4
3 --> Destroy node with data: 3
2 --> Destroy node with data: 2
1 --> Destroy node with data: 1
0 --> Destroy node with data: 0
nullptr

We can observe that the life time of new_node ends when insert() returns. I would like to know if it's possible to use smart pointers to achieve singly linked list and retains the functions interface as above.

Lion Lai
  • 1,862
  • 2
  • 20
  • 41
  • Why did you change the `head` parameter to `insert()` from an lvalue reference to an rvalue reference? – JaMiT Nov 23 '22 at 05:36
  • *"We can observe that the life time of `new_node` ends when `insert()` returns."* -- true, but not as significant as you seem to think. When `insert()` returns, `new_node` should be managing a null pointer, so the end of its lifetime has no observable effect. (You could stick a line like `std::cout << "new_node.get() == " << new_node.get() << "\n";` at the end if `insert()` to verify this.) – JaMiT Nov 23 '22 at 05:40
  • Related: [Is unique_ptr guaranteed to store nullptr after move?](https://stackoverflow.com/questions/24061767/) – JaMiT Nov 23 '22 at 05:45
  • "I am trying to use smart pointers (std::unique_ptr) to create a singly linked list." You don't want this, unless your goal is to convince yourself that you don't want this – n. m. could be an AI Nov 23 '22 at 06:23
  • @n.m. You should explain why that is. Insert a million nodes and observe your list explode during destruction. – j6t Nov 23 '22 at 06:42
  • @j6t right. You need to write your own copy ctor and assignment, you need to write your own move ctor and assignment, you need to write your own dtor because of the recursive destruction, *and* the iterators must use raw pointers so the uniqueness of `next` is simply a lie. There is no win absolutely anywhere. – n. m. could be an AI Nov 23 '22 at 06:52

1 Answers1

1

First thing, there is a problem with your print_list implementation(for both version for unique_ptr only). With your print_list, every time you assign head with a different uniq_ptr, you are actually deallocating the only Node in head, which is not desired. Instead, in your print_list, you should first create a temporary pointer pointing to head, then only iterate on the temporary pointer.


Now onto your unique_ptr version, you don't have to pass a unique_ptr as rvalue reference, you can also pass it as lvalue reference. Instead, your function signature would probably look like:

void print_list(const std::unique_ptr<Node>& head);
void insert(std::unique_ptr<Node> &head, int data);

This allow you to call them without using std::move in your main.


Now on to definitions. For your insertion, what you have is you first create a new Node with the given value, then you assign the old head to new node's next, and make the new node as the new head:

void insert(std::unique_ptr<Node> &head, int data)
{
    // Use `auto` to avoid typing `....<Node>` twice
    auto new_node = std::make_unique<Node>(data);
    new_node->next = std::move(head);
    head = std::move(new_node);
}

Alternatively, you can also add one more parameter to Node's constructor:

Node(int data, std::unique_ptr<Node>&& next = nullptr) 
: data{data}, next{std::move(next)}
{}

Now you can simply create new_node like:

void insert(std::unique_ptr<Node> &head, int data)
{
    // No need to assign `Node::next` separately
    auto new_node = std::make_unique<Node>(data, std::move(head));
    head = std::move(new_node);
}

Or even assign the new node to head directly:

void insert(std::unique_ptr<Node> &head, int data)
{
    head = std::make_unique<Node>(data, std::move(head));
}

For print_list, we should first create a temporary pointer that points to the underlying object of head, then iterate the list by assigning the temporary pointer to its next object's underlying object:

void print_list(const std::unique_ptr<Node>& head)
{
    // Create a pointing to the underlying object
    // You can use `.get()` to get the underlying pointer
    auto current = head.get();

    // No need to explicit compare pointer types to `nullptr`
    while (current) {
        std::cout << current->data << " --> ";
        // Make `current` point to the next underlying object
        current = current->next.get();
    }
    std::cout << "nullptr" << std::endl;
}

Now your main would look like:

int main(int, char *[]) {
    std::unique_ptr<Node> head;
    for (int i = 0; i < 5; ++i) {
        insert(head, i);
    }
    print_list(head);
    return 0;
}

Demo

Ranoiaetep
  • 5,872
  • 1
  • 14
  • 39
  • 2
    I'd even suggest that the signature should look like `void print_list(const Node& head)`, assuming we don't try to encapsulate the `Node` further into a dedicated `LinkedList` class. `print_list` doesn't have any obvious reason to care about the ownership structure of the node it's printing. – Nathan Pierson Nov 23 '22 at 05:22
  • 1
    @NathanPierson I agree. It does alternate the function call slightly however, so not sure if it's desired for OP – Ranoiaetep Nov 23 '22 at 05:27
  • @Ranoiaetep Thanks for the detailed answer. There is one thing kind of intriguing me is that in the raw pointer version of `print_list`, the head is actually not lost. I can call `print_list(head);` twice in a row and they both print the complete list. See this link: https://godbolt.org/z/hP8fbGEK1 – Lion Lai Nov 23 '22 at 05:45
  • Yeah, because raw pointers don't have ownership over the things they point to. When `head` is a pointer and you write `head = head->next;`, nothing much happens to whatever used to be pointed to by `head->next`. When you do `head = std::move(head->next);` and `head` is a `unique_ptr`, then you have to look at what happens with `unique_ptr`'s move-assignment operation to see what happens to `head->next`. – Nathan Pierson Nov 23 '22 at 05:47
  • 1
    ok, i think i understand what happens. In the raw pointer version of `void print_list(Node *head)`, `head` is a pass-by-value pointer. `head` is indeed modified in `print_list` but not modified in `main`. However, if changing to `void print_list(Node *&head)`, now `head` is a pass-by-reference pointer, so whatever happens to `head` in `print_list` will also change `head` in `main`. – Lion Lai Nov 23 '22 at 05:55
  • @LionLai You are correct. I have misspoken originally. As you said `print_list(Node*)` passes `head` by copy, so the original `head` does not change. But like ***@NathanPierson*** said, when you reassign the `unique_ptr`, it will deallocate the original object in it first. – Ranoiaetep Nov 23 '22 at 06:06