-1

I am creating a doubly linked list template and I have to write 2 constructors where we pass in a linked list, and then construct a new list that has the same number of nodes as well as the same numbers in those nodes. The memory locations of the nodes will be the only difference between the two.

I have two constructors. One is a copy constructor that accepts a List& and a move constructor that accepts a List&&:

List(const List &rhs);
List(List &&rhs);

Now my question is, the code is the same for both since it's not a shallow copy, right? The distinction is only for compilation purposes? My understanding is I do not need to std::move to implement the second constructor.

The purpose of the second constructor is to be able to create a list that uses a temporary object as its argument such as: L1 = L2+ L3 where L2+L3 becomes a temporary list that is deleted once its been assigned to L1.

double-beep
  • 5,031
  • 17
  • 33
  • 41
  • 3
    Why would you *copy* anything in `List(List &&rhs);`? The whole point of the overload is to steal (move) the contents of the argument. – cigien Sep 23 '20 at 22:35
  • I suspect you've got to write the code of the two constructors according to how you want them to behave. If you want them to behave the same, then what's the point of defining the move ctor? Just define the copy ctor, and the move ctor will be deleted. You don't even want to use it... – Enlico Sep 23 '20 at 22:36

2 Answers2

2

If you're only doing a shallow copy, particularly of non-pointer values (or any other allocated resource), you should have no need for a move constructor.

jkb
  • 2,376
  • 1
  • 9
  • 12
1

the code is the same for both since it's not a shallow copy, right?

The List(const List &rhs) copy constructor is expected to deep copy (not shallow copy) the data (not the nodes themselves) from rhs to this, but otherwise leave rhs intact and untouched.

The List(List &&rhs) move constructor is expected to move (steal) the nodes themselves from rhs to this and leave rhs in an empty state.

So, no, they would not be using the same code, not even close.

Same thing with the operator= copy assignment and move assignment operators (which are commonly implemented utilizing the copy/move constructors via the copy-swap idiom to avoid code duplication).

For example:

template<typename T>
class List
{
private:
    struct node
    {
        T data;
        node *previous;
        node *next;
    };

    node *head = nullptr;
    node *tail = nullptr;
    size_t size = 0;

public:
    // default constructor
    List() = default;

    // copy constructor
    List(const List &rhs)
    {
        node **newNode = &head;
        for(node *curNode = rhs.head; curNode; curNode = curNode->next)
        {
            *newNode = new node{curNode->data, tail, nullptr};
            tail = *newNode;
            ++size;
            newNode = &(tail->next);
        }
    }

    // move constructor
    List(List &&rhs)
        : head(rhs.head), tail(rhs.tail), size(rhs.size)
    {
        rhs.head = rhs.tail = nullptr;
        rhs.size = 0;
    }

    // destructor
    ~List()
    {
        node *curNode = head;
        while (curNode) {
            node *next = curNode->next;
            delete curNode;
            curNode = next;
        }
    }

    // copy assignment
    List& operator=(const List &rhs)
    {
        if (this != &rhs) {
            List tmp(rhs);
            std::swap(head, tmp.head);
        }
        return *this;
    }

    // move assignment
    List& operator=(List &&rhs)
    {
        List tmp(std::move(rhs));
        std::swap(head, tmp.head);
        return *this;
    }

    /*
    Alternatively, you can use just 1 implementation of operator=
    for both copy and move assignments, by taking the input parameter
    *by value* and letting the compiler decide which constructor
    to call to initialize it, based on the type of input being assigned:

    // copy/move assignment
    List& operator=(List rhs)
    {
        List tmp(std::move(rhs));
        std::swap(head, tmp.head);
        return *this;
    }
    */
};

An operation like L1 = L2 + L3 would have to implement operator+ that takes 2 List objects as input and returns a new List object with data (not nodes) that is copied from both L2 and L3, eg:

template<typename T>
class List
{
    ...
public:
    ...

    List& operator+=(const List &rhs)
    {
        if (rhs.head) {
            List tmp(rhs);
            node **ptr = (tail) ? &(tail->next) : &head;
            *ptr = tmp.head; tmp.head = nullptr;
            tail = tmp.tail; tmp.tail = nullptr;
            size += tmp.size;
        }
        return *this;
    }

    // operator+ can be implemented either as a class member
    // using *this as the left-hand operand...
    List operator+(const List &rhs) const
    {
        List res(*this);
        res += rhs;
        return res;
    }
};

// Or, operator+ can be implemented as a non-member function...
List operator+(const List &lhs, const List &rhs)
{
    List res(lhs);
    res += rhs;
    return res;
}

The returned List would be a temporary object, aka an rvalue, which would then be assigned to L1 using the operator=(List&&) move assignment operator (or, the operator=(List) assignment operator using the List(List&&) move constructor).

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Obviously, you would need to implement `operator+` before you can test `L1 = L2 + L3`. But just to test the move (not copy) constructor, you can simply use something like this: `List L1; add some nodes to L1 ...; List L2(std::move(L1));` – Remy Lebeau Sep 23 '20 at 23:25
  • Not for the constructors themselves, no. Only for `operator=`. This way, the caller can assign an lvalue or an rvalue, and the compiler will construct the `rhs` parameter using either the copy constructor or the move constructor, respectively. And then the `operator=` can simply move from `rhs` – Remy Lebeau Sep 23 '20 at 23:29
  • The move constructor merely sets `this->head`/`this->tail` to point at the existing nodes in `rhs`, and then resets `rhs.head`/`rhs.tail` to `nullptr`, thus *stealing* the nodes from `rhs`. `this` is now responsible for managing the nodes that were previously managed by `rhs`. The nodes themselves stay in memory right where they are. **This is the whole point of move semantics.** `rhs` is not gone, merely reset to a "valid but indeterminate state" by the move constructor (or move assignment operator) - that state being an empty list - where `rhs` will be destructed at some later time. – Remy Lebeau Sep 24 '20 at 00:10
  • @Zevias yes, that is exactly what you need to do. If `T` supports move semantics (`std::string`, etc) then `new Node(std::move(val))` will actually *move* `val`'s data into the node's data. Otherwise, if `T` does not support move semantics (`int`, etc), then `new Node(std::move(val))` will end up *copying* `val` instead. Either way, make sure that `Node` has a *move constructor* implemented. – Remy Lebeau Sep 24 '20 at 23:12
  • @RemyLebeau I reread my provided List.h file and actually noticed my nested Node class (nested inside List) has a constructor as follows: ```Node(T && d, Node *p = nullptr, Node *n = nullptr): data{std::move(d)}, prev{p}, next{n} {}```. There is an identical one in place except it takes in a const &d rather than &&d. Does this mean my push_front move would not work when I call the Node constructor as ```Node(std::move(val),p,n)```t since the ```Node``` constructor already has a move for the data in one of the constructors or would it simply be redundant? –  Sep 24 '20 at 23:26