unique_ptr in move-assignment operator



  • Hallo ihr Lieben,

    ich habe mal wieder eine Frage: Ich habe angefangen meine Pointer in einen unique_ptr zu packen, dann copy und assignment zu verbieten (private) und direkt auf die move Semantik zu gehen.

    Jetzt wurmt mich nur der move-assigment operator, weil er nicht geht.

    Ich habe deklariert

    std::vector<double> x,y;
    std::unique_ptr<gsl_interp_accel,decltype((gsl_interp_accel_free))> acc;
    std::unique_ptr<gsl_spline,decltype((gsl_spline_free))> spline;
    

    Und den move-assigment operator

    interpolation& interpolation::operator=(interpolation&& rhs)
    {
    	if( this != &rhs)
    	{
    		x = rhs.x;
    		y = rhs.y;
    
    		acc = std::move(rhs.acc);
    		spline = std::move(rhs.spline);
    		gsl_spline_init(spline.get(),x.data(),y.data(),x.size());
    
    		rhs.acc = nullptr;
    		rhs.spline = nullptr;
    	}
    
    	return *this;
    }
    

    Jetzt kriege ich als Fehlermeldung z.B.

    /usr/include/c++/4.7/bits/unique_ptr.h: In instantiation of ‘std::unique_ptr<_Tp, _Dp>& std::unique_ptr<_Tp, _Dp>::operator=(std::unique_ptr<_Tp, _Dp>&&) 
    [with _Tp = gsl_interp_accel; _Dp = void (&)(gsl_interp_accel*); std::unique_ptr<_Tp, _Dp> = std::unique_ptr<gsl_interp_accel, void (&)(gsl_interp_accel*)>]’:
    interpolation.cpp:43:26:   required from here
    /usr/include/c++/4.7/bits/unique_ptr.h:182:2: error: assignment of read-only location ‘std::unique_ptr<_Tp, _Dp>::get_deleter<gsl_interp_accel, void (&)(gsl_interp_accel*)>()’
    /usr/include/c++/4.7/bits/unique_ptr.h:182:2: error: cannot convert ‘void(gsl_interp_accel*)’ to ‘void(gsl_interp_accel*)’ in assignment
    

    Wobei ich mich dabei frage, ob die Definition wirklich nötig ist. Wenn der Zuweisungsoperator benötigt wird, dann wird das Programm doch den Zuweisungsoperator der beteiligten Objekte verwenden, also vector und unique_ptr - und diese sind doch wohldefiniert. Also eigentlich unnötig dies nochmal explizit von Hand in den move-assigment operator zu schreiben? 😕

    Immerhin hat es einen Lerneffekt. 😃

    Gruß,
    -- Klaus.



  • Ich erinnere mich.

    Dieses decltype(()) Geraffel war seinerzeit mit der heißen Nadel gestrickt (und fiel mir in der Folge auch vor die Füße).

    Nimm einen
    unique_ptr<gsl_interp_accel,void(*)(gsl_interp_accel*)> (analog für spline).



  • <-- Edit -->

    Okay, habs verstanden und es funktioniert. Danke! 🙂

    Jetzt habe ich noch eine Verständnisfrage: Warum muss ich in dem obigen Beispiel in Zeile 8 und 9 die beiden unique_ptr mittels move übergeben? Dieses move macht aus seinem Argument ein r-value, s.d. bei dem unique_ptr die unique_ptr assigment getriggert wird.

    Aber.

    Das Funktionsargument des move-assigment operators meiner Klasse nimmt als Argument ohnehin ein r-value auf, immerhin schreibe ich

    operator=(interpolation&& rhs)
    

    Also was soll z.B. in Zeile 8 das rhs.acc sein außer schon ein r-value? Warum muss ich mittels move diesen Ausdruck nochmal in ein r-value nachcasten?

    Gruß,
    -- Klaus.



  • Nein, ein && ist nicht zwingend eine rvalue reference. Und ein && das einen namen hat (wie dein argument) sowieso nicht.

    http://thbecker.net/articles/rvalue_references/section_01.html



  • ScottZhang schrieb:

    Und ein && das einen namen hat (wie dein argument) sowieso nicht.

    Das ist natürlich ein Argument, doch ich verstehe die Abfolge nicht.

    Wenn ich eine Zeile habe wie foo = move(bar) , dann ist bar zunächst ein lvalue. Daraus wird mittels move(bar) ein rvalue. Das triggert den move-assignment operator. Jetzt befinden wir uns im Rumpf des move-assignment operators von foo :

    foo& operator=(foo&& rhs)
    {
      rhs.stuff
    }
    

    Und obwohl ich bar als rvalue übergeben habe verhält es sich im Rumpf des move-assigment operators von foo wieder als lvalue!?

    Sehe ich das richtig? Falls ja -> Hä? Warum bleibt es nicht ein rvalue?

    Gruß,
    -- Klaus.



  • Klaus82 schrieb:

    Und obwohl ich bar als rvalue übergeben habe verhält es sich im Rumpf des move-assigment operators von foo wieder als lvalue!?

    Sehe ich das richtig? Falls ja -> Hä? Warum bleibt es nicht ein rvalue?

    Gruß,
    -- Klaus.

    Ein Parameter mit Namen ist "von innen" immer ein lValue. Als Faustregel gilt: "Alles was einen Namen hat ist ein lvalue"



  • Klaus82 schrieb:

    Sehe ich das richtig? Falls ja -> Hä? Warum bleibt es nicht ein rvalue?

    Ja, genau. Weil es "einen Namen hat". Warum weiß ich auch nicht genau, vllt kann das einer der Gurus hier beantworten. Vllt weil man sonst unbemerkt ein wegemovetes objekt übrig haben kann, ka. Bzw eiegntlich logisch, man könnte ja sonst das argument in der Funktion nicht kopieren sonder nu moven, obwohl das bei nem rvalue auch wieder egal ist. Hm, ka.


  • Mod

    Klaus82 schrieb:

    Sehe ich das richtig?

    ja

    Klaus82 schrieb:

    Falls ja -> Hä? Warum bleibt es nicht ein rvalue?

    Zuerst die einfach zu merkende Regel: wenn etwas einen Namen hat, ist es ein lvalue (abgesehen von Aufzählungskonstanten).

    Ein rvalue, das auch ein Objekt verweist, drückt damit aus, dass das betreffende Objekt nur vorübergehend in diesem Kontext existiert (temporär ist). Temporär-Sein ist keine Eigenschaft des Objektes an sich, sondern drückt eine Beziehung aus, zwischen dem Objekt und dem Kontext, in dem es auftaucht.
    Innerhalb des Zuweisungsoperators ist das Argument ein ganz Normales. Das Objekt auf das verwiesen wird existierte bereits vor dem Funktionsaufruf und existiert immer noch nach Verlassen der Funktion, es besteht hierin keinerlei Unterschied zu einem Objekt, das per lvalue-Referenz übergeben wird. Weil das Objekt innerhalb der Funktion auch einen (Alias-)Namen hat - die Referenz - kann es nat. auch innerhalb der Funktion mehrfach verwendet werden. Es wäre etwas unglücklich, wenn gleich die erste Verwendung das Objekt "leeren" würde.



  • ScottZhang schrieb:

    Nein, ein && ist nicht zwingend eine rvalue reference. Und ein && das einen namen hat (wie dein argument) sowieso nicht.

    Naja, als Ausdruck nicht, aber der deklarierte Typ ist ggf eine Rvalue-Referenz. Da kommt man immer noch per decltype dran.

    Ich hatte gestern erst wieder das Vergnügen mit einer C-API, wo ich mir dachte, ich würde liebend gern unique_ptr für all die Handles benutzen. Das mit den Deletern war aber lästig. Wenn man als Deleter-Typ einen Funktionszeiger wählt, kann er nicht so leicht ge inline d werden. Die Deleter erfordert auch an vielen Stellen eine Initialisierung mit der richtigen Funktionsadresse, weil ja ein Null-Zeiger wenig hilft. Sich von Hand für jede Sache einen Deleter-Funktor zu schreiben, nervt auch. Ich habe dann folgendes gebastelt:

    // ---- allgemeiner boiler plate code ----
    
    #include <memory>
    #include <type_traits>
    
    template<class FuncType, FuncType* FuncPtr>
    struct free_func_deleter
    {
    	template<class T>
    	void operator()(T* ptr) const
    	{ FuncPtr(ptr); }
    };
    
    #define TYPEDEF_UNIQUE_PTR(PointeeType,FreeFunc) \
        typedef std::unique_ptr< \
        	PointeeType, \
        	free_func_deleter< \
        		typename std::remove_pointer< \
        			typename std::decay<decltype(FreeFunc)>::type \
        		>::type, \
        		FreeFunc \
        	> \
        > unique_##PointeeType##_ptr
    
    // ---- So kann man jetzt fix Aliase für unique_ptr<...>
    //      erzeugen und verwenden ... ----
    
    #include <stdio.h>
    
    TYPEDEF_UNIQUE_PTR(FILE,fclose); // erstellt Alias unique_FILE_ptr
    
    #include <iostream>
    
    int main() {
    	unique_FILE_ptr p (std::fopen("test.txt","rt"));
    	if (p) {
    		std::cout << "Datei wurde wohl geoeffnet.\n";
    	} else {
    		std::cout << "Datei wurde nicht geoeffnet.\n";
    	}
    	return 0;
    }
    

    Wenn man jetzt aber einen C++ Wrapper für SDL2 basteln will, könnte man das z.B. so machen:

    #ifndef SDL_CXX_WRAPPER
    #define SDL_CXX_WRAPPER
    
    #include <sdl header dateien>
    #include <unique_ptr_alias.hpp>
    
    TYPEDEF_UNIQUE_PTR(SDL_Surface,SDL_FreeSurface);
    TYPEDEF_UNIQUE_PTR(SDL_Palette,SDL_FreePalette);
    TYPEDEF_UNIQUE_PTR(SDL_PixelFormat,SDL_FreeFormat);
    TYPEDEF_UNIQUE_PTR(SDL_Cursor,SDL_FreeCursor);
    
    TYPEDEF_UNIQUE_PTR(SDL_Cond,SDL_DestroyCond);
    TYPEDEF_UNIQUE_PTR(SDL_mutex,SDL_DestroyMutex);
    TYPEDEF_UNIQUE_PTR(SDL_Renderer,SDL_DestroyRenderer);
    TYPEDEF_UNIQUE_PTR(SDL_sem,SDL_DestroySemaphore);
    TYPEDEF_UNIQUE_PTR(SDL_Texture,SDL_DestroyTexture);
    TYPEDEF_UNIQUE_PTR(SDL_Window,SDL_DestroyWindow);
    
    #endif
    

    Das geht mit der GSL-Bibliothek wahrscheinlich ähnlich.

    Das einzige, was dann noch nervt, ist die für C typische Fehlerbehandlung. 😉



  • ScottZhang schrieb:

    Klaus82 schrieb:

    Sehe ich das richtig? Falls ja -> Hä? Warum bleibt es nicht ein rvalue?

    Ja, genau. Weil es "einen Namen hat". Warum weiß ich auch nicht genau, vllt kann das einer der Gurus hier beantworten. Vllt weil man sonst unbemerkt ein wegemovetes objekt übrig haben kann, ka.

    Ja. Es hat einen Namen, also kann man sich mit Hilfe dieses Namens mehrfach auf ein und dasselbe Objekt beziehen. Das hat also gar nichts mehr von einem "temporären Objekt" in dem Kontext. Wenn man da was "moven" will, muss man das eben wieder explizit machen. Das ist gut so; denn man will lieber versehentlich kopieren anstatt versehentlich moven.



  • Aber mal ne ganz andere Frage: Wenn alle Pointer Smartpointer sind, müsste dann nicht sogar der default move-assignment operator funktionieren? Der macht doch auch nichts anderes als elementweise zu moven.



  • TNA schrieb:

    Aber mal ne ganz andere Frage: Wenn alle Pointer Smartpointer sind, müsste dann nicht sogar der default move-assignment operator funktionieren? Der macht doch auch nichts anderes als elementweise zu moven.

    Ja, das würde mich auch interessieren, hatte die Frage ja schon im Eingangspost aufgeworfen. 😃



  • Klaus82 schrieb:

    Ja, das würde mich auch interessieren, hatte die Frage ja schon im Eingangspost aufgeworfen. 😃

    Hab ich doch glatt übersehen 😃
    Schreib doch einfach mal:

    interpolation& interpolation::operator=(interpolation&& rhs) = default;
    

    und schaue ob es kompiliert und das macht, was du möchtest.



  • Also es kompiliert. 🙂

    Was mich nun noch etwas wundert, dass ich dieses Objekt z.B. mittels push_back in einen vector schiebe und der Kompiler sich nicht beschwert.

    Müsste es nicht einen Fehler geben, weil der default Copy Constructor keinen eben solchen für unique_ptr aufrufen kann?

    Gruß,
    -- Klaus.



  • Die meisten std:: Container verlangen keine kopierbaren Elemente mehr. Die müssen aber mindestens Movable sein.



  • Klaus82 schrieb:

    Was mich nun noch etwas wundert, dass ich dieses Objekt z.B. mittels push_back in einen vector schiebe und der Kompiler sich nicht beschwert.

    Wie genau rufst du denn das push_back auf? So dass ein move stattfinden kann?



  • Also ich habe z.B. folgendes Minimalprogramm und kurioser Weise funktioniert dies, obwohl ich dachte, dass wegen der unique_ptr nur die auskommentierte Zeile gehen dürfte.

    Und weil ich einen unique_ptr habe, dachte ich, dass ich Copy Constructor und Assignment Operator verbieten solle, also als private deklarieren sollte.

    //main.cpp
    #include <iostream>
    #include <vector>
    
    #include "interpolation.h"
    
    int main()
    {
    	std::vector<double> x,y;
    
    	x = {0, 1, 2, 3};
    	y = {0, 1, 4, 9};
    
    	std::vector<interpolation> vec;
    
    	//vec.push_back(std::move(interpolation(x,y)));
    	vec.push_back(interpolation(x,y));
    
    	std::cout << vec[0](1.5) << std::endl;
    
    return 0;
    }
    

    Mit folgender header und sourcefile von interpolation

    /*
    interpolation.h
    */
    #ifndef INTERPOLATION_H_
    #define INTERPOLATION_H_
    
    #include <iostream>
    #include <memory>
    #include <vector>
    #include <gsl/gsl_spline.h>
    
    class interpolation
    {
    	public:
    		interpolation();
    		interpolation(std::vector<double>, std::vector<double>);
    		interpolation(interpolation&&) = default;
    		interpolation& operator=(interpolation&&) = default;
    		double operator()(double);
    
    		std::vector<double> x,y;
    		std::unique_ptr<gsl_interp_accel,void(*)(gsl_interp_accel*)> acc;
    		std::unique_ptr<gsl_spline,void(*)(gsl_spline*)> spline;
    
    	private:
    		interpolation(const interpolation&);
    		interpolation& operator=(const interpolation&);
    };
    #endif
    
    /*
    interpolation.cpp
    */
    #include <iostream>
    #include "interpolation.h"
    /*
    ---{}--- default constructor ---{}---
    */
    interpolation::interpolation():
    x(), y(),
    acc(nullptr,nullptr),spline(nullptr,nullptr)
    {}
    /*
    ---{}--- constructor ---{}---
    */
    interpolation::interpolation(std::vector<double> x, std::vector<double> y):
    x(x), y(y),
    acc(gsl_interp_accel_alloc(),gsl_interp_accel_free),
    spline(gsl_spline_alloc(gsl_interp_cspline,x.size()),gsl_spline_free)
    {
    	gsl_spline_init(spline.get(),x.data(),y.data(),x.size());
    }
    /*
    ---{}--- operator() ---{}---
    */
    auto interpolation::operator() (double x) -> double
    {
    	return gsl_spline_eval(spline.get(),x,acc.get());
    }
    

    Gruß,
    -- Klaus.



  • Klaus82 schrieb:

    //main.cpp
    #include <iostream>
    #include <vector>
    
    #include "interpolation.h"
    
    int main()
    {
    	std::vector<double> x,y;
    
    	x = {0, 1, 2, 3};
    	y = {0, 1, 4, 9};
    
    	std::vector<interpolation> vec;
    
    	//vec.push_back(std::move(interpolation(x,y)));
    	vec.push_back(interpolation(x,y));
    
    	std::cout << vec[0](1.5) << std::endl;
    
    return 0;
    }
    

    Das ist klar, dass das funktioniert. Da du das Objekt in dem Funktionsaufruf erzeugst, ist es ein namenloses temporäres Objekt und somit ein rvalue. Es kann also der Move-Konstruktor verwendet werden. Das std::move benötigst du nur, wenn du ein lvalue hast, das aber explizit wie ein rvalue behandelt werden soll. Das ist quasi ein cast. In der Realität würdest du in einem solchen fall natürlich besser gleich emplace_back anstelle vom push_back verwenden.



  • Tipp: Verwende kein std::move wenn es unnötig ist. In den Fällen ist es ggf. sogar kontraproduktiv. Wann gemoved werden kann, weiß der Compiler oft selbst. Die Regeln dazu sind einfach und erfordern keinen besonders intelligenten Compiler. Zum Beispiel ist std::move bei temporären Objekten überflüssig. Es ist auch überflüssig, wenn man ein funktionslokales Objekt per value über retrun zurück geben will. Ggf muss man aus einem copy ein move mit std::move machen; denn alles andere, was einen Namen hat, wird sonst als Lvalue behandelt. Was beim Moven passiert, ist Sache des Klassenautors.


Anmelden zum Antworten