Name für Datenstruktur gesucht (eine Art Tupel mit Mengensemantik)



  • In Fortführung der hier erwähnten Problemstellung habe ich mir eine std::tuple<> -artige Datenstruktur namens, nunja, "Gadget" gebaut, die allerdings semantisch etwas abweicht:

    • jeder Typ darf nur einmal vorkommen
    • dadurch kann man implizite Umwandlungen in "kleinere" Gadgets zulassen, außerdem toleriert Gadget<> die Permutation der Argumentliste
    • überhaupt ist ein Gadget<> tolerant gegen implizite Umwandlungen, d.h. man kann auch get<B>() für ein Gadget<A> aufrufen, sofern A "vernünftig" in B wandelbar ist

    Was eine "vernünftige Umwandlung" ist, wäre noch zu klären, aber es sollte meiner Meinung nach mindestens sein:

    • integrale Typen untereinander
    • Gleitkommatypen untereinander
    • const char* <-> std::string<>
    • Zeiger auf Subklasse zu Zeiger auf Basisklasse (auch bei Verwendung von Smartpointern)

    Ich illustriere das am besten an ein paar Beispielen:

    Gadget<int, char, double> g1(42, 'Z', 1.41421);
    auto g1_implicit = makeGadget(42, 'Z', 1.41421);
    Gadget<double, char> g2 = g1; // implizite Permutation und Auswahl
    Gadget<std::optional<int>, std::optional<float>> g3 = g1; // ergibt (42, nullopt)
    Gadget<std::optional<int>, double, char> g4 = makeGadget(g2, g3); // implizites Flattening erlaubt, also Gadget<Gadget<A...>, B...> -> Gadget<A..., B...>
    Gadget<long, std::string> g5 = makeGadget("s", 42); // einige implizite Konvertierungen sind erlaubt (aber nicht alle)
    Gadget<std::unique_ptr<Base>> g6 = makeGadget(std::make_unique<Derived>()); // Zeigerumwandlungen gehen auch
    
    int v1 = get<int>(g1); // 42
    auto v2 = get<const char*>(g5); // string<> -> const char* geht auch
    

    Wie man nun sieht, ist mir noch kein gescheiter Name für diese Datenstruktur eingefallen. Und das ist mein Problem. Es ist kein tuple<> , weil die Position egal ist, es ist aber auch kein set<> , weil jeder Typ nur einmal vorkommt. Es ist eine Art Dictionary, eine Abbildung von einem Typen zu einem Wert dieses Typs, aber das ergäbe einen sehr unhandlichen Namen ( StaticTypeToValueDictionary<> ?).

    Hat jemand dazu vielleicht eine Idee? Einen schönen griffigen Namen, der auf Anhieb einleuchet?



  • Ich tu mich bei sowas auch schwer und da kommen oft seltsame Namen raus 😉
    Vllt iwas wie aggregate ?



  • wie wär's mit deck (im Sinne von card deck)? In einem Skat-Blatt kommt jede Karte nur einmal vor.



  • TypeIndexedTuple

    Bzw. wenn du den Aspekt mit den komischen erlaubten Konvertierungen noch unterbringen willst, dann vielleicht TypeIndexedFluffyTuple 🤡



  • Danke für die Vorschläge. Aber ein deck könnte ja auch ein Doppelkopfblatt sein, nicht? Außerdem hat das irreführende phonetische Nähe zu deque<> . Und aggregate ist schon etwas überladen und sehr unspezifisch. type-indexed tuple trifft es eher, aber zumindest ich würde das als type-indexed only tuple lesen, d.h. "wie tuple<> , aber ohne get<size_t>() ".

    hustbaer schrieb:

    den Aspekt mit den komischen erlaubten Konvertierungen

    So komisch finde ich das gar nicht. Aber ich glaube, meine Auflistung oben war irreführend. Vielleicht sollte ich etwas mehr zur Motivation sagen. Erlaubt mir, daß ich etwas aushole:

    In C++ werden Argumente ja by position übergeben. Daß das nicht immer optimal ist, weiß jeder Python- und C#-Programmierer, weil man dort Argumente auch by name übergeben kann, was oftmals die Lesbarkeit von Code erhöht. (Natürlich kann man auch in C++ by name zu übergeben, müßte sich dann aber eine Argument- struct mit benannten Feldern definieren. Das wird aber nur selten eingesetzt, weil das Ein- und Auspacken syntaktisch sehr umständlich ist.)

    C# hat ja seit einiger Zeit auch Sprachsyntax für Tupel. Das ist im Prinzip nur Syntaxzucker für ValueTuple<> , aber die Sprachdesigner haben die Gelegenheit genutzt, um die Semantik etwas zu erweitern (cf. https://github.com/dotnet/roslyn/issues/11031). So sind Tupel, anders als ValueTuple<> selbst, kovariant in den Argumenttypen:

    (string, Derived) c = ("x", new Derived());
    (string, Base) d = c;
    

    Außerdem können Tupeleinträge einen Namen haben, der im Typen nicht repräsentiert wird. Das führte natürlich zur Frage, wie man mit folgendem Code umgehen sollte:

    public static void Foo((string second, string first) a)
        {
            Console.WriteLine($"{a.first}, {a.second}");
        }
    
        public static void Main()
        {
            (string first, string second) = ("Hello", "World");
            Foo((first: first, second: second)); // #1
            Foo((second: second, first: first)); // #2
        }
    

    #1 aber ergibt eine Warnung, weil die Feldnamen nicht passen, #2 hingegen geht ohne Warnung durch. Anders gesagt: Tupeleinträge sind zwar weiterhin positional, aber der Compiler paßt wenigstens (manchmal :() auf, daß man sie nicht versehentlich durcheinanderbringt. Wenn der Compiler hier etwas flexibler wäre und Permutation und Auswahl erlauben würde, hätte man die Vorteile eines Argument- struct (Argumentidentifikation by name, einfaches Herumreichen aller Argumente auf einmal) und einer Argumentliste (Argumentauswahl, in-place-Konstruktion) kombiniert. Aber das ging den Designern wohl zu sehr in Richtung JavaScript.

    Mir ist nun aufgefallen, daß es Situationen gibt, in denen man eigentlich weder by position noch by name übergeben möchte, sondern by type, unter gleichzeitiger Beibehaltung der Kovarianz. Und zwar immer dann, wenn der Typ einen semantischen Namen trägt. Das ist bei int , string etc. natürlich nicht der Fall (insofern führt meine obige Auflistung in die Irre); aber ein sicheres Anzeichen dafür sind Argumentlisten, in denen der Variablenname dem Typnamen gleicht:

    public void Transmogrify(Frobnicator frobnicator, CancellationToken cancellationToken = default) { ... }
    

    In welchen Situationen treten Typen mit semantischen Namen auf? Immer dann, wenn es innerhalb eines bestimmten Kontextes nur ein sinnvolles Objekt diesen Typs gibt. Dies ist oft der Fall im Zusammenhang mit dependency injection. Wenn man DIContainer.Create<Intf>(args...) sagt, erwartet man, daß der Container genau eine Factory für Klassen registriert hat, die Intf implementieren. Desgleichen für DIContainer.Get<FrobnicatorConfig>() . Das ist genau so eine Typ-Wert-Zuordnung wie bei Gadget<> . Nur daß ich die üblichen DI-Container bescheuert finde wegen global solution to local problem und Umgehung der statischen Typüberprüfungen. Und mithilfe von Gadget<> könnte ich mir eine statisch typisierte lokale Lösung bauen.

    Eine weitere Anwendungsmöglichkeit ergäbe sich in Fällen wie diesem Thread neulich:
    https://www.c-plusplus.net/forum/344347

    class IGreeter
    {
    public:
        virtual void greet(const std::string& name) const = 0;
    };
    class IOpener
    {
    public:
        virtual void open(void) const = 0;
    };
    
    class English : public IGreeter, public IOpener
    {
    public:
        void greet(const std::string& name) const override { ... }
        void open(void) const override { ... }
    };
    
    void open_door(Gadget<IOpener*> g)
    {
        get<IOpener*>(g)->open();
    }
    
    void greet_tom(Gadget<IGreeter*> g)
    {
        get<IGreeter*>(g)->greet("Tom");
    }
    
    void open_door_and_greet_john(Gadget<IOpener*, IGreeter*> g)
    {
        get<IOpener*>(g)->open();
        get<IGreeter*>(g)->greet("John");
    }
    
    int main(void)
    {
        English en;
        auto enGadget = makeGadget(&en);
    
        open_door(enGadget);
        std::cout << "----------------\n";
        greet_tom(enGadget);
        std::cout << "----------------\n";
        open_door_and_greet_john(enGadget);
    }
    

    So, vielleicht regt das bei irgendjemandem die Kreativität an. Bei mir leider noch nicht 😞


Anmelden zum Antworten