[Gelöst] Spezialisierung std::swap
-
@DocShoe sagte in Spezialisierung std::swap:
So wie ich das verstehe ist das der Anwendungsfall für swap, aber nicht die Implementierung
Hi,
ja, der erste Teil dreht sich darum, wie man das implementiert, aber da macht er es ja genauso wie du.
Sorry, da hab ich nicht richtig geguckt und hatte nur was mit "da muss std::swap in den namespace mit rein", was mich getriggert hat.
Was anderes: Gibt es einen Unterschied, wenn du den Typ in der Spezialisierung nochmal explizit angibst?namespace std { template<> void swap<A::B::C::MyClass>( A::B::C::MyClass& lhs, A::B::C::MyClass& rhs ) noexcept { lhs.swap( rhs ); } }
-
@DocShoe Doofe Frage:
MyClass
ist nur Minimalbeispiel, oder? Ich gehe davon aus, dass es für die tatsächliche Klasse einen guten Grund gibt,swap
zu implementieren (z.B. weil Move/Copy-Constructor/Assignment nicht zur Verfügung stehen, du diese überswap
implementieren willst oder aus Performance-Gründen)? Ich denke mal ja.Ansonsten bin ich auch überrascht wegen der Fehlermeldung. Ich hätte erwartet, dass man das exakt so implementiert und kann auch auf Godbolt keinen Compiler finden, der bei deinem Beispiel meckert.
swap
ist allerdings auch eine Funktionalität der C++Standardbibliothek und soweit ich weiss verwendet der Embarcadero Compiler - im Gegensatz zum LLVM clang 5 hier eine implementierung von Dinkumware. Ich würde mal tippen, dass dort die Ursache dieser Fehlermeldung zu finden ist. Zur Dinkumware-Implementierung kann ich leider nicht viele frei verfügbare Informationen finden - eventuell solltest du dir mal deren<algorithm>
-Header ansehen und herausfinden, wie dortstd::swap
umgesetzt wurde.
-
Die Idee ist ja, wenn die Standardbibliothek (und andere Libraries natürlich auch), es so machen, wie Meyers es empfiehlt (und das sollten sie, denn darauf beruht das Swappable Konzept in C++17/C++20), dann brauchst du keine Spezialisierung in std, sondern eine Überladung im eigenen Namespace ist ausreichend und besser. Und vor allem entfallen die ganzen Schwierigkeiten vonwegen der Spezialisierung in std, das ist quasi die Hauptmotivation hinter diesem Konzept. Streng genommen darf man ab C++20 swap gar nicht mehr in std spezialisieren, vielleicht ist es das was deinen Compiler stört.
C++20 Standard 16.4.5.2.1, 2 sagte:
Unless explicitly prohibited, a program may add a template specialization for any standard library class template to namespace std provided that (a) the added declaration depends on at least one program-defined type and (b) the specialization meets the standard library requirements for the original template
Explizit so geändert, dass da nur noch Klassenspezialisierungen zugelassen sind, keine Funktionstemplates.
-
@SeppJ sagte in Spezialisierung std::swap:
Die Idee ist ja, wenn die Standardbibliothek (und andere Libraries natürlich auch), es so machen, wie Meyers es empfiehlt [...]
Bin auf einen uralten Thread gestossen, der interessanterweise die Dinkumware STL zu diesem Thema erwähnt:
@Werner-Salomon sagte in swap, abs & Co im template mit oder ohne std:::
Wirft man jetzt aber einen Blick in die Implementierung der (Dinkumware-)STL so findet man dort das explicite std::swap vor, was zwar i.A. funktionieren wird, aber eben bei selbst geschriebener swap-Funktion, die sich nicht im namespace std befindet (darf sie das?, soll sie das?) nicht das gewünschte Ergebnis liefert.
Ich hoffe mal, dass sich das in den letzten 16 Jahren geändert hat, sonst bräuchte man soweit ich beurteilen kann tatsächlich die
std
-Spezialisierung. Noch ein Grund mehr, sich den Code der Standardbibliothek mal anzusehen bzw. Tests zu schreiben, die feststellen, ob das eigeneswap
tatsächlich aufgerufen wird.
-
@DocShoe Kannst Du die Implementierung von Deiner stdlib einsehen und die Funktionsprototypen posten? Koennte ja irgendwas albernes sein, wie ein fehlendes noexcept oder so.
-
@Finnegan
Nein, das ist nur ein Minimalbeispiel, die echte Klasse hat noch einen std::vector<double>, bei dem ich unnötige Kopien gern vermeiden würde.Eigentlich ist es noch verrückter:
Ich habe ein paar Klassen, für die ich die swap Funktion anbieten möchte. Am Ende der Header Datei habe ich einen Block:
namespace A { namespace B { namespace C { class MyClassA { public: void swap( MyClassA& other ) noexcept { } }; class MyClassB { public: void swap( MyClassB& other ) noexcept { } }; ... // 3 weitere Klassen ... } // namespace C } // namespace B } // namespace A namespace std { template<> void swap( A::B::C::MyClassA& lhs, A::B::C::MyClassA& rhs ) noexcept { lhs.swap( rhs ); } template<> void swap( A::B::C::MyClassB& lhs, A::B::C::MyClassB& rhs ) noexcept { lhs.swap( rhs ); } template<> void swap( A::B::C::MyClassC& lhs, A::B::C::MyClassC& rhs ) noexcept { lhs.swap( rhs ); } template<> void swap( A::B::C::MyClassD& lhs, A::B::C::MyClassD& rhs ) noexcept { lhs.swap( rhs ); } template<> void swap( A::B::C::MyClassE& lhs, A::B::C::MyClassE& rhs ) noexcept { lhs.swap( rhs ); } }
Das sind alles Klassen, die keine Abhängigkeiten, d.h. keine Klasse erbt von irgendwas. Der Compiler bemängelt nur die Zeilen 5 & 6, alle anderen gehen.
Die Dinkumware-Implementation der swap-Funktion sieht so aus:
template<class _Ty, class = enable_if_t<is_move_constructible<_Ty>::value && is_move_assignable<_Ty>::value, void> > inline void swap(_Ty& _Left, _Ty& _Right) _NOEXCEPT_OP(is_nothrow_move_constructible<_Ty>::value && is_nothrow_move_assignable<_Ty>::value) { // exchange values stored at _Left and _Right _Ty _Tmp(_Move(_Left)); _Left = _Move(_Right); _Right = _Move(_Tmp); }
-
Langsam kommt Licht in's Dunkel. Die beiden Klassen, für die der Compiler Fehler anzeigt, haben einen expliziten Konstruktor, alle anderen nicht. Die einzige Erklärung, die ich da habe, ist dass mindestens eins der Prädikate
is_move_constructible
,is_move_assignable
,is_nothrow_move_constructible
oderis_nothrow_move_assignable
nicht erfüllt ist. Ich habe eine Klasse dann um die passenden Konstruktoren/Assignment Operatoren erweitert:class MyClassA { public: MyClassA() = default; explicit MyClass( std::size_t window_size ); MyClassA( MyClassA const& other ) = default; MyClassA( MyClassA && other ) = default; MyClassA& operator=( MyClassA const& other ) = default; MyClassA& operator=( MyClassA && other ) = default; };
Das Problem bleibt, aber das ist mal ein Ansatz.
So, weiter gegraben und das hier festgestellt: Die beiden Klassen benutzen intern eine Klasse
RingBuffer<double>
, die selbst die Rule of Five implementiert. Für RingBuffer<T> darf keine swap-Spezialisierung in der Formnamespace std { template<typename T> void swap( RingBuffer<T>& lhs, RingBuffer<T>& rhs ) noexcept { /*... */ } }
implementiert werden, weil man das nicht tun soll (versuche grade noch rauszufinden, warum man das nicht tun soll).
Edit:
Erklärung: Man darf Templates im std-Namespace spezialisieren, aber keine neuen Funktionen hinzufügen. Weil das obige swap keine Spezialisierung ist, sondern eine neue swap-Funktion für RingBuffer-templates ist, ist das illegal. Ich dürfte std::swap nur für konkrete RingBuffer-Typen spezialisieren:namespace std { template<> void swap( RingBuffer<double>& lhs, RingBuffer<double>& rhs ) noexcept { /* ... */ } }
-
Soooo... Problem gefunden und gelöst. Das Problem war die Klasse
RingBuffer
, die zwar die Rule of Five implementiert, aber eben nicht richtig, weil der Move-Constructor und Move-Assignment nicht noexcept waren. Ich habe das ergänzt und jetzt kompiliert's. Werd' jetzt mal alle Move-Constructor und Move-Assignment prüfen, ob sie noexcept sind.
Vielen Dank nochmal.
-
Das heißt, der Code, wie im ersten Beitrag gezeigt, hätte bei dir compiliert? Und erst bei einem nicht gezeigten Ringbuffermember würde der Fehler auftreten? Ich sollte hier irgendwas über die Erstellung von Minimalbeispielen schreiben, von dem ich eigentlich denken würde, das du es wüsstest.
-
Vermutlich.
Das Minimalbeispiel wäre wahrscheinlich ziemlich groß geworden, weil der RingBuffer selbst wieder einige Abhängigkeiten hat.
-
@DocShoe sagte in [Gelöst] Spezialisierung std::swap:
Vermutlich.
Das Minimalbeispiel wäre wahrscheinlich ziemlich groß geworden, weil der RingBuffer selbst wieder einige Abhängigkeiten hat.Der Punkt ist eher, dass du bei der Erstellung des Minimalbeispiels stets hättest ausprobieren müssen, ob der Fehler noch auftritt. Dann wäre aufgefallen, dass der Fehler nicht mehr auftritt, wenn man den Ringbuffer entfernt. Dann wäre die Fehlersuche viel gezielter verlaufen, anstatt hier vermeintliche Compilerfehler zu jagen.
-
@DocShoe aber überprüfen ob in einem geposteten Minimalbeispiel der Fehler auch wirklich Auftritt würde einiges rätseln ersparen
Edit: @SeppJ war schneller
-
Jut, dann entschuldige ich mich für den unnötigen Aufwand, den ich bereitet habe.
Wer von den Antwortenden mal im Ruhrgebiet sein sollte: Meldet euch, ich geb ein/zwei/drei Bier aus.
-
@DocShoe sagte in [Gelöst] Spezialisierung std::swap:
Jut, dann entschuldige ich mich für den unnötigen Aufwand, den ich bereitet habe.
Wer von den Antwortenden mal im Ruhrgebiet sein sollte: Meldet euch, ich geb ein/zwei/drei Bier aus.Bin Karneval in Köln. Gib jedem Piraten ein Bier aus, den du siehst!
-
a) Köln ist nicht Ruhrgebiet
b) Ich frag jeden Piraten vorher nach std::swap Spezialisierungen. Der Pirat, der mir was dazu sagen kann, bekommt ein Bier.
-
@DocShoe sagte in [Gelöst] Spezialisierung std::swap:
a) Köln ist nicht Ruhrgebiet
Sei mal nicht so kleinlich, immerhin ist die Metropolregion Rhein-Ruhr die einzige deutsche "Megacity" ... die zwei Rübenacker dazwischen und die kommunale Organisationsstruktur machen den Braten nicht wirklich fett
-
@DocShoe sagte in [Gelöst] Spezialisierung std::swap:
Soooo... Problem gefunden und gelöst. Das Problem war die Klasse
RingBuffer
, die zwar die Rule of Five implementiert, aber eben nicht richtig, weil der Move-Constructor und Move-Assignment nicht noexcept waren.Da bist Du auf die Bugs in der Definition der Standard Library gestoßen von denen ich schon mehrfach geschrieben habe. Die Norm fordert, dass swap und move noexcept sind, weil bestimmte Mikrooptimierungen genutzt werden können sollen. Diese können aber nur dann genutzt werden, wenn der Allocator dies durch
propagate_on_container_move_assignment
oder durchis_always_equal
anzeigt. Sollte das beides nicht erfüllt sein, und Du änderst das trotzdem auf noexcept, hast Du UB ins Programm eingebaut.
-
@DocShoe sagte in [Gelöst] Spezialisierung std::swap:
Das Problem war die Klasse RingBuffer, die zwar die Rule of Five implementiert, aber eben nicht richtig, weil der Move-Constructor und Move-Assignment nicht noexcept waren.
Nur fuer mein eigenes Verstaendnis: Das Problem war, dass die explizite Spezialisierung fälschlicherweise
noexcept
markiert war, wo das stdlib Templatenoexcept(false)
markiert haette (weil letzteres ad hoc deduziert wird)?@john-0 Das hat eigentlich mehr mit exception safety zu tun, und ueberhaupt sprechen wir hier von swap und nicht von Containern, aber juckt Dich das eigentlich?
-
@Columbo Nein, @john-0 juckt prinzipiell nix, will nur vorbringen was ihm gerade durchs Hirn spukt. Aber ich geh' jetzt nachschauen was template deduction zu noexcept sagt, danke dafür
-
@Columbo sagte in [Gelöst] Spezialisierung std::swap:
@john-0 Das hat eigentlich mehr mit exception safety zu tun, und ueberhaupt sprechen wir hier von swap und nicht von Containern, aber juckt Dich das eigentlich?
TLDR
Wäre die Norm korrekt, hätte DocShoes Code ohne Mikrooptimierungen übersetzt werden müssen.@Columbo Man merkt mal wieder, dass Du keine Ahnung hast.
Das Thema hängt direkt zusammen, weil in der ISO Norm Annahmen gemacht werden, damit Mikrooptimierungen genutzt werden können. D.h. die Norm fordert, dass
move
undswap
noexcept sind und der Aufwand dafür O(1) ist, weil nur so Zeiger auf Daten direkt getauscht werden können. Diese Annahme ist aber nur dann erfüllbar, wenn der verwendete Allocator die entsprechenden Member (IAE, POCS, POCMA) definiert. Alle Algorithmen, Container, Funktionen, … in der Norm setzen dieses Verhalten voraus, obwohl die Norm explizit erlaubt, dass Allocatoren sich so nicht verhalten müssen.Korrekt wäre es, wenn die Norm die Mikrooptimierungen und Exception Safety in der Standard Library nicht voraussetzen würde, sondern nur dann, wenn man durch die entsprechenden Meta Programming Prädikate für einen durch den Nutzer definierten Typen anzeigen. DocShoe führte doch die Prädikate
is_move_constructible
,is_move_assignable
,is_nothrow_move_constructible
oderis_nothrow_move_assignable
an. Wenn ein Allocator weder (IAE, POCS, POCMA) definiert kann wederis_nothrow_move_constructible
nochis_nothrow_move_assignable
erfüllt sein, weil dann immer die Möglichkeit besteht, dass alloziert und kopiert wird und somit kann das durch den Nutzer definierteswap
bzw.move
auch nicht noexcept sein. Oder anders formuliert wozu gibt es all die schönen Prädikate, wenn die Library sie selbst nicht nutzt?