7

I wanted to create a list data structure with an iterator class in it. Everything works well but when I declare a move assignment operator the program doesn't compile if it's using the C++14 or C++11 standards, but works fine in C++17, C++2a.

list.h:

#pragma once

#include <iostream>

template <typename T>
class list {
    struct node {
        node(T data, node* prev = nullptr, node* next = nullptr)
            : data{ data }, prev{ prev }, next{ next } {}
        
        T data;
        node* prev;
        node* next;
    };
public:
    struct iterator {
        template <typename>
        friend class list;

        explicit iterator(node *_node = nullptr) 
            : _node(_node) {}

        iterator& operator=(iterator const &it) {
            _node = it._node;
        
            return *this;
        }
       
        iterator& operator=(iterator &&it) {
            // does nothing
            return *this;
        }

        T& operator*() {
            return _node->data;
        }

    private:
        node *_node;
    };

    list(T data) {
        Head = Tail = new node(data);

        size = 1;
    }

    iterator begin() {
        return iterator(Head);
    }
private:
    node* Head;
    node* Tail;

    int size;
};

main.cpp:

#include "list.h"

int main() {

    list<int> lst(100);

    std::cout << *lst.begin() << std::endl;

}

It's a stripped-down version but it's enough to describe the problem.

Compiler error and note message:

error: use of deleted function ‘constexpr list::iterator::iterator(const list::iterator&)’
     return iterator(Head);
                         ^

note: ‘constexpr list::iterator::iterator(const list::iterator&)’ is implicitly declared as
       deleted because ‘list::iterator’ declares a move constructor or move assignment operator
     struct iterator {
        ^~~~~~~~
Boann
  • 48,794
  • 16
  • 117
  • 146
Kanony
  • 509
  • 2
  • 12
  • 12
    You don't have a copy constructor for `iterator`. You only defined the assignment operators. – François Andrieux Aug 30 '20 at 13:58
  • 2
    Why do you have a move-assignment that does nothing? That seems like a bad decision. You might be safer just using `= default` at the constructors and assignment operators, since you appear to only need default behavior – Human-Compiler Aug 30 '20 at 14:14
  • 2
    Yeah I know it's meaningless, but it's a long story and my goal is to find out why it's an error not adding method or overloading operator – Kanony Aug 30 '20 at 14:21
  • 1
    @Human-Compiler -- that move-assignment operator is sufficient for demonstrating the problem. Don't thump people for writing minimal examples. – Pete Becker Aug 30 '20 at 16:26
  • @PeteBecker I'm not thumping anything; it's a valid question since it explicitly documents "`// does nothing` which is the same amount of lines as `_node = it._node` would be (and even more lines than `= default` would be). – Human-Compiler Aug 30 '20 at 21:18

1 Answers1

14
return iterator(Head);

Why this works in C++17 is because in C++17 this is guaranteed copy elision. No copy or move takes place here.

In a return statement, when the operand is a prvalue of the same class type (ignoring cv-qualification) as the function return type:

Prior to C++17, this requires a constructor, but the defined move assignment operator deletes it, it's also deleted in C++17, but copy elision does not require it (ibid):

The copy/move constructors need not be present or accessible

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
  • Historical footnote on guaranteed copy elision: many compilers had supported that as an optimization option for decades. C++17 finally made it official, so that behavior can be expected and relied upon. (A bit disappointing it took so long, but better late than never.) – Eljay Aug 30 '20 at 16:46
  • @Sam Varshavchik but why it deletes? – Kanony Aug 30 '20 at 18:19
  • When certain operators/constructors get defined, other default ones get deleted. For example, defining an explicit copy constructor deletes the implicit move constructor. There must be a reason for a specific copy-constructor, then. Therefore, things that would normally be moved must be copied, instead. If you also want a move constructor, it's your responsibilty to define it. The rules for which things get deleted when other things get defined are ...complicated, but that's the underlying reason: to preserve the intended semantics. Move semantics are basically an all or nothing proposition. – Sam Varshavchik Aug 30 '20 at 18:30
  • @Sam Varshavchik I got you, but my question is different. I explicitly declared both copy move assignment operators and my problem is one deletes another. I can't declare move assignment operator explicitly because the compiler will implicitly delete my explicitly defined copy assignment operator. They are not default. – Kanony Aug 30 '20 at 18:49
  • Copy/move assignments are not the same thing as copy/move constructors, which is what's required here (and what gets deleted). – Sam Varshavchik Aug 30 '20 at 18:55
  • @Sam Varshavchik sorry about previous comment just forget that. When I declare that deleted copy constructor it compiles, but why we need that if I just use another constructor, even putting breakpoint to that line shows that copy constructor never will be invoked. Why we need that constructor which will never be invoked? – Kanony Aug 30 '20 at 19:08
  • 2
    Before C++17 copy elision was optional, and up to the compiler, but even if the compiler elides the copy, as is yours, the relevant constructor must still be accessible. See the same link I cited: "this is an optimization: even when it takes place and the copy/move (since C++11) constructor is not called, it still must be present and accessible (as if no optimization happened at all), otherwise the program is ill-formed". C++17 makes copy elision mandatory in this situation, even if the constructors are inaccessible. – Sam Varshavchik Aug 30 '20 at 19:19