Type-safe integer handles - enum class vs. template class vs. ...?



  • Ich arbeite gerade wieder mal an einem Programmteil wo ich integer Handles benötige. Die übliche Variante ist hier einfach nen {{typedef}} zu machen. Ich hätte die Sache aber gerne type safe.

    Ich sehe jetzt zwei Möglichkeiten:

    1. enum class my_handle : uint64_t { invalid = 0 };

    2. Ein Klassentemplate mit schönen operator dies, operator jenes, ala

    template <typename TRAITS>
    class integer_handle final {
    public:
    	typedef typename TRAITS::value_type value_type;
    
    	integer_handle()
    		: m_value(TRAITS::invalid_value) {}
    
    	explicit integer_handle(value_type value)
    		: m_value(value) {}
    
    	bool is_valid() const {
    		return m_value != TRAITS::invalid_value;
    	}
    
    	explicit operator bool () const {
    		return is_valid();
    	}
    
    	value_type value() const {
    		return m_value;
    	}
    
    	friend bool operator == (integer_handle x, integer_handle y) {
    		return x.value() == y.value();
    	}
    	friend bool operator != (integer_handle x, integer_handle y) {
    		return x.value() != y.value();
    	}
    
    	friend bool operator < (integer_handle x, integer_handle y) {
    		return x.value() < y.value();
    	}
    
    private:
    	value_type m_value;
    };
    
    template <typename TRAITS>
    struct hash<integer_handle<TRAITS>> {
    	size_t operator()(integer_handle<TRAITS> const& h) const {
    		return std::hash<typename TRAITS::value_type>{}(h.value());
    	}
    };
    
    // ...
    
    struct my_handle_traits {
    	typedef unsigned long long value_type;
    	static constexpr value_type invalid_value = 0;
    };
    
    using my_handle = integer_handle<my_handle_traits>;
    

    Spricht etwas gegen (1) bzw. deutlich für (2)? Und wenn ja, kennt ihr eine fertige Implementierung von (2)?

    Anmerkungen/Korrekturen/... zu meiner Implementierung von (2) sind auch jederzeit willkommen.



  • Bei Lösung 2 könnte man gleich RAII integrieren. Bei Lösung 1 wäre wohl ein externer RAII Scope nötig. Das würde ich als Vorteil für Lösung 2 sehen.



  • RAII soll aber gar nicht integriert werden 🙂

    Es geht hier bloss um ein "non-owning" Vehikel für Dinge die als Integer darstellbar sind, aber halt eine bestimmte Bedeutung haben.

    Dabei können diese Dinge für Resourcen stehen oder auch nicht. In dem speziellen Fall um den es mir erstmal geht tun sie das, daher hab' ich auch "handle" geschrieben. Allerdings werden diese Handles dann auch irgendwo per Interop in Teile rübergeschoben die in anderen Sprachen implementiert sind, bzw. aus solchen Teilen übernommen. RAII wäre hier sehr hinderlich.


  • Mod

    Wir sprechen hier von einem Handle. Also ein Wert, auf dem keinerlei Operationen ausgeführt werden sollen, der quasi nur gehasht und/oder gegen andere verglichen wird. Da brauchen wir keine Klasse; Enumerationen unterstützen diese Operationen, und verhalten sich typsicher, soweit ich deine Definition davon teile. Vielleicht noch eine Fabrikfunktion à la handle_from<MyEnum>(someInt), die auch den Wert überprüft.

    Das einzige, was deine Klasse bietet, ist die Kapselung. Must natürlich selber wissen, ob das benötigt ist oder nicht.



  • Bin mir nicht sicher ob ich verstehe was du mit Kapselung meinst - ich sehe da nicht wirklich etwas im klassischen Sinn gekapselt. Die Klasse hat schliesslich keine Invarianten. Man kann sie aus dem "underlying type" konstruieren und den Wert als "underlying type" wieder abfragen, und es gibt keinerlei asserts oder sonstige Checks.

    Die zwei Vorteile die ich sehe sind der Default-Ctor der den Wert auf einen bekannten "invalid value" Wert initialisiert, und Convenience Helper Funktionen wie "is_valid".

    Und was ich mit typsicher meine ist einfach dass es keine impliziten Konvertierungen gibt, also weder in der Richtung int -> handle noch in Richtung handle -> int. Mit enum class wäre das ja beides gegeben.


  • Mod

    @hustbaer sagte in Type-safe integer handles - enum class vs. template class vs. ...?:

    Bin mir nicht sicher ob ich verstehe was du mit Kapselung meinst - ich sehe da nicht wirklich etwas im klassischen Sinn gekapselt. Die Klasse hat schliesslich keine Invarianten. Man kann sie aus dem "underlying type" konstruieren und den Wert als "underlying type" wieder abfragen, und es gibt keinerlei asserts oder sonstige Checks.

    Ah, das hatte ich gar nicht beachtet. Warum sollte man das anbieten!? Das ist doch gerade, was wir den User nicht machen lassen wollen.

    Die zwei Vorteile die ich sehe sind der Default-Ctor der den Wert auf einen bekannten "invalid value" Wert initialisiert, und Convenience Helper Funktionen wie "is_valid".

    Wie gesagt, wenn die Erzeugung eines solchen Objekts nur über eine Fabrikfunktion "erlaubt" wird, ist das alles einerlei. is_valid kann man auch implementieren, indem man gegen einen Enumerator eines entsprechenden Werts vergleicht. Mit Kapselung meinte ich hier, dass ein Enumerations-Objekt immer konvertiert werden kann. Die Klasse kann den Wert vollkommen verdecken.

    Und was ich mit typsicher meine ist einfach dass es keine impliziten Konvertierungen gibt, also weder in der Richtung int -> handle noch in Richtung handle -> int. Mit enum class wäre das ja beides gegeben.

    Implizite Konvertierungen mit enum class?



  • @columbo sagte in Type-safe integer handles - enum class vs. template class vs. ...?:

    @hustbaer sagte in Type-safe integer handles - enum class vs. template class vs. ...?:

    Bin mir nicht sicher ob ich verstehe was du mit Kapselung meinst - ich sehe da nicht wirklich etwas im klassischen Sinn gekapselt. Die Klasse hat schliesslich keine Invarianten. Man kann sie aus dem "underlying type" konstruieren und den Wert als "underlying type" wieder abfragen, und es gibt keinerlei asserts oder sonstige Checks.

    Ah, das hatte ich gar nicht beachtet. Warum sollte man das anbieten!? Das ist doch gerade, was wir den User nicht machen lassen wollen.

    Wegen Interop mit anderen Sprachen die vom C++ Typsystem nichts wissen. Klar könnte man bei der Konvertierung Integer -> Handle direkt prüfen ob es den Handle-Wert gibt. Mag sein dass das in bestimmten Fällen sinnvoll wäre, in meinem ist es das allerdings nicht. Weil früher oder später sowieso das Handle "dereferenziert" wird, und dabei kommt dann spätestens raus wenn das Handle ungültig war - und das ist für mich ausreichend. Ein weiterer Check davor würde nur unnötig Rechenzeit verschlingen.

    Und was ich mit typsicher meine ist einfach dass es keine impliziten Konvertierungen gibt, also weder in der Richtung int -> handle noch in Richtung handle -> int. Mit enum class wäre das ja beides gegeben.

    Implizite Konvertierungen mit enum class?

    Ah, Misverständnis. Ich meinte mit enum class ist gegeben dass es keine implizite Konvertierung gibt.


    Also verstehe ich das richtig das du hier enum class verwenden würdest?


  • Mod

    @hustbaer sagte in Type-safe integer handles - enum class vs. template class vs. ...?:

    @columbo sagte in Type-safe integer handles - enum class vs. template class vs. ...?:

    @hustbaer sagte in Type-safe integer handles - enum class vs. template class vs. ...?:

    Bin mir nicht sicher ob ich verstehe was du mit Kapselung meinst - ich sehe da nicht wirklich etwas im klassischen Sinn gekapselt. Die Klasse hat schliesslich keine Invarianten. Man kann sie aus dem "underlying type" konstruieren und den Wert als "underlying type" wieder abfragen, und es gibt keinerlei asserts oder sonstige Checks.

    Ah, das hatte ich gar nicht beachtet. Warum sollte man das anbieten!? Das ist doch gerade, was wir den User nicht machen lassen wollen.

    Wegen Interop mit anderen Sprachen die vom C++ Typsystem nichts wissen. Klar könnte man bei der Konvertierung Integer -> Handle direkt prüfen ob es den Handle-Wert gibt. Mag sein dass das in bestimmten Fällen sinnvoll wäre, in meinem ist es das allerdings nicht. Weil früher oder später sowieso das Handle "dereferenziert" wird, und dabei kommt dann spätestens raus wenn das Handle ungültig war - und das ist für mich ausreichend. Ein weiterer Check davor würde nur unnötig Rechenzeit verschlingen.

    Wozu also is_valid?

    Und was ich mit typsicher meine ist einfach dass es keine impliziten Konvertierungen gibt, also weder in der Richtung int -> handle noch in Richtung handle -> int. Mit enum class wäre das ja beides gegeben.

    Implizite Konvertierungen mit enum class?

    Ah, Misverständnis. Ich meinte mit enum class ist gegeben dass es keine implizite Konvertierung gibt.


    Also verstehe ich das richtig das du hier enum class verwenden würdest?

    Wenn nichts dagegen spricht, warum den Aufwand auf sich nehmen, eine Klasse zu schreiben?
    Was ich tun würde, hängt einfach vom Rahmen ab, den ich natürlich nur erraten kann. Tatsache ist, ein enum class ist einfacher zu begreifen als deine Klasse. Bspw. ist mir der operator bool suspekt. Sicher, dass jeder richtig errät, welche Bedeutung er hat?



  • Ich frage mich hauptsächlich was andere Programmierer besser verstehen werden. Ich hab' bisher noch keine Libraries gesehen die etwas anderes als "nackte" Integers für solche Handles verwenden. Was mich etwas verwirrt, da ich mir eben Type-Safety wünschen würde.

    Und enum impliziert halt irgendwie Enumeration. Was bei nem Handle ja überhaupt nicht stimmt. Bzw. es verwirrt manche C++ Programmierer sogar schon dass man enums verwenden kann um beliebige Werte zu speichern. Die meinen es wäre verboten da nen Wert reinzustecken für den es keine Konstante gibt -- oder sind sich zumindest nicht sicher ob es erlaubt ist.

    U.a. daher die Überlegung eine Klasse dafür zu machen. Den Code der Klasse kann dann jeder lesen der wissen will was das soll, und dann sieht er was abgeht. Weiters wäre ein Klassentemplate eine zentrale Stelle wo man 1x dokumentieren kann was das alles soll. Bei enums müsste man es zu jedem enum dazuschreiben, bzw. auf eine zentrale Stelle verweisen - was bei "in source" Doku wieder schwierig wird.

    Wegen Interop mit anderen Sprachen die vom C++ Typsystem nichts wissen. Klar könnte man bei der Konvertierung Integer -> Handle direkt prüfen ob es den Handle-Wert gibt. Mag sein dass das in bestimmten Fällen sinnvoll wäre, in meinem ist es das allerdings nicht. Weil früher oder später sowieso das Handle "dereferenziert" wird, und dabei kommt dann spätestens raus wenn das Handle ungültig war - und das ist für mich ausreichend. Ein weiterer Check davor würde nur unnötig Rechenzeit verschlingen.

    Wozu also is_valid?

    Ach so, is_valid würde nur value != invalid_value zurückgeben, wobei invalid_value einfach Null ist. Analog zu NULL/nullptr bei Zeigern. Einfach ein Wert mit dem man explizit sagen kann "ich hab kein Handle".


  • Mod

    Genauso ist übrigens std::byte implementiert, als scoped enum.

    Wenn das Handle eine zentrale Rolle spielt, würde ich es durchaus als Klasse implementieren, einfach schon deswegen, weil man die Freiheit hat, den Wert bis zu einem gewissen Grad zu kapseln. Andernfalls ist das Enum ausreichend.


Anmelden zum Antworten