std::mem_movable
-
Können
std::memmove()undstd::memcpy()(übrigens ohne Underscore) nicht genau dann sinvoll angewandt werden, wenn es sich um PODs handelt?Ich sehe hier nichts besonders Neues, im Grunde genommen handelt es sich bei
std::move()1 um die gleiche Thematik wie beistd::copy(). Eine kluge Implementierung kann für PODs durchaus Optimierungen anwenden._____
1: Überladung, die eine Iterator-Range verschiebt.
-
Nexus schrieb:
Können
std::memmove()undstd::memcpy()(übrigens ohne Underscore) nicht genau dann sinvoll angewandt werden, wenn es sich um PODs handelt?std::string, std::vector, std::shared_ptr sind keine pods, können aber trotzdem per memcpy gemoved werden (sofern sie sich nicht intern selbst referenzieren, aber warum sollten sie).
-
Das bezweifle ich. Du kannst vielleicht was zusammenhacken, aber es bleibt undefiniertes Verhalten. Schliesslich ist der Implementierung freigestellt, dir mit "warum sollten sie"-Aktionen den Boden unter den Füssen wegzureissen.
-
Natürlich müssen sie nicht mem_movable sein, könnten es aber trotzdem sein. Deshalb meinte ich ja, wäre es sinnvoll, einen type trait zu haben, den man spezialisieren kann.
So etwa:
class string { //... }; template<> class mem_movable< string > : public true_type { }; //ein std::string wird vermutlich keine Zeiger auf sich selbst besitzen //falls er das doch tut, kann man die Spezialisierung ja weglassen
-
Zunächst mal solltest du schon die Optimierungen einschalten, um zu kucken, ob etwas optimiert wird. Dein Testfall erzeugt bei mir selbst mit -O1 nur noch
Move Constructor: 3610000 memcpy: 2130000Dass memcpy hier schneller ist, ist aber wenig verwunderlich, weil es stumpf weniger macht. Wie soll die Laufzeitumgebung deiner Meinung nach später auseinanderhalten, welche Objekte noch zerstört werden müssen? Stell dir vor, ich schreibe
void move_memory_maybe(mem_movable<foo> *dest, mem_movable<foo> *src, std::size_t n) { if(rand() % 2 == 0) std::memcpy(dest, src, n * sizeof(*dest)); }...woher weiß der aufrufende Code später, wofür er Destruktoren aufrufen muss?
-
Es scheint wohl nicht ganz klar zu sein, was ich meinte...
Also:
Ich wünsche mir einen neuen std type trait, nämlich std::mem_movable.
std::mem_movable gibt an, ob ein Typ statt per copy bzw move constructor per memcpy gemoved werden darf.
Jemand der den std::vector baut dürfte z.B. bei allen Typen, für die std::mem_movable wahr ist, statt move constructor und anschließendem destructor auf den alten Objekten einfach memcpy verwenden und den alten Speicher ohne destructor freigeben.
Offensichtlich ist das für POD Klassen automatisch erfüllt, bei anderen Typen würde std::mem_movable per default false ergeben.
Es gibt aber auch noch viele andere Klassen, bei denen das so erfüllt ist, beispielsweise bei den meisten Versionen von std::vector und std::string.
Heutige compiler können move & destructor nicht optimieren, wie das von mir gepostete benchmark zeigt.
Wer solche Klassen entwickelt könnte jetzt also einfach den type trait std::mem_movable für seine Klasse spezialisieren, um trotzdem klar zu machen, dass seine Klasse auf die schnellstmögliche Weise per memcpy gemoved werden darf.
Vor diesem Hintergrund verstehe ich nicht, wieseldon schrieb:
void move_memory_maybe(mem_movable<foo> *dest, mem_movable<foo> *src, std::size_t n) { if(rand() % 2 == 0) std::memcpy(dest, src, n * sizeof(*dest)); }Sinn ergeben soll. Man erstellt keine Instanzen von type traits, die sind doch nur fürs meta programming.
Wenn man move_maybe mit dem normalen move schreiben würde, wüsste der aufrufende code doch auch nicht, was er jetzt zerstören soll:void move_maybe( T *dest , T *src ) { if( rand() % 2 == 0 ) new ( dest ) T( std::move( *src ) ); } //muss auf dest jetzt der destructor aufgerufen werden?
-
GorbGorb schrieb:
Es scheint wohl nicht ganz klar zu sein, was ich meinte...
Also:
Ich wünsche mir einen neuen std type trait, nämlich std::mem_movable.
std::mem_movable gibt an, ob ein Typ statt per copy bzw move constructor per memcpy gemoved werden darf.
Jemand der den std::vector baut dürfte z.B. bei allen Typen, für die std::mem_movable wahr ist, statt move constructor und anschließendem destructor auf den alten Objekten einfach memcpy verwenden und den alten Speicher ohne destructor freigeben.
Offensichtlich ist das für POD Klassen automatisch erfüllt, bei anderen Typen würde std::mem_movable per default false ergeben.
Es gibt aber auch noch viele andere Klassen, bei denen das so erfüllt ist, beispielsweise bei den meisten Versionen von std::vector und std::string.
Heutige compiler können move & destructor nicht optimieren, wie das von mir gepostete benchmark zeigt.
Wer solche Klassen entwickelt könnte jetzt also einfach den type trait std::mem_movable für seine Klasse spezialisieren, um trotzdem klar zu machen, dass seine Klasse auf die schnellstmögliche Weise per memcpy gemoved werden darf.
Vor diesem Hintergrund verstehe ich nicht, wieseldon schrieb:
void move_memory_maybe(mem_movable<foo> *dest, mem_movable<foo> *src, std::size_t n) { if(rand() % 2 == 0) std::memcpy(dest, src, n * sizeof(*dest)); }Sinn ergeben soll. Man erstellt keine Instanzen von type traits, die sind doch nur fürs meta programming.
Wenn man move_maybe mit dem normalen move schreiben würde, wüsste der aufrufende code doch auch nicht, was er jetzt zerstören soll:void move_maybe( T *dest , T *src ) { if( rand() % 2 == 0 ) new ( dest ) T( std::move( *src ) ); } //muss auf dest jetzt der destructor aufgerufen werden?Objekte, die nicht trivial kopiert werden können, per memcpy/memmove zu kopieren, führt zu undefiniertem Verhalten (genauer: das Kopieren selbst ist unproblematisch, aber an der Zieladresse entstehen keine Objekte des entsprechenden Typs, und dann darauf zugreifen zu wollen, als ob entsprechende Objekte da wären führt zu UB, i.d.R. über 3.10/10).
Es wäre denkbar, ein Prädikat zu haben, dass aussagt, ob die Ausführung eines nicht-trivialen Destruktors unterbleiben darf, falls irgendwie eine flache Kopie erstellt wurde. Ohne eine entsprechend universell einsatzbare und schnelle Kopierroutine (und memcpy ist eben genau das nicht), ist es nicht besonders nützlich.
-
GorbGorb schrieb:
Wenn man move_maybe mit dem normalen move schreiben würde, wüsste der aufrufende code doch auch nicht, was er jetzt zerstören soll:
void move_maybe( T *dest , T *src ) { if( rand() % 2 == 0 ) new ( dest ) T( std::move( *src ) ); } //muss auf dest jetzt der destructor aufgerufen werden?Ich wollte eigentlich mehr auf src hinaus. Ein Move-Konstruktor muss ja nicht nur den Inhalt des Quellobjektes kopieren, sondern auch das alte invalidieren. Für std::vector in gccs libstdc++ sieht das etwa so aus (in einer privaten Basisklasse):
_Vector_base(_Vector_base&& __x) : _M_impl(__x._M_get_Tp_allocator()) { this->_M_impl._M_start = __x._M_impl._M_start; this->_M_impl._M_finish = __x._M_impl._M_finish; this->_M_impl._M_end_of_storage = __x._M_impl._M_end_of_storage; __x._M_impl._M_start = 0; __x._M_impl._M_finish = 0; __x._M_impl._M_end_of_storage = 0; }Der Destruktor wird dann für alle Objekte nach wie vor aufgerufen, macht aber für die geleerten (praktisch) nichts. Vor diesem Hintergrund ist es wenig verwunderlich, dass dein Benchmark einen Geschwindigkeitsvorteil für memcpy heraushaut, weil memcpy ja stumpf weniger macht. Ich sehe nicht, wie bei deinem Ansatz sichergestellt werden kann, dass die Laufzeitumgebung bewegte von unbewegten Objekten unterscheiden kann.
-
camper schrieb:
Objekte, die nicht trivial kopiert werden können, per memcpy/memmove zu kopieren, führt zu undefiniertem Verhalten (genauer: das Kopieren selbst ist unproblematisch, aber an der Zieladresse entstehen keine Objekte des entsprechenden Typs, und dann darauf zugreifen zu wollen, als ob entsprechende Objekte da wären führt zu UB, i.d.R. über 3.10/10).
Schade, dann bräuchte ein solches feature also Änderungen am c++ core (und wohl nicht unerhebliche).
Es wäre denkbar, ein Prädikat zu haben, dass aussagt, ob die Ausführung eines nicht-trivialen Destruktors unterbleiben darf, falls irgendwie eine flache Kopie erstellt wurde. Ohne eine entsprechend universell einsatzbare und schnelle Kopierroutine (und memcpy ist eben genau das nicht), ist es nicht besonders nützlich.
Mir ist das Ganze gekommen, als ich über einen move-Konstruktor für shared_ptr nachgedachte habe. Mit mem_movable könnte man sich hier ein
if( ptr )bzw. ein Zugriff auf ein std::atomic beim moven sparen (beides nicht gerade billig).
Außerdem ist das halt etwas, das idiomatisches c++ langsamer als c macht, was ich irgendwie unbefriedigend finde.
-
GorbGorb schrieb:
camper schrieb:
Objekte, die nicht trivial kopiert werden können, per memcpy/memmove zu kopieren, führt zu undefiniertem Verhalten (genauer: das Kopieren selbst ist unproblematisch, aber an der Zieladresse entstehen keine Objekte des entsprechenden Typs, und dann darauf zugreifen zu wollen, als ob entsprechende Objekte da wären führt zu UB, i.d.R. über 3.10/10).
Schade, dann bräuchte ein solches feature also Änderungen am c++ core (und wohl nicht unerhebliche).
Ich gehe sogar soweit zu behaupten dass so ein Feature nicht konsistent in die Sprache eingeführt werden könnte.
-
pumuckl schrieb:
Ich gehe sogar soweit zu behaupten dass so ein Feature nicht konsistent in die Sprache eingeführt werden könnte.
c++ und Konsistenz... aber das ist ein anderes Thema.
-
GorbGorb schrieb:
c++ und Konsistenz... aber das ist ein anderes Thema.
Beispiele?
-
otze schrieb:
GorbGorb schrieb:
c++ und Konsistenz... aber das ist ein anderes Thema.
Beispiele?
Typenme z.B.
(Von Effective C++ kopiert)
template<typename T> class Derived : public Base<T>::Nested { //typename nicht zulässig public: explicit Derived(int x) : Base<T>::Nested(x) {} // typename nicht zulässig // ... void some_member_function() { //... typename Base<T>::Nested temp; // typename erforderlich //... } };Auch wenn einem die Regeln klar sind, warum man an den verschiedenen Stellen typename braucht bzw. es nicht zulässig ist, so ist es dennoch inkonsistent. Man hätte typename wenigstens dann, wenn es unnötig ist, trotzdem zulassen können.
Man muss ja auch bei geerbten virtuellen Funktionen das
virtualnicht mehr extra hinschreiben, aber es ist kein Fehler, wenn man es trotzdem tut.class Base { virtual foo(); virtual bar(); }; class Derived { foo(){ // implizit virtual // ... } virtual bar(){ // virtual zwar unnötig, aber dennoch kein Fehler // ... } };
-
Mist, Derived im zweiten Beispiel sollte natürlich von Base ableiten!
-
Zuerst mal wäre da die Erbsünde c zu nennen, Präprozessor, Ellipsen, etc.
Dann kommen noch ein paar Sachen von c++ dazu:- don't pay what you don't use wird verletzt:
- rtti
- exceptions (war es nicht so dass die compiler dann nicht so gut optimieren
können? wobei exceptions ein Programm auch schneller machen können, hier gibt es wohl keinen Königsweg) - die Standardbibliothek ist mit der Sprache verwurstelt: typeid, dynamic_cast und sizeof
- private Member sieht man auch in einer abgeleiteten Klasse:
class A { }; class Base { class A { }; }; class Derived : public Base { A a; };- template Syntax, bin ja grade erst wieder auf die Schnauze geflogen, weil sowas möglich ist:
template< template< class > class Template > class A { }; template< class Type > class B { }; template< class Type > class C { A< C > a; //C als class template B< C > b; //C als class };
- don't pay what you don't use wird verletzt:
-
bmario_ schrieb:
Auch wenn einem die Regeln klar sind, warum man an den verschiedenen Stellen typename braucht bzw. es nicht zulässig ist, so ist es dennoch inkonsistent.
Unintuitiv, aber nicht inkonsistent. Inkonsistenz würde bedeuten, dass der Standard sich selbst widerspricht.
GorbGorb schrieb:
Zuerst mal wäre da die Erbsünde c zu nennen, Präprozessor, Ellipsen, etc.
Was ist da inkonsistent?
don't pay what you don't use wird verletzt:
- rtti
- exceptions (war es nicht so dass die compiler dann nicht so gut optimieren
können? wobei exceptions ein Programm auch schneller machen können, hier gibt es wohl keinen Königsweg)Wo wird da was verletzt? Für Klassen ohne virtuelle Funktionen gibts auch kein RTTI. Wenn man keine Exceptions benutzt, verlangt der Standard auch keinen Overhead dafür. (Was die Compiler draus machen ist was anderes)
[*] die Standardbibliothek ist mit der Sprache verwurstelt: typeid, dynamic_cast und sizeof
Mal abgesehen davon dass weder dynamic_cast noch sizeof irgendwas mit der Standardbibliothek zu tun haben: Was macht den Standard in sich inkonsistent, wenn zwei seiner Features (typeid-Operator und type_info Klasse) voneinander abhängig sind? Beide sind Teil des Standards, genauso wie Klassen und Memberfunktionen - letztere würden ohne erstere auch keinen Sinn machen.
[*] private Member sieht man auch in einer abgeleiteten Klasse:
class A { }; class Base { class A { }; }; class Derived : public Base { A a; };Da bist du einem Irrtum aufgesessen. Derived sieht nur ::A, nicht Base::A.
[*] template Syntax, bin ja grade erst wieder auf die Schnauze geflogen, weil sowas möglich ist:
template< template< class > class Template > class A { }; template< class Type > class B { }; template< class Type > class C { A< C > a; //C als class template B< C > b; //C als class };Und? Wo ist die Inkonsistenz? Dass du mit den Regeln auf dem Kriegsfuß stehst und Probleme damit hast, bedeutet doch nicht, dass der Standard inkonsistent ist.
-
Zu GorbGorbs Beitrag wollte ich auch gerade was schreiben, pumuckl hat aber schon alles gesagt

Wie sieht es aber mit dem
typenameaus, warum genau ist das bei Vererbungs- und Initialisierungsliste verboten? Ich würde hier übrigens auch von Inkonsistenz sprechen, dazu muss kein Widerspruch vorhanden sein.
-
GorbGorb schrieb:
Zuerst mal wäre da die Erbsünde c zu nennen, Präprozessor, Ellipsen, etc.
Mit beidem kann man tolle Sachen machen. Den Präprozessor, um Schreibarbeit zu sparen, Ellipsen für TMP.
-
Ich denke man sollte inkonsistent hier schon als unintuitiv zu den sonstigen Regeln sehen, dass der Standard selbst nicht zweideutig ist, ist glaube ich jedem hier klar.
@virtual -> Hmja, man sollte virtual wohl eher zur Pflicht machen, anstatt es zu verbieten.
@Don't pay
1. @RTTI -> Hm.. man zahlt dafür wenn man es nicht nutzt? Das ist mir neu.
2. @exceptions -> Ohne try/catch hast du keine Nachteile. Und try/catch zu nutzen ohne etwas zu werfen ist irgendwie.. suboptimal.
@die Standardbibliothek ist mit der Sprache verwurstelt.. ->
@template -> Na ja. Das würde ich in der Tat als etwas inkonsistent ansehen, aber es ist einfach zu hilfreich um es abzuschaffen.
-
pumuckl schrieb:
bmario_ schrieb:
Auch wenn einem die Regeln klar sind, warum man an den verschiedenen Stellen typename braucht bzw. es nicht zulässig ist, so ist es dennoch inkonsistent.
Unintuitiv, aber nicht inkonsistent. Inkonsistenz würde bedeuten, dass der Standard sich selbst widerspricht.
Inkonsistent in dem Sinne, dass an vielen Stellen in C++ implizit Festlegungen gelten, aber es kein Fehler ist, diese dennoch explizit aufzuschreiben.
Beispiele dafür:
- in der Klassendefinition definierte Funktionen sind implizit inline, aber es ist kein Fehler, ein inline explizit hinzuschreiben.
- definiert man einen der großen Drei nicht, wird vom Kompiler implizit einer generiert, aber es ist kein Fehler, einen semantisch äquivalenten explizit zu definieren.
- beim Ableiten einer Klasse wird ohne Schlüsselwort implizit angenommen, dass die Ableitungpublicist, aber es ist kein Fehler dies explizit hinzuschreiben
- geerbte virtuelle Funktionen sind implizit virtuell, aber es ist kein Fehler, dies explizit hinzuschreiben.vs.
- An Stellen, wo Typen erwartet werden, werden geschachtelte abhängige Namen implizit als Typen angenommen, ABER es ist ein Fehler, dies explizit hinzuschreiben
- Voll qualifizierte geschachtelte Namen werden implizit als Typen erkannt, aber es ist ein Fehler, dies explizit hinzuschreiben.Das würde ich schon inkonsistent nennen, aber man kann natürlich jetzt darüber diskutieren, was man als inkonsistent ansieht
