unordered_map emplace mit Parameterpack-Template
-
Hallo zusammen
ich habe ein Verhalten festgestellt, das ich mir nicht erklären kann.
Ich habe eine Klasse "Group", die ein Parameter Pack im Constructor übernimmt und in einem Tuple zwischenpuffert.
Wenn ich eine Instanz dieser Klasse z.B. in ein std::pair move und wieder hinaus move funzt alles perfekt und die Argumente sind korrekt.Wenn ich eine instanz dieser Klasse aber mittels emplace versuche in Map zu werfen, wird nicht der move-Assignment-Operator aufgerufen sondern abermals der Parameter-Pack-Constructor.
Der Constructor ist eigentlich nur "Convenience", aber ich möchte dennoch rausfinden, warum sich die Klasse so verhält bzw. warum emplace dieses Verhalten auslöst.
Jemand ne Idee?#include <cstdint> #include <tuple> #include <iostream> #include <unordered_map> template <typename ...Args> class Group { public: Group( uint32_t id_, Args ... args ) : Key( id_ ), Arguments( std::forward<Args>( args )... ) { std::cout << "C Constructor\n"; } Group( const Group & ) = delete; Group &operator=( const Group & ) = delete; Group &operator=( Group &&other ) { std::cout << "C Move\n"; Arguments = std::move( other.Arguments ); Key = std::move( other.Key ); return *this; } operator bool() const { return true; } inline uint32_t key() const { return Key; } private: uint32_t Key; std::tuple<Args...> Arguments; }; int main( int, char ** ) { int groupkey = 99999; std::unordered_map<int, Group<>> map; map.emplace( 1, Group( groupkey, 13, 19, std::string( "foobar" ) ) ); std::cout << "Ist: " << map.begin()->second.key() << " Soll: " << groupkey << std::endl; return 0; }
-
Welchen Compiler und welche STL-Implementierung verwendest du denn?
Für
std::vector
ist in how to write my own vector.emplace_back beschrieben, daß dort beiemplace_back
intern das placement new mit dem Parameterpack aufgerufen wird (also der von dir angelegte Konstruktor).
Fürstd::map
bzw.std:.unordered_map
sollte das dann nicht viel anders sein - s.a. map::emplace:Inserts a new element into the container constructed in-place with the given args if there is no element with the key in the container.
-
Ich verwende einen MinGW 11.2 64Bit.
-
Group<>
ist eine Group mit leeren "Args", und damit nicht das was du "emplacen" willst:Group<int, int, std::string>
.Daher kommt der move-ctor nicht in Frage, weil ja ne
Group<int, int, std::string>
eben keineGroup<>
ist.Der "Args" ctor von
Group<>
geht aber, weil der nimmt als einzigen Parameter nenuint32_t
, und dieGroup<int, int, std::string>
kann mittelsoperator bool()
+ eingebauter Konvertierungbool -> uint32_t
passend gemacht werden. (1x user defined + 1x built in ist erlaubt).Also 1. Änderung:
//std::unordered_map<int, Group<>> map; std::unordered_map<int, Group<int, int, std::string>> map;
Was dann nicht geht, weil deine Klasse non-copyable ist (copy ctor ist explizit deleted, move ctor ist implizit deleted). Falls du dich wunderst warum hier nochmal kopiert/gemoved wird: es wird ja nur das
pair
in-place konstruiert. Derpair
ctor muss aber noch mit einemfirst
und einemsecond
aufgerufen werden - und die müssen halt vonemplace
in denpair
ctor rein-gemoved werden.- Möglichkeit das zu fixen: mach nen move-ctor.
Group(Group&& other) : Key{other.Key} , Arguments{std::move(other.Arguments)} { }
- Möglichkeit das zu fixen: verwende
std::piecewise_construct
.
map.emplace( std::piecewise_construct, std::tuple{1}, std::tuple{groupkey, 13, 19, std::string("foobar")} );
Das funktioniert dann trotz dem
Group
uncopyable ist, weil hier dann der passende ctor vonpair
aufgerufen wird, derfirst
undsecond
direkt in-place impair
konstruiert. Dabei wird dann neben den Integers nur noch derstd::string
gemoved, alles andere wird in-place konstruiert.
-
Danke euch beiden. Ihr habt mich auf den richtigen Weg gebracht. Klar. Die Map ist mit einem Group<> definiert... Nun dann muss ich auf den Constructor verzichten. Weil es zwar kompiliert, aber dann ein Verhalten entsteht, was dem Nutzer der Klasse nicht klar sein wird.
@hustbaer : danke für den Tipp. Das mit dem pieceweise probiere ich an der Stelle mal.
-
@hustbaer: Das hatte ich gar nicht gesehen. Wow, tolle Erklärung!
-
@It0101 Du könntest auch den
operator bool
explicit
machen. Ich würde sagen: man sollteoperator bool
fast immerexplicit
machen. Sorgt bloss für Verwirrung wenn man es nicht macht. Impliziteroperator bool
erlaubt z.B. das Addieren mit Integers, Addieren mit Zeigern und lauter so Sachen die man normalerweise so gar nicht ermöglichen will. Und halt auch die Verwendung an Stellen wo Integers erwartet werden, wie in diesem Fall.
-
@hustbaer Interessanterweise kompiliert das Beispiel bei mir nicht, wenn ich den bool()-Operator auskommentiere. ^^
-
@It0101 Ja, interessant. Das ist ja Sinn der Sache dass der unsinnige Code nicht mehr kompiliert. Den selben Effekt erreichst du auch wenn du den
operator bool
explicit machst.
Damit kann man immer noch schreibenif (group)
, aber man kann nicht mehr schreibenint i = group;
oderpointer + group
.
-
@hustbaer sagte in unordered_map emplace mit Parameterpack-Template:
@It0101 Ja, interessant. Das ist ja Sinn der Sache dass der unsinnige Code nicht mehr kompiliert. Den selben Effekt erreichst du auch wenn du den
operator bool
explicit machst.
Damit kann man immer noch schreibenif (group)
, aber man kann nicht mehr schreibenint i = group;
oderpointer + group
.Ja, ich muss zugeben dass war mir so nicht bewusst, dass der operator-bool derart vom Compiler missbraucht wird
Ich habe diese operatoren jetzt konsequent explicit gemacht und stelle fest, dass ich froh sein kann, dass die Klasse bisher überhaupt größtenteils funktioniert hat...