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 bei emplace_back intern das placement new mit dem Parameterpack aufgerufen wird (also der von dir angelegte Konstruktor).
    Für std::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 keine Group<> ist.

    Der "Args" ctor von Group<> geht aber, weil der nimmt als einzigen Parameter nen uint32_t, und die Group<int, int, std::string> kann mittels operator bool() + eingebauter Konvertierung bool -> 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. Der pair ctor muss aber noch mit einem first und einem second aufgerufen werden - und die müssen halt von emplace in den pair ctor rein-gemoved werden.

    1. Möglichkeit das zu fixen: mach nen move-ctor.
        Group(Group&& other)
            : Key{other.Key}
            , Arguments{std::move(other.Arguments)}
        {
        }
    
    1. 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 von pair aufgerufen wird, der first und second direkt in-place im pair konstruiert. Dabei wird dann neben den Integers nur noch der std::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 sollte operator bool fast immer explicit machen. Sorgt bloss für Verwirrung wenn man es nicht macht. Impliziter operator 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 schreiben if (group), aber man kann nicht mehr schreiben int i = group; oder pointer + 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 schreiben if (group), aber man kann nicht mehr schreiben int i = group; oder pointer + 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...


Anmelden zum Antworten