Fehler C2280 move-ctor deleted und std::push_back



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


Anmelden zum Antworten