Search code examples
c++linked-listdesign-principles

Intrinsic benefit of using a LinkedList class to point to head Node vs. just using Node*


We can define a LinkedListNode as below:

template <typename T>
struct LinkedListNode {
    T val;
    LinkedListNode* next;
    LinkedListNode() : val{}, next(nullptr) {}
    LinkedListNode(T x) : val{ x }, next(nullptr) {}
    LinkedListNode(T x, LinkedListNode* next) : val{ x }, next(next) {}
};

If we want to define a function that takes a "Linked List", we have two options. First, we could pass a LinkedListNode* to the function.

template <typename T>
int func(LinkedListNode<T>* node);

Second, we could define a LinkedList class that holds a pointer to the "head" node. Then we could define a function that takes a LinkedList.

template <typename T>
struct LinkedList {
    LinkedListNode<T>* head;
    // other member functions
};

template <typename T>
int func(LinkedList<T>& llist);

One reason the second appears preferable because it might allow better encapsulation of functions that modify a "Linked List". For example, a FindMax that takes a LinkedListNode* might better fit as a member function of LinkedList than as a member function of LinkedListNode.

What concrete reasons are there to prefer one over the other? I'm especially interested in reasons you might prefer to just use LinkedListNode*s.


Solution

  • Defining an actual LinkedList type allows you to directly support operations that would be relatively difficult to support by just passing around a pointer to a node.

    One comment has already mentioned storing the size of the linked list as a member, so you can have a function return the size of the linked list in constant time. That is a useful thing to do, but I think it only hints at the real point, which (in my opinion) is having things that apply to the linked list as a whole, rather than just operations on individual nodes.

    In C++, one obvious possibility here is having a destructor that properly destroys a complete linked list when it goes out of scope.

    int foo() { 
        LinkedList a;
        // code that uses `a`
    
    } // <-- here `a` goes out of scope, and should be destroyed
    

    One of the big features of C++ as a whole is deterministic destruction, and its support for that is based on destructors that run when objects go out of scope.

    With a linked list, you'd (at least normally) plan on all the nodes in the linked list being allocated dynamically. If you just use a pointer to node, it'll be up to you to keep track of when you no longer need/want a particular linked list, and manually destroy all the nodes in the linked list when it's no longer needed.

    By creating a linked-list class, you get the benefit of deterministic destruction, so you no longer need to keep track of when a list is no longer needed--the compiler tracks that for you, and when it goes out of scope, it gets destroyed automatically.

    I'd also expect a linked list to support copy construction, move construction, copy assignment, and move assignment--and probably also a few things like comparison (at least for in/equality, and possibly ordering). And all of these require a fair amount of manual intervention if you decide to implement your linked list as a pointer to a node, instead of having an actual linked list class.

    As such, I'd say if you really want to use C++ (even close to how it's intended to work) creating a class to encapsulate your linked list is an absolute necessity. As long as you're just passing around pointers to nodes, what you're writing is fundamentally C (even if it may use some features specific to C++ so a C compiler won't accept it).