Fehler C2280 move-ctor deleted und std::push_back





  • @SoIntMan sagte in Fehler C2280 move-ctor deleted und std::push_back:

    EDIT: Danke für den "godbold.org" linke, sehr hilfreich;()

    Die URL ist https://godbolt.org/ ... "godbold" gibt es nicht. Und ja, sehr angenehm/nützlich die Seite.



  • Interessanter Post von Howard Hinnant dort:
    = delete bedeutet gerade nicht, daß diese Überladung nicht herangezogen wird, sondern explizit, daß man dort einen Kompilerfehler erzwingen will, sobald ein passender Parameter benutzt wird.

    Also einfach nur die anderen Überladungen anbieten (und damit ist der Move-Konstruktor automatisch nicht benutzbar).



  • @Th69 sagte in Fehler C2280 move-ctor deleted und std::push_back:

    Interessanter Post von Howard Hinnant dort:
    = delete bedeutet gerade nicht, daß diese Überladung nicht herangezogen wird, sondern explizit, daß man dort einen Kompilerfehler erzwingen will, sobald ein passender Parameter benutzt wird.

    Also einfach nur die anderen Überladungen anbieten (und damit ist der Move-Konstruktor automatisch nicht benutzbar).

    Das ist korrekt, zu mindestens mit gcc 12, liefert folgender Beispielcode, dass immer copy constructor/assignment operator aufgerufen wird

    #include <iostream>
    #include <vector>
    
    class Slice
    {
        public:
    
            Slice(const size_t& id, const size_t& maxSize) {std::cout<<"ctor\n";}
    
            Slice(const Slice& src){std::cout<<"copy ctor\n";}
            Slice& operator=(const Slice &src){std::cout<<"copy assignment\n"; return *this;}
            virtual ~Slice() {}
          
        private:
    
    };
    
    int main()
    {
        std::vector<Slice> list;
        std::cout<<"begin x\n";
        Slice x(0,0);
        std::cout<<"begin y\n";
        Slice y(x);
        std::cout<<"begin y2\n";
        Slice y2 = std::move(x);
        std::cout<<"begin y2 assign rvalue\n";
        y2 = std::move(y);
        std::cout<<"begin push_back\n";
        list.push_back(y2);
        std::cout<<"end\n"; 
        return 0;
    }
    
    

    Ausgabe:

    begin x
    ctor
    begin y
    copy ctor
    begin y2
    copy ctor
    begin y2 assign rvalue
    copy assignment
    begin push_back
    copy ctor
    end
    

    @SoIntMan: Du musst nur den copy assignment operator (Slice& operator=(const Slice &src)) definieren und implementieren. Dadurch ist jede move semantic automatisch deaktiviert.

    unabhängig von dem Problem solltest du unbedingt den copy assignment operator implementieren, da du einen eigenen copy constructor definiert hast.
    Ansonsten wird einer vom compiler erstellt, der sehr wahrscheinlich nicht das macht was du erwartest (weil sonst hättest du auch kein eigenen copy constructor implementiert)



  • @firefly sagte in Fehler C2280 move-ctor deleted und std::push_back:

    @SoIntMan: Du musst nur den copy assignment operator (Slice& operator=(const Slice &src)) definieren und implementieren. Dadurch ist jede move semantic automatisch deaktiviert.

    super, sehr schön.. danke für euren support 😉



  • @firefly sagte in Fehler C2280 move-ctor deleted und std::push_back:

    Laut der kompletten fehlermeldung wird std::vector::push_back(const T&) verwendet.
    Nur, wieso auch immer, meint der compiler oder die implementierung der STL dass intern ein codepath genutzt werden soll der ein rvalue reference erwartet.

    Der Teil passt schon so.

    push_back(const T&) muss verwendet werden, selbst wenn der Typ movable wäre, weil im Code y steht und nicht std::move(y).

    Der Code-Pfad der dann trotzdem den move-ctor referenziert ist die "grow" Implementierung. Dabei müssen ja alle Elemente vom alten Speicherblock in den neuen Speicherblock "verschoben" werden. Und für Typen die "noexcept moveable" sind wird da halt der move-ctor verwendet.

    Der Fehler ist einfach dass MSVC der Meinung ist dass der Typ "noexcept moveable" ist, obwohl er einen gelöschten move-ctor hat.



  • @hustbaer sagte in Fehler C2280 move-ctor deleted und std::push_back:

    Der Fehler ist einfach dass MSVC der Meinung ist dass der Typ "noexcept moveable" ist, obwohl er einen gelöschten move-ctor hat.

    Der aber nur ausgelöst wird, wenn explizit z.b. der move constructor (wie im falle von SoIntMan) als deleted markiert ist.
    Wobei laut folgenden hinweis aus https://stackoverflow.com/questions/20819192/attempt-to-push-back-a-lvalue-with-move-ctor-deleted-doesnt-compile
    das angeblich kein "Bug" sei und vom standard so gefordert sei:

    @IgorTandetnik: This is not a bug. As per private communications with Stephan Lavavej, 23.2.1/13 from N3797 states: — T is CopyInsertable into X means that, in addition to T being MoveInsertable into X [...] "in addition to" seems to imply CI requires MI? – 
    ForeverLearning
    

    Und Laut diesem Abschnitt der Antwort von Howard Hinnant gilt wohl folgendes:

    Deleted move members attract rvalue arguments, and if they bind, result in a compile-time error.
    Non-existent move members do not attract any arguments at all.
    Therefore other overloads, such as copy members, are available to be chosen by overload resolution.
    rvalues in particular will get (often confusingly) snagged by deleted move members.
    But with nonexistent move members, rvalues are free to bind to (traditional const&) copy members.
    

    Wenn ich das richtig verstehe, dann wird ein member, welcher explizit als deleted markiert ist, in der suche nach der passenden Methode mit einbezogen.
    Im Falle des move constructors wird diese dann ausgewählt, wenn ein rvalue argument übergeben werden soll an einen constructor.
    Da die Methode aber explizit als deleted markiert ist, löst das ein compiler fehler aus.

    Die Frage ist ob das so im standard definiert ist oder nicht.
    Da GCC/Clang auch explizit deleted methoden aussortieren aber der MSVC nicht (und das seit mindestens der Version von VS 2017) scheint so eine definition nicht zu geben im Standard, die das explizit fordert.



  • @firefly
    Ah, mein Fehler.
    std::is_nothrow_move_constructible_v von solchen Typen ist mit MSVC eh richtigerweise false.

    Was den Rest angeht: keine Ahnung ob da Clang und GCC oder MSVC Recht haben.



  • @hustbaer sagte in Fehler C2280 move-ctor deleted und std::push_back:

    Was den Rest angeht: keine Ahnung ob da Clang und GCC oder MSVC Recht haben.

    Ich glaube ich habe mein simples Beispiel von oben fehlinterpretiert. GCC und Clang default-konstuieren in Zeile 10 mit A a{ A{} }; das Objekt direkt in a, ohne dass Move und/oder Copy-Konstruktoren berücksichtigt werden und daher auch kein Fehler gemeldet wird.

    Wenn man das so umformuliert:

    A a0;
    A a{ std::move(a0) };
    

    dann laufen auch GCC und Clang auf einen Fehler.

    Der eigentliche Unterschied ist also nicht, dass GCC und Clang in diesem Fall den Copy-Konstruktor verwenden, sondern dass sie eben nur den Default-Konstruktor aufrufen. Ich denke das gehört in diesem Fall sogar zur Mandatory Copy Elision, die vorhandene Copy/Move-Konstruktoren nicht berücksichtigt, womit MSVC in diesem Fall zu übereifrig wäre und hier falsch liegt.

    Dass es überhaupt zu einem Fehler kommt finde ich schon etwas unintuitiv, da ja eben noch der Copy-Konstruktor existiert. Aber eine deleted-Funktion ist eben "deklariert" und wird tatsächlich für Overload Resolution herangezogen.

    Zuerst dachte ich, da gäbe es mit dem hier auf cppreference sei die entsprechende Ausnahme:

    Additional rules for constructor candidates
    Defaulted move constructor and move assignment that are defined as deleted are never included in the list of candidate functions. (since C++11)

    Die Formulierung fand ich aber recht seltsam. "Defaulted move" die als "deleted" definiert sind. Hä? Entweder "defaulted" oder "deleted", wie soll denn beides gehen?

    Die relevante Stelle im Standard (in dieser Version unter 12.4.1 (8)) - verewendet diese ähnliche Formulierung:

    A defaulted move special function that is defined as deleted is excluded from the set of candidate functions in all contexts.

    und bringt aber auch ein Beispiel, was damit anscheinend gemeint ist:

    struct A {
      A(); // #1
      A(A &&); // #2
      template<typename T> A(T &&); // #3
    };
    struct B : A {
      using A::A;
      B(const B &); // #4
      B(B &&) = default; // #5, implicitly deleted
      struct X { X(X &&) = delete; } x;
    };
    
    extern B b1;
    B b2 = static_cast<B&&>(b1); // calls #4: #1 is not viable, #2, #3, and #5 are not candidates
    struct C { operator B&&(); };
    B b3 = C(); // calls #4
    

    #5 ist so wie ich das verstehe ein default Move-Konstruktor der implizit deleted ist, da die Klasse einen Member mit einem explizit deleted Move-Konstruktor hat. Also "defaulted" und "deleted" gleichzeitig scheint es tatsächlich zu geben 😉

    Diese Ausnahme würde tatsächlich eine Workaround erlauben, wenn man es denn wirklich darauf anlegen will, unbedingt einen solchen gelöschten Move zu haben. Man könnte eben dafür sorgen, dass der "defaulted" Move-Konstruktor implizit "deleted" ist:

    #include <iostream>
    #include <utility>
    
    struct A
    {
        A() = default;
        A(const A&)
        {
            std::cout << "copy A" << std::endl;
        }
    
        A(A&&) = default;
    
        struct X
        { 
            X() = default;
            X(const X&) = default;
            X(X&&) = delete;
        } x;
    };
    
    int main()
    {
        A a0;
        A a{ std::move(a0) };
    }
    

    Und in der Tat kompiliert das auf allen 3 Compilern und gibt jeweils copy A aus. Clang findet dieses barocke Konstrukt allerdings gar nicht toll und spuckt eine Warnung aus, man solle A(A&&) doch bitte explizit deleten:

    https://godbolt.org/z/73vf8jvcP



  • @Finnegan
    In dem Beispiel kannst du das A(A&&) = default; auch genau so gut weglassen, da die Deklaration von A(const A&) schon dafür sorgt dass A keinen move-ctor hat.



  • @hustbaer sagte in Fehler C2280 move-ctor deleted und std::push_back:

    @Finnegan
    In dem Beispiel kannst du das A(A&&) = default; auch genau so gut weglassen, da die Deklaration von A(const A&) schon dafür sorgt dass A keinen move-ctor hat.

    Ich will ja für das Beispiel gerade einen Move Constructor haben, um ihn implizit löschen zu können. Das Ziel ist ja, den compiler-generierten Move-CTOR so zu löschen, dass er nicht für die Overload Resolution herangezogen wird (und dann wegen "deleted" auf einen Fehler läuft). Wenn erst gar keiner erzeugt wird, dann macht das ganze Beispiel keinen Sinn 😁

    Ferner ist A(const A&) eh nur für die Debug-Ausgabe um zu sehen, dass der Copy Constructor auch tatsächlich aufgerufen wird und eben nicht aus irgendeinem Grund doch A(A&&).

    Vielleicht ginge das auch noch etwas eleganter und ohne extra Speicher zu belegen mit ner Basisklasse, die ein gelöschtes Move hat (Empty Base Optimization)*.

    Dennoch frage ich mich immer noch, welchen Use Case es für sowas geben könnte. Vielleicht ein Member, dessen Klasse ein sinnvolles Move implementiert, das man aber in speziell diesem Kontext (als Member einer bestimmten Klasse) unterbinden möchte (?).

    * Edit: Ja, so ists besser, wenn man sowas wirklich ernsthaft nutzen will: https://godbolt.org/z/secahe6rs.
    Die Warnungen stören in diesem Fall allerdings etwas, da das ja genau das ist, was erreicht werden soll - die gibts aber eh nur bei dem expliziten default.


Log in to reply