bind temporary to copy-ctor when move-ctor deleted



  • Hallo zusammen,

    ich kenne mich bei C++11 noch nicht so wirklich aus. Ich spiele gerade mit boost::asio und vinniefalco/Beast library . In der vinniefalco/Beast library kommt des Öfteren sowas vor:

    // string_ref.hpp
    // Constructing a string_ref from a temporary string is a bad idea
    template<typename Allocator>
    basic_string_ref(std::basic_string<charT, traits, Allocator>&&) = delete;
    
    // basic_fields.hpp
    template<class Allocator> void basic_fields<Allocator>::replace(boost::string_ref const& name, boost::string_ref value)
    {
        // ...
    }
    
    // basic_fields.ipp
    replace(boost::string_ref const& name, T const& value)
    {
        replace(name, lexical_cast<std::string>(value)); // error: attempting to reference a deleted function
    }
    

    Mal in ein einfaches Beispiel übersetzt:

    class Foo
    {
    public:
    	Foo() {}
    	Foo( const Foo& f ) {}
    	Foo( const Foo&& f ) = delete;
    };
    
    void fkt( Foo f ) {}
    
    int main( )
    {
    	fkt( Foo() ); // error: attempting to reference a deleted function
    }
    

    Bin aus C++03 gewöhnt, dass man temporäre Objekte an const-ref binden kann. Wie löst man das nun in C++11?



  • Bin aus C++03 gewöhnt, dass man temporäre Objekte an const-ref binden kann. Wie löst man das nun in C++11?

    Das funktioniert in C++11 noch genauso. Du hast aber einen Move-Ctor deklariert und der ist einfach ein besserer Match, ganz egal, ob das Ding aufrufbar ist oder als delete d deklariert wurde. Wenn Du möchtest, dass in jedem Fall (lvalue als auch rvalue) der Copy-Ctor benutzt wird, dann lass den Move-Ctor einfach komplett weg. Ich verstehe auch nicht ganz, was dein Foo -Beispiel mit dem vorherigen string_ref Beispiel zu tun hat. Was willst Du eigentlich erreichen?

    BTW: Ab C++17 könnte dein Beispiel sogar kompilieren. Warum? Weil das Wegoptimieren von der Kopie bzw des Moves in einer Situation wie Deiner dann Pflicht ist. Heutzutage verlangen Compiler die Existenz eines aufrufbaren Kopierkonstruktors oder Movekonstruktors in einigen Kontexten, obwohl sie gar nicht aufgerufen werden müssen. Die Compiler dürfen das Wegoptimieren. Und Ab C++17 ist das Pflicht. Ab C++17 kannst Du dann z.B. auch Objekte durch eine Funktion zurückgeben lassen, die gar nicht kopierbar oder movebar sind.



  • Bin aus C++03 gewöhnt, dass man temporäre Objekte an const-ref binden kann. Wie löst man das nun in C++11?

    Indem man das Problem gar nicht erst erzeugt, also den move constructor nicht deleted.

    Ist dir überhaupt klar, warum das beim string_ref-Konstruktor der Fall ist?

    // Constructing a string_ref from a temporary string is a bad idea
    


  • ;kar schrieb:

    Ist dir überhaupt klar, warum das beim string_ref-Konstruktor der Fall ist?

    // Constructing a string_ref from a temporary string is a bad idea
    

    Das temporäre Objekt wird am Ende des ganzen Ausdrucks (also beim ; ) zerstört und wenn ich dann eine Referenz drauf hab und einen Schreibzugriff mache, haut's mir das Ding (hoffentlich) um die Ohren.
    Bin mir aber nicht sicher ob meine Erklärung stimmt, müsste ich mir mal genau anschauen, was move überhaupt bedeutet.

    Wundert mich nur gerade, wieso das erst in boost 1.64.0 gefixt wird.



  • krümelkacker schrieb:

    Bin aus C++03 gewöhnt, dass man temporäre Objekte an const-ref binden kann. Wie löst man das nun in C++11?

    Das funktioniert in C++11 noch genauso. Du hast aber einen Move-Ctor deklariert und der ist einfach ein besserer Match, ganz egal, ob das Ding aufrufbar ist oder als delete d deklariert wurde. Wenn Du möchtest, dass in jedem Fall (lvalue als auch rvalue) der Copy-Ctor benutzt wird, dann lass den Move-Ctor einfach komplett weg. Ich verstehe auch nicht ganz, was dein Foo -Beispiel mit dem vorherigen string_ref Beispiel zu tun hat. Was willst Du eigentlich erreichen?

    BTW: Ab C++17 könnte dein Beispiel sogar kompilieren. Warum? Weil das Wegoptimieren von der Kopie bzw des Moves in einer Situation wie Deiner dann Pflicht ist. Heutzutage verlangen Compiler die Existenz eines aufrufbaren Kopierkonstruktors oder Movekonstruktors in einigen Kontexten, obwohl sie gar nicht aufgerufen werden müssen. Die Compiler dürfen das Wegoptimieren. Und Ab C++17 ist das Pflicht. Ab C++17 kannst Du dann z.B. auch Objekte durch eine Funktion zurückgeben lassen, die gar nicht kopierbar oder movebar sind.

    Ok ich verstehe.
    Also ein rvalue ist ein Objekt ohne Namen, also ein temporäres Objekt. Ein move-Ctor ist ein Konstruktor, der für rvalues ist. && ist eine rvalue reference, an die ich einen rvalue binde, darum auch der bessere Match, wie du sagst.

    class string_ref
    {
    public:
    	string_ref( )
    	{
    		cout << "default ctor" << '\n';
    	}
    
    	string_ref( const string& str )
    	{
    		cout << "conversion ctor" << '\n';
    	}
    
    	string_ref( const string_ref& ori )
    	{
    		cout << "copy ctor" << '\n';
    	}
    
    	string_ref( string_ref&& ori ) // = delete;
    	{
    		cout << "move ctor" << '\n';
    	}
    };
    
    int main( )
    {
    	// 1. conversion ctor erzeugt ein temporäres Objekt (rvalue)
    	// 2. rvalue wird an move ctor übergeben
    	// 3. move ctor initialisiert sr
    	// Aber: Kopie wird wegoptimiert, weil es keinen Sinn macht ein temp. Objekt zu erzeugen, zu kopieren und dann wieder zu zerstören. Schritt 2 und 3 fallen also weg.
    	// Aber: Der theorische Ablauf (Schirtt 2 und 3) muss trotzdem durchführbar sein, weil das Wegoptimieren der Kopie keine Pflicht des Compilers ist.
         //      Wenn ich den move-ctor nun delete, ist der theoretische Ablauf nicht mehr durchführbar, darum error.
    	string_ref sr = string_ref( "Hallo" );
    }
    

    Fazit: Find ich gut, dass in C++17 der theoretische Ablauf hinfällig wird, wenn das Wegoptimieren der Kopie zur Pflicht wird und damit immer stattfindet, und das ganze dann kompiliert.


  • Mod

    out schrieb:

    Fazit: Find ich gut, dass in C++17 der theoretische Ablauf hinfällig wird, wenn das Wegoptimieren der Kopie zur Pflicht wird und damit immer stattfindet, und das ganze dann kompiliert.

    Copy-ctor haben aber Move-ctor deleten erscheint mir ziemlich sinnlos.
    Und es bleibt völlig unklar, was das Ganze mit dem ersten Codeschnipsel zu tun hat.



  • camper schrieb:

    out schrieb:

    Fazit: Find ich gut, dass in C++17 der theoretische Ablauf hinfällig wird, wenn das Wegoptimieren der Kopie zur Pflicht wird und damit immer stattfindet, und das ganze dann kompiliert.

    Copy-ctor haben aber Move-ctor deleten erscheint mir ziemlich sinnlos.
    Und es bleibt völlig unklar, was das Ganze mit dem ersten Codeschnipsel zu tun hat.

    Ja richtig, das war gedanklich für mich, um nachzuvollziehen, was krümelkacker mit besserer Match gemeint hat.
    Die Antwort auf die Frage, wieso man ein move-ctor nicht zulassen soll, einen copy-ctor aber schon, ist - wenn ich es richtig verstanden habe - wenn ein temporäres Objekt auf temporäre Daten verweist. Ich kann die Daten ja dann nicht "klauen", weil sie am Ende des Ausdrucks nicht mehr gültig sind. Der copy-ctor macht hingegen ein deep-copy und klaut die Daten nicht.


  • Mod

    out schrieb:

    Die Antwort auf die Frage, wieso man ein move-ctor nicht zulassen soll, einen copy-ctor aber schon, ist - wenn ich es richtig verstanden habe - wenn ein temporäres Objekt auf temporäre Daten verweist. Ich kann die Daten ja dann nicht "klauen", weil sie am Ende des Ausdrucks nicht mehr gültig sind. Der copy-ctor macht hingegen ein deep-copy und klaut die Daten nicht.

    Copy- und Move-ctor machen das Gleiche: sie erstellen ein neues Objekt unter Benutzung eines anderen Objektes als Vorlage - erzeugen also eine Kopie.
    Der Move-ctor hat dabei den Vorteil, dass er weiss, dass niemand mehr auf das Original zugreifen kann, und darf deshalb beim Kopieren schummeln - ein Zwang besteht hier aber nicht (es ist im Prinzip ein bloße Optimierung). Warum sollte ein Move-ctor nicht auch "richtig" kopieren dürfen, wenn direktes Resourcen klauen aus dem einen oder anderen Grund nicht angezeigt ist?



  • camper schrieb:

    Warum sollte ein Move-ctor nicht auch "richtig" kopieren dürfen, wenn direktes Resourcen klauen aus dem einen oder anderen Grund nicht angezeigt ist?

    Also du meinst also, ich implementiere den move-Ctor genauso wie den copy-ctor, bzw. ich delegiere an den copy-Ctor?



  • out schrieb:

    camper schrieb:

    Warum sollte ein Move-ctor nicht auch "richtig" kopieren dürfen, wenn direktes Resourcen klauen aus dem einen oder anderen Grund nicht angezeigt ist?

    Also du meinst also, ich implementiere den move-Ctor genauso wie den copy-ctor, bzw. ich delegiere an den copy-Ctor?

    Nein, das macht keinen Sinn. Wenn Dein Move-Ctor genau dasselbe wie der Copy-Ctor machen würde, lass ihn einfach weg.

    Im deinem boost::string_ref -Beispiel sind es auch nicht Copy-Ctor und Move-Ctor, sondern Konvetierungs-Konstruktoren:

    class string_ref
    {
       const char* begin_;
       std::size_t len_;
    
       ...
    
       string_ref(const string& x) : ... {}
    
       string_ref(const string&&) = delete;
    
       ...
    };
    

    Hier wird ja keine Kopie eines string_ref s erstellt sondern ein string_ref so erzeugt, dass es auf Zeichenkette zeigt, die irgendwo im Speicher steht. Die Gefahr, dabei temporäre String-Objekte hier als Konstruktor-Parameter zuzulassen, ist die, dass diese temporären String-Objekte relativ kurzlebig sein können und sich dann das string_ref -Objekt ggf. auf etwas bezieht, was gar nicht mehr vorhanden ist. Man wollte mit dem "=delete;" einfach Fehler zur Übersetzungszeit abfangen können. Ein ähnliches Paar von Konstruktor-Überladungen bietet auch std::reference_wrapper aus demselben Grund.

    Das ärgerliche bei diesem string_ref -Ansatz ist aber, dass folgender Use-Case nicht mehr funktioniert:

    string foo();
    
    void bar(string_ref x) {
        cout << x << endl;
    }
    
    int main() {
        bar(foo()); // klappt nicht, wäre aber OK sonst
    }
    

    Man will string_ref bzw string_view typischerweise als Funktionsparameter-Typ benutzen. Temporäre String-Objekte wie das, was hier foo zurück gibt, würden in diesen Fällen aber lang genug leben.

    Tatsächlich unterstützt std::string_view (C++17) diesen Usecase. Und auf der Boost-Mailingliste ist dieser Fall auch zumindest 2014 diskutiert worden. Seit Boost 1.61 wurde boost::string_ref zugunsten von boost::string_view als deprecated markiert. In der aktuellen string_ref -Doku finde ich auch keinen Hinweis darüber, dass es diesen "ge delete ten" Konvertierungs-Konstruktor gibt. Ich kann mir vorstellen, dass sie das aus Gründen der usability wieder entfernt haben.

    Edit: Vielleicht werden die Compiler ja auch irgendwann schlau genug, dass sie solche Lebenszeit-Probleme abfangen können. Dann wäre der Verlust von der "=delete;"-Überladung ja kein Problem mehr.



  • krümelkacker schrieb:

    Nein, das macht keinen Sinn. Wenn Dein Move-Ctor genau dasselbe wie der Copy-Ctor machen würde, lass ihn einfach weg.

    Ok

    krümelkacker schrieb:

    class string_ref
    {
       const char* begin_;
       std::size_t len_;
    
       ...
    
       string_ref(const string& x) : ... {}
    
       string_ref(const string&&) = delete;
    
       ...
    };
    

    Hier wird ja keine Kopie eines string_ref s erstellt sondern ein string_ref so erzeugt, dass es auf Zeichenkette zeigt, die irgendwo im Speicher steht. Die Gefahr, dabei temporäre String-Objekte hier als Konstruktor-Parameter zuzulassen, ist die, dass diese temporären String-Objekte relativ kurzlebig sein können und sich dann das string_ref -Objekt ggf. auf etwas bezieht, was gar nicht mehr vorhanden ist. Man wollte mit dem "=delete;" einfach Fehler zur Übersetzungszeit abfangen können. Ein ähnliches Paar von Konstruktor-Überladungen bietet auch std::reference_wrapper aus demselben Grund.

    Ok, danke genau da wollte ich hören. 🙂

    krümelkacker schrieb:

    Man will string_ref bzw string_view typischerweise als Funktionsparameter-Typ benutzen. Temporäre String-Objekte wie das, was hier foo zurück gibt, würden in diesen Fällen aber lang genug leben.

    Ok, verstehe.

    krümelkacker schrieb:

    In der aktuellen string_ref -Doku finde ich auch keinen Hinweis darüber, dass es diesen "ge delete ten" Konvertierungs-Konstruktor gibt. Ich kann mir vorstellen, dass sie das aus Gründen der usability wieder entfernt haben.

    Ich bezog mich auf boost 1.64.0:
    https://svn.boost.org/trac/boost/ticket/12917
    https://github.com/boostorg/utility/commit/9960d9f395b79ee860e39064cd46961f76d2cb55


Log in to reply