1

I am still trying to learn more about copy and move constructors. I have a linked list class that I want to deep copy using copy and move constructors but I'm having issues. First, to deep copy List class, do I only copy head_ and tail_ in the constructors. I know the code is horrendous, maybe I shouldn't jump into high-level stuff right away.

Any help is appreciated!

template<typename T>
class List
{
public:

    class Node {
    public:
        Node(T value) : value_(value) {}

        T value_;
        Node* next_;
        Node* prev_;
    };

    Node* head_;
    Node* tail_;


    //! Default constructor
    List() :tail_(nullptr) {}

    //! Copy constructor
    List(const List& lst) : head_(nullptr) {

        //not sure what goes in here
        }
    }

    //! Move constructor
    List(List&& move) {
        head_ = move.head_;
        move.head_ = nullptr;
        tail_ = move.tail_;
        move.tail_ = nullptr;
    }


//! Copy assignment operator
    List& operator= (const List& list) {
        
        tail_ = nullptr;
        head_ = tail_;
    
        Node* current = list.head_;
        Node* next = list.head_->next_;
        Node* replace = head_;
        while (next != list.tail_) {
            current = current->next_;
            next = next->next_;
            replace->next_ = tail_;
            replace->next_->value_;
            replace = replace->next_;
        }
        return *this;
    }
    

    //! Move assignment operator
    List& operator= (List&& other) {        
        
        tail_ = nullptr;
        head_ = tail_;

        head_->next_ = other.head_->next_;
        Node* current = other.head_;
        Node* next = other.head_->next_;

        while (next != other.tail_) {

            current = current->next_;
            next = next->next_;
        }
        current->next_ = tail_;
        other.head_->next_ = other.tail_;

        return *this;
    }

Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
SegFaulter
  • 61
  • 7
  • *//not sure what goes in here* -- How are you able to test your class without public member functions to add items to the list? Also, the copy constructor and the destructor should be the first two functions you would write, and leave the assignment operator for later. – PaulMcKenzie Jun 23 '20 at 18:43
  • I figure I wouldn't include that to make the code less cluttered but I do have insert members for the linked list – SegFaulter Jun 23 '20 at 18:45
  • *//not sure what goes in here* -- Consider moving all of that code you have in your assignment operator and placing it there. – PaulMcKenzie Jun 23 '20 at 18:49
  • Side note: If you do the copy and move constructors first, you can take advantage of the [Copy and Swap Idiom](https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom) and make the assignment operators brutally simple. – user4581301 Jun 23 '20 at 18:51
  • 1
    Note: `tail_ = nullptr; head_ = tail_;` leaked whatever was in the list. – user4581301 Jun 23 '20 at 18:54
  • @HazemEnnaceur -- If you have a function that adds to the back of the list, then the copy constructor is very easy. That's why I would have liked to see your insertion function. All you would have needed to do is repeatedly call `addToBack` or whatever your function is called, in a loop until the passed-in list hits the tail. That's 4 lines of code. – PaulMcKenzie Jun 23 '20 at 19:04
  • @PaulMcKenzie thanks for the tip, I will see how I can implement it and get back to you :D – SegFaulter Jun 23 '20 at 19:14

2 Answers2

2

Because List tracks the tail, you can call a function that adds items to the end of the list without the added overhead of iterating to the end of the list.

List(const List& lst) : head_(nullptr), tail_(nullptr) 
{
    Node * cur = lst.head_; //get first source item.
    while (cur) // if there is a source item to copy
    {
        push_back(cur->value_); // stick the item on the end of this list
        cur = cur->next_; // get next source item
    }
}

where push_back looks something like

void push_back(T value)
{
    Node * newnode = new Node(value, tail_, nullptr); //make and link new tail node

    if (tail_)
    {
        tail_->next_ = newnode; // link in new node
    }
    else
    {
         head_ = newnode;
    }
    tail_ = newnode; // update tail
}

and Node picked up a new constructor to simplify insertion:

Node(T value, 
     Node * prev, 
     Node * next) : value_(value), prev_(prev), next_(next) 
{
}

Note: In the assignment operators,

tail_ = nullptr;
head_ = tail_;

