Tupel und Objektzeiger: geht das auch ohne SFINAE?


  • Mod

    Vllt. etwa so:

    template <typename E>
    struct TupleLeaf
    {
        E field;
    
        operator E&() noexcept {return field;}
        operator E const&() const noexcept {return field;}
    };
    
    template <typename... ElementsT>
    struct Tuple : TupleLeaf<ElementsT>...
    {
        template <typename ElementT>
        ElementT const& get(void) const
        {
            return *this;
        }
        ...
    };
    

    Man müsste natürlich einige Ausbesserungen machen, damit die Semantik in Sonderfällen auch stimmt.

    PS: Dein TupleLeaf schöpft EBO nicht aus, das ist dir klar?



  • @firefly: danke für die Korrektur, das war ein Tippfehler.

    @Arcoth: Danke, aber da zeigt sich leider, daß mein Beispiel zu stark vereinfacht war. Tatsächlich möchte ich auch den Fall unterstützen, daß der Argumenttyp irgendein Smartpointer ist:

    Tuple<std::shared_ptr<D>> t;
        t.get<std::shared_ptr<B>>(); // soll std::shared_ptr<B> zurückgeben
    

    Mit meiner SFINAE-Lösung geht das, mit den conversion operators leider nicht.

    Im Prinzip könnte ich ja den Objekttyp aus dem Smartpointer herausschälen, dann mit dem conversion operator das richtige Feld finden und dann den Zeiger wieder in einen Smartpointer verpacken. Mit einem intrusiven Smartpointer wäre das kein Problem, aber mit einem std::shared_ptr<> eben schon. Außerdem will ich vielleicht noch ein Flag setzen, das im Leaf liegt...

    Allgemein könnte man das Problem so formulieren: ich will nicht das Feld finden, sondern das TupleLeaf<> , das das Feld enthält. Mit einem conversion operator geht diese Information verloren.

    Arcoth schrieb:

    PS: Dein TupleLeaf schöpft EBO nicht aus, das ist dir klar?

    Ja, weil es ein Minimalbeispiel ist und keine fertige Implementation.


  • Mod

    Spricht etwas dagegen, std::tuple zu erweitern? Etwa:

    template <typename E, typename... T>
    const E& fuzzy_get(const std::tuple<T...>& t) noexcept {
        static_assert((false + ... + std::is_convertible_v<T*, E*>) >= 1, "no compatible element");
        static_assert((false + ... + std::is_convertible_v<T*, E*>) <= 1, "ambigous");
        return std::get<std::remove_pointer_t<std::common_type_t<std::conditional_t<std::is_convertible_v<T*, E*>, T*, std::nullptr_t>...>>>(t);
    }
    
    template <typename E, typename... T>
    void fuzzy_set(std::tuple<T...>& t, const E& v) noexcept {
        static_assert((false + ... + std::is_convertible_v<const E, T>) >= 1, "no compatible element");
        static_assert((false + ... + std::is_convertible_v<const E, T>) <= 1, "ambigous");
        std::get<std::remove_pointer_t<std::common_type_t<std::conditional_t<std::is_convertible_v<const E, T>, T*, std::nullptr_t>...>>>(t) = v;
    }
    

    Die Verwendung von TupleLeaf könnte man einfach durch ein Templatealias erreichen. Und ein fuzzy_leaf_get/set liesse sich ganz einfach analog schreiben.


  • Mod

    Arcoth schrieb:

    Dein TupleLeaf schöpft EBO nicht aus, das ist dir klar?

    Wenn alle Tupleelemnte state haben, ist das nicht besonders nützlich, oder übersehe ich etwas?

    Interessant wäre ja mal eine tuple-Klasse, die Elemente auch nach Alignment geordnet sortiert (unter Beibehaltung der üblichen Initialisierungsreihenfolge), so dass der Speicherverbrauch minimiert wird. Mir fällt als Weg dahin allerdings nur ein, reinterpret_cast über ein data-Array zu verwenden, womit constexpr oder Trivialität des Tuples (für entsprechende Parameter) leider aufgegeben werden müssen.



  • camper schrieb:

    Spricht etwas dagegen, std::tuple zu erweitern?

    Ja. Ich will eigentlich nicht mein eigenes Tupel schreiben, sondern benutze das nur als Beispiel, um mein Lookup-Problem zu erklären. Ich brauche u.a. explizite Kontrolle über Konstruktion und Destruktion der Felder und kann deren Verwaltung deshalb nicht an std::tuple<> delegieren.

    camper schrieb:

    template <typename E, typename... T>
    const E& fuzzy_get(const std::tuple<T...>& t) noexcept {
        static_assert((false + ... + std::is_convertible_v<T*, E*>) >= 1, "no compatible element");
        static_assert((false + ... + std::is_convertible_v<T*, E*>) <= 1, "ambigous");
        return std::get<std::remove_pointer_t<std::common_type_t<std::is_convertible_v<T*, E*>, T*, std::nullptr_t>>>>(t); // <---
    }
    

    Ich sehe die Intention, aber den Code verstehe ich nicht. Ich nehme an, da fehlt irgendwo eine pack expansion? Und wolltest du std::is_convertible<> schreiben statt std::is_convertible_v<> ? Sonst verstehe ich nicht, wieso std::common_type_t<> ein bool -Argument erwarten sollte. Und überhaupt hat std::common_type<> doch einfach kein type -Member, wenn es keinen common type gibt, so daß std::common_type_t<> für alle T bis auf eines nicht wohlgeformt ist, und somit auch der ganze Ausdruck nicht, oder? Falls der std::nullptr_t als kleinster gemeinsamer Nenner gemeint war, um immer einen wohlgeformten Ausdruck zu erhalten: das geht leider so nicht, weil ich ja auch Werttypen im Tupel haben kann.

    camper schrieb:

    Die Verwendung von TupleLeaf könnte man einfach durch ein Templatealias erreichen.

    Eher nicht, weil das ja ein Implementationsdetail von std::tuple<> ist.


  • Mod

    audacia schrieb:

    camper schrieb:

    template <typename E, typename... T>
    const E& fuzzy_get(const std::tuple<T...>& t) noexcept {
        static_assert((false + ... + std::is_convertible_v<T*, E*>) >= 1, "no compatible element");
        static_assert((false + ... + std::is_convertible_v<T*, E*>) <= 1, "ambigous");
        return std::get<std::remove_pointer_t<std::common_type_t<std::is_convertible_v<T*, E*>, T*, std::nullptr_t>>>>(t); // <---
    }
    

    Ich sehe die Intention, aber den Code verstehe ich nicht. Ich nehme an, da fehlt irgendwo eine pack expansion? Und wolltest du std::is_convertible<> schreiben statt std::is_convertible_v<> ? Sonst verstehe ich nicht, wieso std::common_type_t<> ein bool -Argument erwarten sollte. Und überhaupt hat std::common_type<> doch einfach kein type -Member, wenn es keinen common type gibt, so daß std::common_type_t<> für alle T bis auf eines nicht wohlgeformt ist, und somit auch der ganze Ausdruck nicht, oder? Falls der std::nullptr_t als kleinster gemeinsamer Nenner gemeint war, um immer einen wohlgeformten Ausdruck zu erhalten: das geht leider so nicht, weil ich ja auch Werttypen im Tupel haben kann.

    Ja, da fehlte ein conditional_t. Habe es oben korrigiert (ist aber immer noch ungetested).

    audacia schrieb:

    camper schrieb:

    Die Verwendung von TupleLeaf könnte man einfach durch ein Templatealias erreichen.

    Eher nicht, weil das ja ein Implementationsdetail von std::tuple<> ist.

    Ich dachte an so etwas:

    template <typename ...T>
    using Tuple = std::tuple<TupleLeaf<T>...>;
    

  • Mod

    camper schrieb:

    Arcoth schrieb:

    Dein TupleLeaf schöpft EBO nicht aus, das ist dir klar?

    Wenn alle Tupleelemnte state haben, ist das nicht besonders nützlich, oder übersehe ich etwas?

    Aus welchem Finger hast Du diese Prämisse gezogen?

    camper schrieb:

    Interessant wäre ja mal eine tuple-Klasse, die Elemente auch nach Alignment geordnet sortiert (unter Beibehaltung der üblichen Initialisierungsreihenfolge), so dass der Speicherverbrauch minimiert wird. Mir fällt als Weg dahin allerdings nur ein, reinterpret_cast über ein data-Array zu verwenden, womit constexpr oder Trivialität des Tuples (für entsprechende Parameter) leider aufgegeben werden müssen.

    Dass Basisklassen nicht ihrer Ausrichtung nach im Speicher geordnet liegen, ist ein QoI issue, nicht? Das Problem für uns als TMP-Hacker besteht darin, dass unter dem aktuellen Regelwerk die Initialisierungsreihenfolge mit der Auslegung im Speicher verknüpft ist. Ich bin allerdings noch nicht ganz überzeugt, dass es nicht möglich ist.

    Die Reihenfolge der Initialisierung ist aber AFAICS selten ausschlaggebend---schließlich können wir auf andere Elemente des Tupels doch nicht zugreifen, während es konstruiert wird. Und ohne Beachtung dieser Reihenfolge können wir tatsächlich einen Weg finden:

    #include <iostream>
    #include <type_traits>
    #include <utility>
    
    template <typename...>
    struct pack {
    	using type = pack;
    };
    
    template <std::size_t X>
    using SizeT = std::integral_constant<std::size_t, X>;
    
    template <std::size_t I, typename=std::make_index_sequence<I>> struct SelectN;
    template <std::size_t I, std::size_t... Is>
    struct SelectN<I, std::index_sequence<Is...>> {
    	template <std::size_t> struct accept_all {template <typename T> constexpr accept_all(T&&) noexcept {}};
    	template <typename T, typename... Rest>
    	static constexpr T&& get(accept_all<Is>..., T&& t, Rest&&...) noexcept {
    		return std::forward<T>(t);
    	}
    };
    
    struct Ignore {};
    
    template <typename Pred, typename PrevPack, typename AltPrevPack, typename Pack,
              typename CurMin, std::size_t CurI, typename=void>
    struct FindMinAndExtractImpl;
    
    template <typename Pred, typename... Prevs, typename... AltPrevs, typename T, typename... Ts,
              typename CurMin, std::size_t CurI>
    struct FindMinAndExtractImpl<Pred, pack<Prevs...>, pack<AltPrevs...>, pack<T, Ts...>, CurMin, CurI,
                                 std::enable_if_t<!std::is_same<T, Ignore>{} && Pred::template eval<T, CurMin>>>
     : FindMinAndExtractImpl<Pred, pack<AltPrevs..., Ignore>, pack<AltPrevs..., T>, pack<Ts...>,
                              T, sizeof...(AltPrevs)> {};
    
    template <typename Pred, typename... Prevs, typename... AltPrevs, typename T, typename... Ts,
              typename CurMin, std::size_t CurI>
    struct FindMinAndExtractImpl<Pred, pack<Prevs...>, pack<AltPrevs...>, pack<T, Ts...>, CurMin, CurI,
                                 std::enable_if_t<std::is_same<Ignore, T>{} || !Pred::template eval<T, CurMin>>>
     : FindMinAndExtractImpl<Pred, pack<Prevs..., T>, pack<AltPrevs..., T>, pack<Ts...>, CurMin, CurI> {};
    
    template <typename Pred, typename PrevPack, typename AltPack,
              typename CurMin, std::size_t CurI>
    struct FindMinAndExtractImpl<Pred, PrevPack, AltPack, pack<>, CurMin, CurI> {
    	using type = CurMin;
    	using new_pack = PrevPack;
    	static constexpr auto index = CurI;
    };
    
    template <typename Pred, typename Pack>
    struct FindMinAndExtract;
    template <typename Pred, typename T, typename... Ts>
    struct FindMinAndExtract<Pred, pack<T, Ts...>> : FindMinAndExtractImpl<Pred, pack<Ignore>, pack<T>, pack<Ts...>, T, 0> {};
    template <typename Pred, typename... Ts>
    struct FindMinAndExtract<Pred, pack<Ignore, Ts...>> : FindMinAndExtract<Pred, pack<Ts...>> {
    	static constexpr auto index = FindMinAndExtract<Pred, pack<Ts...>>::index+1;
    };
    
    namespace Test1 {
    	struct SizeofComp {
    		template <typename A, typename B>
    		static constexpr bool eval = sizeof(A) < sizeof(B);
    	};
    	static_assert(std::is_same<typename FindMinAndExtract<SizeofComp, pack<int, long, char>>::type, char>{});
    	static_assert(FindMinAndExtract<SizeofComp, pack<int, long, char>>::index == 2);
    	static_assert(std::is_same<typename FindMinAndExtract<SizeofComp, pack<int, long, char>>::new_pack,
    	                           pack<int, long, Ignore>>{});
    
    	static_assert(std::is_same<typename FindMinAndExtract<SizeofComp, pack<int, long, Ignore>>::type, int>{});
    	static_assert(FindMinAndExtract<SizeofComp, pack<int, long, Ignore>>::index == 0);
    	static_assert(std::is_same<typename FindMinAndExtract<SizeofComp, pack<int, long, Ignore>>::new_pack,
    	                           pack<Ignore, long, Ignore>>{});
    }
    
    struct AlignComp {
    	template <typename L, typename R>
    	static constexpr bool eval = alignof(L) < alignof(R);
    };
    
    template <std::size_t I, std::size_t J, std::size_t Max, typename T, typename >
    struct TupleLeaf;
    template <std::size_t I, std::size_t J, std::size_t Max, typename T, typename... Ts>
    struct TupleLeaf<I, J, Max, T, pack<Ts...>>
     : TupleLeaf<FindMinAndExtract<AlignComp, pack<Ts...>>::index, J+1, Max,
                 typename FindMinAndExtract<AlignComp, pack<Ts...>>::type,
                 typename FindMinAndExtract<AlignComp, pack<Ts...>>::new_pack> {
    	T value;
    	template <typename X, typename... Xs>
    	constexpr TupleLeaf(X&& x, Xs&&... xs) :
    		TupleLeaf<FindMinAndExtract<AlignComp, pack<Ts...>>::index, J+1, Max,
    		          typename FindMinAndExtract<AlignComp, pack<Ts...>>::type,
    		          typename FindMinAndExtract<AlignComp, pack<Ts...>>::new_pack>(std::forward<Xs>(xs)...),
    		value(std::forward<X>(x)) {}
    	constexpr TupleLeaf() = default;
    };
    template <std::size_t I, std::size_t J, typename T, typename... Ts>
    struct TupleLeaf<I, J, J, T, pack<Ts...>> {
    	T value;
    	template <typename X>
    	constexpr TupleLeaf(X&& x) : value(std::forward<X>(x)) {}
    	constexpr TupleLeaf() = default;
    };
    
    template <typename, typename> struct TupleImpl;
    template <typename... Ts, std::size_t... Is>
    struct TupleImpl<pack<Ts...>, std::index_sequence<Is...>>
     : TupleLeaf<FindMinAndExtract<AlignComp, pack<Ts...>>::index, 0, sizeof...(Is)-1,
                 typename FindMinAndExtract<AlignComp, pack<Ts...>>::type,
                 typename FindMinAndExtract<AlignComp, pack<Ts...>>::new_pack>
    {
    private:
    	using base = TupleLeaf<FindMinAndExtract<AlignComp, pack<Ts...>>::index, 0, sizeof...(Is)-1,
    	             typename FindMinAndExtract<AlignComp, pack<Ts...>>::type,
    	             typename FindMinAndExtract<AlignComp, pack<Ts...>>::new_pack>;
    
    	template <std::size_t Given, std::size_t Deduced, typename Type, typename Rest>
    	static SizeT<Deduced> getIndexOf(TupleLeaf<Deduced, Given, sizeof...(Is)-1, Type, Rest>&&);
    
    	static constexpr std::size_t indexMapping[sizeof...(Is)] {
    		decltype(getIndexOf<Is>(std::declval<base>())){}...};
    
    public:
    	template <typename... Xs>
    	constexpr TupleImpl(Xs&&... xs) : base(SelectN<indexMapping[Is]>::get(std::forward<Xs>(xs)...)...) {}
    
    	constexpr TupleImpl() = default;
    
    };
    template <>
    struct TupleImpl<pack<>, std::index_sequence<>> {};
    
    template <typename... Ts>
    using Tuple = TupleImpl<pack<Ts...>, std::index_sequence_for<Ts...>>;
    
    template <std::size_t I, std::size_t J, std::size_t Max, typename T, typename Rest>
    constexpr decltype(auto) get(TupleLeaf<I, J, Max, T, Rest> const& tup) noexcept {
    	return tup.value;
    }
    template <std::size_t I, std::size_t J, std::size_t Max, typename T, typename Rest>
    constexpr decltype(auto) get(TupleLeaf<I, J, Max, T, Rest>& tup) noexcept {
    	return tup.value;
    }
    template <std::size_t I, std::size_t J, std::size_t Max, typename T, typename Rest>
    constexpr decltype(auto) get(TupleLeaf<I, J, Max, T, Rest>&& tup) noexcept {
    	return std::move(tup).value;
    }
    
    #include <tuple>
    int main() {
    	constexpr Tuple<int, long, char> t{1, 2, '3'};
    	std::cout << get<0>(t) << '\n';
    	std::cout << get<1>(t) << '\n';
    	std::cout << get<2>(t) << '\n';
    	std::cout << sizeof t << " vs. " << sizeof(std::tuple<int, long, char>);
    }
    

    Wir sparen ein Drittel des Speichers im Vergleich zu der Standardbibliotheks-Implementierung. 😋


  • Mod

    Arcoth schrieb:

    camper schrieb:

    Arcoth schrieb:

    Dein TupleLeaf schöpft EBO nicht aus, das ist dir klar?

    Wenn alle Tupleelemnte state haben, ist das nicht besonders nützlich, oder übersehe ich etwas?

    Aus welchem Finger hast Du diese Prämisse gezogen?

    Bring doch einfach ein (einigermaßen sinnvolles) Beispiel, um mich vom Gegenteil zu überzeugen.

    Arcoth schrieb:

    Dass Basisklassen nicht ihrer Ausrichtung nach im Speicher geordnet liegen, ist ein QoI issue, nicht?

    Im Prinzip ja, dummerweise aber ein Aspekt dessen Änderung extreme ABI-Breakage verursachen würde, und deshalb außerordentlicher Rechtfertigung bedürfte. Tuple-Layout dürfte kaum ein ausreichender Grund sein.

    Arcoth schrieb:

    Das Problem für uns als TMP-Hacker besteht darin, dass unter dem aktuellen Regelwerk die Initialisierungsreihenfolge mit der Auslegung im Speicher verknüpft ist. Ich bin allerdings noch nicht ganz überzeugt, dass es nicht möglich ist.

    Wenn du eine Idee hast, bin ich interessiert.

    Arcoth schrieb:

    Die Reihenfolge der Initialisierung ist aber AFAICS selten ausschlaggebend---schließlich können wir auf andere Elemente des Tupels doch nicht zugreifen, während es konstruiert wird.

    Richtig. Gegenseitiger Zugriff während der Konstruktion kann im Prinzip nur entstehen, wenn die Konstruktion der Elemente globalen State beeinflusst. z.B. könnte jemand auf die Idee kommen, GUI-Elemente in ein Tuple zu packen. Umgekehrt ist die Optimierung immer dann sicher möglich, wenn nicht mehr als ein Tupleelement nicht-trivial konstruierbar ist.


  • Mod

    camper schrieb:

    Arcoth schrieb:

    camper schrieb:

    Arcoth schrieb:

    Dein TupleLeaf schöpft EBO nicht aus, das ist dir klar?

    Wenn alle Tupleelemnte state haben, ist das nicht besonders nützlich, oder übersehe ich etwas?

    Aus welchem Finger hast Du diese Prämisse gezogen?

    Bring doch einfach ein (einigermaßen sinnvolles) Beispiel, um mich vom Gegenteil zu überzeugen.

    Es ist doch ein allgemein bekannter Trick, Allokatoren oder sonstige Objekte, von denen wir erwarten, dass ihre Klasse leer ist, zusammen mit mindestens einer anderen Klasse in tuple zu speichern, damit nur der Speicherverbrauch der anderen Klasse greift. Oder ist das schon aus der Mode?


  • Mod

    Arcoth schrieb:

    camper schrieb:

    Arcoth schrieb:

    camper schrieb:

    Arcoth schrieb:

    Dein TupleLeaf schöpft EBO nicht aus, das ist dir klar?

    Wenn alle Tupleelemnte state haben, ist das nicht besonders nützlich, oder übersehe ich etwas?

    Aus welchem Finger hast Du diese Prämisse gezogen?

    Bring doch einfach ein (einigermaßen sinnvolles) Beispiel, um mich vom Gegenteil zu überzeugen.

    Es ist doch ein allgemein bekannter Trick, Allokatoren oder sonstige Objekte, von denen wir erwarten, dass ihre Klasse leer ist, zusammen mit mindestens einer anderen Klasse in tuple zu speichern, damit nur der Speicherverbrauch der anderen Klasse greift. Oder ist das schon aus der Mode?

    Richtig. Das war aber auch nicht die Frage. Die Frage war, ob man solche Objekte, die potentiell leer sind, in ein Tupel packen würde in einem Kontext, bei dem die Größe des Tupels eine Rolle spielt (Funktionsargumente/-rückgaben also nicht). Macht std::tuple irgendwelche Aussagen dazu?


  • Mod

    camper schrieb:

    Arcoth schrieb:

    camper schrieb:

    Arcoth schrieb:

    camper schrieb:

    Arcoth schrieb:

    Dein TupleLeaf schöpft EBO nicht aus, das ist dir klar?

    Wenn alle Tupleelemnte state haben, ist das nicht besonders nützlich, oder übersehe ich etwas?

    Aus welchem Finger hast Du diese Prämisse gezogen?

    Bring doch einfach ein (einigermaßen sinnvolles) Beispiel, um mich vom Gegenteil zu überzeugen.

    Es ist doch ein allgemein bekannter Trick, Allokatoren oder sonstige Objekte, von denen wir erwarten, dass ihre Klasse leer ist, zusammen mit mindestens einer anderen Klasse in tuple zu speichern, damit nur der Speicherverbrauch der anderen Klasse greift. Oder ist das schon aus der Mode?

    Richtig. Das war aber auch nicht die Frage. Die Frage war, ob man solche Objekte, die potentiell leer sind, in ein Tupel packen würde in einem Kontext, bei dem die Größe des Tupels eine Rolle spielt (Funktionsargumente/-rückgaben also nicht).

    Innerhalb einer String Klasse könnte dieser Trick legitim angewendet werden. Und es ist durchaus vorstellbar, viele Strings zu erzeugen. 😕


Anmelden zum Antworten