Fehler C2280 move-ctor deleted und std::push_back



  • Hallo Leute ,

    ich versteh meinen compiler nicht:)

    Ich habe folgende Klasse:

    class Slice
          {
            ....
    
          public:
    
              Slice(const size_t& id, const size_t& maxSize);
    
              Slice(const Slice& scr);
    
              Slice(const Slice&& scr) = delete;
    
              virtual ~Slice();
    
    ...
    }
    

    und ich will eine Slice- Instanz in einen Vector Kopieren:

    std::vector<  CoreLib::Recording::Slice> vector;
    
              CoreLib::Recording::Slice x(0, 0);
              CoreLib::Recording::Slice y(x);
              vector.push_back(y);
    

    aber sobald ich vector.push_back(y) aufrufe

    bekomme ich nen compiler fehler:

    Schweregrad	Code	Beschreibung	Projekt	Datei	Zeile	Unterdrückungszustand
    Fehler	C2280	"CoreLib::Recording::Slice::Slice(const CoreLib::Recording::Slice &&)" : Es wurde versucht, auf eine gelöschte Funktion zu verweisen	CoreLib	C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Tools\MSVC\14.34.31933\include\xmemory	680
    

    aber wieso, std::vector::push_back hat ja zwei overde mit copy-ctor und move-ctor!?

    wie rafft er das nich?

    Habe ich was übersehen?

    Schönes Wochenende euch;)



  • @SoIntMan

    Verstehe ich auch nicht, aber kannst du y const machen? Damit dürfte y nicht mehr moevable sein.



  • Das scheint eine "spezielles" verhalten von MSVC zu sein.
    Denn mit gcc (12.2.1) und clang(15.0.7) lässt sich das ganze problemlos übersetzen.

    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.

    Hier die Meldung von godbolt.org run (Beachte die zeile 11 der Fehlermeldung)

    C:/data/msvc/14.36.32530-Pre/include\xmemory(677): error C2280: 'Slice::Slice(const Slice &&)': attempting to reference a deleted function
    <source>(18): note: see declaration of 'Slice::Slice'
    <source>(18): note: 'Slice::Slice(const Slice &&)': function was explicitly deleted
    C:/data/msvc/14.36.32530-Pre/include\xmemory(1777): note: see reference to function template instantiation 'void std::_Default_allocator_traits<_Alloc>::construct<_Ty,_Ty>(_Alloc &,_Objty *const ,_Ty &&)' being compiled
            with
            [
                _Alloc=std::allocator<Slice>,
                _Ty=Slice,
                _Objty=Slice
            ]
    C:/data/msvc/14.36.32530-Pre/include\xmemory(1921): note: see reference to function template instantiation 'void std::_Uninitialized_backout_al<_Alloc>::_Emplace_back<Slice>(Slice &&)' being compiled
            with
            [
                _Alloc=std::allocator<Slice>
            ]
    C:/data/msvc/14.36.32530-Pre/include\vector(844): note: see reference to function template instantiation 'Slice *std::_Uninitialized_move<Slice*,std::allocator<Slice>>(const _InIt,const _InIt,Slice *,_Alloc &)' being compiled
            with
            [
                _InIt=Slice *,
                _Alloc=std::allocator<Slice>
            ]
    C:/data/msvc/14.36.32530-Pre/include\vector(783): note: see reference to function template instantiation 'Slice *std::vector<Slice,std::allocator<Slice>>::_Emplace_reallocate<const _Ty&>(Slice *const ,const _Ty &)' being compiled
            with
            [
                _Ty=Slice
            ]
    C:/data/msvc/14.36.32530-Pre/include\vector(871): note: see reference to function template instantiation '_Ty &std::vector<_Ty,std::allocator<_Ty>>::_Emplace_one_at_back<const _Ty&>(const _Ty &)' being compiled
            with
            [
                _Ty=Slice
            ]
    C:/data/msvc/14.36.32530-Pre/include\vector(870): note: while compiling class template member function 'void std::vector<Slice,std::allocator<Slice>>::push_back(const _Ty &)'
            with
            [
                _Ty=Slice
            ]
    


  • Ja, ich habe auch den Eindruck, dass das ein Bug in der Standardbibliothek ist. Zumindest laut cppreference sind die hier relevanten Anforderungen an den im Vektor gespeicherten Typen:

    T must meet the requirements of CopyAssignable and CopyConstructible.

    CopyAssignable und CopyConstructible erfordern zwar MoveAssignable und MoveConstructible, bei denen steht aber explizit:

    A class does not have to implement a move constructor to satisfy this type requirement: a copy constructor that takes a const T& argument can bind rvalue expressions.

    Ich denke mal, dass das exakt den Anforderungen des Standards entspricht, auch wenn ich mir das dort nicht extra angesehen habe.

    Es gibt meines wissens aber auch noch den Effekt, dass ein Slice(Slice&& scr) = delete ebenfalls implizit auch den compiler-generierten Slice(const Slice& scr) löscht. Da du den Konstruktor allerdings explizit hinzufügst, sollte das hier nicht das Problem sein.

    Edit: Ich habe mir gerade nochmal den Effekt von = delete genauer angesehen, wo mir das hier aufgefallen ist, das mir bisher gar nicht so bewusst war:

    Deleted functions
    [...]
    If the function is overloaded, overload resolution takes place first, and the program is only ill-formed if the deleted function was selected

    Es scheint, als ob ein = delete nicht dazu führt, dass die Funktion salopp gesagt nicht komplett unvorhanden ist, sondern schon irgendwie "deklariert" ist, aber eben als gelöscht markiert. Nach meinem Verständnis sollte also bereits so etwas hier zu einem Fehler führen, obwohl ein T&& eigentlich auch an ein T& binden kann:

    struct A
    {
        A();
        A(const A&);
        A(A&&) = delete;
    };
    
    int main()
    {
        A a{ A{} };
    }
    

    Das Ergebnis ist hier ebenfalls sehr interessant:

    MSVC: error C2280: 'A::A(A &&)': attempting to reference a deleted function
    GCC: kein Fehler
    Clang: kein Fehler

    Wer hat hier also recht? So wie ich die Passage mit der "overload resolution" bei = delete verstehe, hat MSVC recht, aber ich würde bevorzugen, wenn der Standard das Verhalten von GCC und Clang vorschreibt. Das ist wesentlich intuitiver und leichter zu handhaben, wenn man es mit einem solchen gelöschten Konstruktor zu tun hat.

    Jedenfalls scheint das Problem hier grundlegender zu sein als lediglich die Implementierungs-Details der Standardbibliothek.

    Und noch was wichtiges: Der Move-Konstruktor für deinen Typ ist

    Slice(Slice&& scr);
    

    nicht const Slice&& scr. Versuch das mal zu ändern und schau ob das eventuell funktioniert.



  • Ich meine auch, daß const entfernt werden sollte:

     Slice(Slice&& scr) = delete;
    

    s.a. What are const rvalue references good for?



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

    Und noch was wichtiges: Der Move-Konstruktor für deinen Typ ist

    Slice(Slice&& scr);
    

    nicht const Slice&& scr. Versuch das mal zu ändern und schau ob das eventuell funktioniert.

    Ändert nichts am verhalten beim MSVC. Es kommt der gleiche fehler.



  • @firefly Nochwas relevantes bezüglich der Kandidaten für Overload Resolution:

    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)

    Mit dieser Regel liegen dann doch wieder GCC und Clang richtig und MSVC falsch. Mein kleines Testprogramm im vorherigen Beitrag müsste also eigentlich kompilieren - falls ich den Satz richtig gelesen und verstanden habe 🤔 ... sind T(T&&) = default und operator=(T&&) = delete gemeint, oder dass die compiler-generierten defaults der beiden deleted sind?

    Darf ich fragen welche Motivation hinter dem gelöschten Move-Konstruktor steckt? Ich hatte für so etwas bisher noch keinen Bedarf und vielleicht kann man das ja auch anders lösen, so dass es zumindest auch mit MSVC funktioniert.



  • @Finnegan Öhm ich bin nicht der thread ersteller 😉 Hatte das jetzt nur auf die schnelle via godbolt.org getestet



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

    @Finnegan Öhm ich bin nicht der thread ersteller 😉 Hatte das jetzt nur auf die schnelle via godbolt.org getestet

    Oh sorry, stimmt ja..dachte ich echt grad wegen der Antwort. Ich meine natürlich @SoIntMan 😁



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

    Darf ich fragen welche Motivation hinter dem gelöschten Move-Konstruktor steckt? Ich hatte für so etwas bisher noch keinen Bedarf und vielleicht kann man das ja auch anders lösen, so dass es zumindest auch mit MSVC funktioniert.

    ich hab das "per zufall" rausgefunden, und habe mich gefragt was la lass ist. ich verwende dann auch den move ctor.

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

    @SoIntMan
    Verstehe ich auch nicht, aber kannst du y const machen? Damit dürfte y nicht mehr moevable sein.

    das hat auch nicht funktioniert.

    Danke euch, ist aber echt suspekt



  • Ja ist echt suspekt. Auf godbolt.org kann man bis zu vs2017 zurück gehen und auch dort tritt die Fehlermeldung auf.
    Daher ist dieses verhalten in der STL des C++ Compilers von MS schon länger drinn.
    Eventuell haben wir hier es mit einer Definitionslücke im Standard zu tun, wodurch es zu unterschiedlichen Interpretationen kommt.
    Aber ob meine Spekulation stimmt kann ich nicht sagen, da ich mich mit dem C++ Standard und dessen interpretation überhaupt nicht auskenne.

    Wobei ich aktuell eher dazu tendieren würde, dass die Interpretation von GCC und Clang die korrekte ist und die von MSVC als die falsche.

    Wenn ich mich recht entsinne, ist es nicht so, wenn für eine Klasse der copy constructor explizit definiert wird, dass dadurch der move constructor und move assignment operator implizit als nicht vorhanden definiert wird?

    Wodurch eigentlich nie die move semantic verwendet werden sollte.



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

    Ja ist echt suspekt. Auf godbolt.org kann man bis zu vs2017 zurück gehen und auch dort tritt die Fehlermeldung auf.
    Daher ist dieses verhalten in der STL des C++ Compilers von MS schon länger drinn.
    Eventuell haben wir hier es mit einer Definitionslücke im Standard zu tun, wodurch es zu unterschiedlichen Interpretationen kommt.
    Aber ob meine Spekulation stimmt kann ich nicht sagen, da ich mich mit dem C++ Standard und dessen interpretation überhaupt nicht auskenne.
    Wobei ich aktuell eher dazu tendieren würde, dass die Interpretation von GCC und Clang die korrekte ist und die von MSVC als die falsche.
    Wenn ich mich recht entsinne, ist es nicht so, wenn für eine Klasse der copy constructor explizit definiert wird, dass dadurch der move constructor und move assignment operator implizit als nicht vorhanden definiert wird?
    Wodurch eigentlich nie die move semantic verwendet werden sollte.

    hmm das bedeutet ja dass hier noch nie jemand über das "Problem" gestolpert ist?:)
    EDIT: Danke für den "godbold.org" linke, sehr hilfreich;()





  • @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


Anmelden zum Antworten