cut the pointers to any data that was in the linked list, leaking those Nodes. You need to free these nodes before replacing them. The Copy and Swap Idiom (What is the copy-and-swap idiom?) makes this easy by using the copy construction and destruction of a local variable to automate the process.

user4581301
  • 33,082
  • 7
  • 33
  • 54
  • thanks it makes sense, so now if I wanted to overload the copy assignment operator, it would look something like this? `List& operator= (const List& list_copy){ List tmp(list_copy); return *this;}` – SegFaulter Jun 23 '20 at 19:34
  • 1
    @user4581301 Your `push_back()` is not updating `head_` if the list is empty. – Remy Lebeau Jun 23 '20 at 19:34
  • Just spotted that myself. Thanks Remy. – user4581301 Jun 23 '20 at 19:36
  • @HazemEnnaceur -- You forgot to swap `tmp` members with `*this`'s members. That's why the technique is called copy and **swap**. – PaulMcKenzie Jun 23 '20 at 19:40
  • @HazemEnnaceur assignment has the added problem that the list may already contain data that must be disposed of. You can iterate through the list and `delete` all of the existing `Nodes` before iterating through the source list and inserting the new nodes, but again I'm going to push Copy and Swap because it's about as close to foolproof as you get in life. – user4581301 Jun 23 '20 at 19:40
2

Here is my five cents.:)

The demonstrative program below shows how the copy constructor, move constructor, copy assignment operator, move assignment operator, and the destructor can be implemented including some other auxiliary functions.

#include <iostream>
#include <utility>
#include <functional>
#include <iterator>

template<typename T>
class List
{
private:
    struct Node 
    {
        T value;
        Node *prev;
        Node *next;
    } *head = nullptr, *tail = nullptr;

    void copy( const List &list )
    {
        if ( list.head )
        {
            head = tail = new Node { list.head->value, nullptr, nullptr };

            for ( Node *current = list.head->next; current; current = current->next )
            {
                tail = tail->next = new Node { current->value, tail, nullptr };             
            }
        }           
    }
    
public:
    //! Default constructor
    List() = default;

    //! Copy constructor
    List( const List &list )
    {
        copy( list );
    }

    //  Constructor with iterators
    template <typename InputIterator>
    List( InputIterator first, InputIterator last )
    {
        if ( first != last )
        {
            head = tail = new Node { *first, nullptr, nullptr };

            while ( ++first != last )
            {
                tail = tail->next = new Node { *first, tail, nullptr };             
            }
        }
    }

    //  Destructor
    ~List()
    {
        clear();
    }
    
    //! Move constructor
    List( List &&list ) 
    {
        std::swap( head, list.head );
        std::swap( tail, list.tail );
    }

    //! Copy assignment operator
    List & operator =( const List &list ) 
    {
        clear();
        copy( list );
        
        return *this;
    }
    
    //! Move assignment operator
    List & operator =( List &&list ) 
    {
        std::swap( head, list.head );
        std::swap( tail, list.tail );
        
        return *this;
    }
    
    void clear()
    {
        while ( head )
        {
            delete std::exchange( head, head->next );
        }
        
        tail = head;
    }
    
    void push_front( const T &value )
    {
        head = new Node{ value, nullptr, head };

        if ( !tail )
        {
            tail = head;
        }
        else
        {
            head->next->prev = head;
        }
    }

    void push_back( const T &value )
    {
        Node *new_node = new Node{ value, tail, nullptr };

        if ( tail )
        {
            tail = tail->next = new_node;
        }
        else
        {
            head = tail = new_node;
        }
    }
    
    friend std::ostream & operator <<( std::ostream &os, const List &list )
    {
        for ( Node *current = list.head; current; current = current->next )
        {
            os << current->value << " -> ";
        }
        
        return os << "null";
    }
};

int main()
{
    int a[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    
    List<int> list1( std::begin( a ), std::end( a ) );
    
    std::cout << list1 << '\n';
    
    list1 = List<int>( std::rbegin( a ), std::rend( a ) );

    std::cout << list1 << '\n';
    
} 

The program output is

0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> null
9 -> 8 -> 7 -> 6 -> 5 -> 4 -> 3 -> 2 -> 1 -> 0 -> null

For example in this statement

list1 = List<int>( std::rbegin( a ), std::rend( a ) );

there is used the move assignment operator.

Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335