Container für polymorphe Objekte gleicher Größe



  • Nichts besonderes... Hab an sowas wie einen std::vector<Base> gedacht. Wobei keine Ableitung mehr Daten enthalten sollte, als Base, sonst darf der insert nicht kompilieren.



  • PolyCollection schrieb:

    Wobei keine Ableitung mehr Daten enthalten sollte, als Base, sonst darf der insert nicht kompilieren.

    Aber ein tatsächlicher vector<Base> und slicing kommen nicht in Frage?



  • PolyCollection schrieb:

    Nichts besonderes... Hab an sowas wie einen std::vector<Base> gedacht. Wobei keine Ableitung mehr Daten enthalten sollte, als Base, sonst darf der insert nicht kompilieren.

    Ein vector<polymorph<Base>> ist relativ einfach zu implementieren, wenn Base (und alle Ableitungen) ein nothrow-Move hat. Andernfalls wird die Implementierung von Move-Zuweisung so kompliziert wie in std::any.



  • Beispiel

    #include <cassert>
    #include <new>
    #include <unordered_map>
    #include <utility>
    #include <typeinfo>
    #include <typeindex>
    
    struct copy_move_info_t {
        void* (*copy_construct)(void* dst, const void* src);
        void* (*move_construct)(void* dst, void* src);
        void* (*copy_assign)(void* dst, const void* src);
        void* (*move_assign)(void* dst, void* src);
        void (*swap)(void* x, void* y);
        bool is_nothrow_copy_constructible;
    };
    inline std::unordered_map<std::type_index, const copy_move_info_t*> cm_infos{};
    
    template <typename T>
    void* copy_construct(void* dst, const void* src) {
        return new(dst) T{*std::launder(static_cast<const T*>(src))};
    }
    template <typename T>
    void* move_construct(void* dst, void* src) {
       return new(dst) T{std::move(*std::launder(static_cast<T*>(src)))};
    }
    template <typename T>
    void* copy_assign(void* dst, const void* src) {
        *std::launder(static_cast<T*>(dst)) = *std::launder(static_cast<const T*>(src));
        return dst;
    }
    template <typename T>
    void* move_assign(void* dst, void* src) {
        *std::launder(static_cast<T*>(dst)) = std::move(*std::launder(static_cast<T*>(src)));
        return dst;
    }
    template <typename T>
    void swap_(void* x, void* y) {
        using std::swap;
        swap(*std::launder(static_cast<T*>(x)), *std::launder(static_cast<T*>(y)));
    }
    template <typename T>
    const copy_move_info_t copy_move_info{ copy_construct<T>, move_construct<T>, copy_assign<T>, move_assign<T>, swap_<T>, std::is_nothrow_copy_constructible_v<T> };
    
    template <typename T>
    class polymorph {
        static_assert(std::is_polymorphic_v<T>);
        static_assert(std::has_virtual_destructor_v<T>);
    public:
        template <typename U>
        polymorph(U&& src) noexcept(noexcept(std::decay_t<U>{std::forward<U>(src)})) {
            using src_type = std::decay_t<U>;
            static_assert(std::is_convertible_v<src_type*, T*>);
            static_assert(sizeof(src_type) == sizeof(T));
            static_assert(alignof(src_type) == alignof(T));
            static_assert(std::is_nothrow_move_constructible_v<src_type>);
            static_assert(std::is_nothrow_move_assignable_v<src_type>);
            static_assert(std::is_nothrow_destructible_v<src_type>);
            static_assert(std::is_nothrow_swappable_v<src_type>);
    
            cm_infos.try_emplace(typeid(U), &copy_move_info<src_type>);
            auto p = new(static_cast<void*>(data)) src_type{std::forward<U>(src)};
            assert(p == std::addressof(get()));
        }
        polymorph(const polymorph& other) {
            auto p = cm_infos.at(typeid(other.get()))->copy_construct(data, other.data);
            assert(p == std::addressof(get()));
        }
        polymorph(polymorph&& other) noexcept {
            auto p = cm_infos.at(typeid(other.get()))->move_construct(data, other.data);
            assert(p == std::addressof(get()));
        }
        polymorph& operator=(const polymorph& rhs) {
            auto&& info = cm_infos.at(typeid(rhs.get()));
            if (typeid(get()) == typeid(rhs.get()))
                info->copy_assign(data, rhs.data);
            else if (info->is_nothrow_copy_constructible) {
                get().~T();
                info->copy_construct(data, rhs.data);
            } else {
                alignas(T) char tmp[sizeof(T)];
                info->copy_construct(tmp, rhs.data);
                get().~T();
                auto p = info->move_construct(data, tmp);
                assert(p == std::addressof(get()));
                std::launder(reinterpret_cast<T*>(tmp))->~T();
            }
            return *this;
        }
        polymorph& operator=(polymorph&& rhs) noexcept {
            auto&& info = cm_infos.at(typeid(rhs.get()));
            if (typeid(get()) == typeid(rhs.get()))
                info->move_assign(data, rhs.data);
            else {
                get().~T();
                auto p = info->move_construct(data, rhs.data);
                assert(p == std::addressof(get()));
            }
            return *this;
        }
        ~polymorph() noexcept {
            get().~T();
        }
    
        void swap(polymorph& other) noexcept {
            auto&& info = cm_infos.at(typeid(get()));
            if (typeid(get()) == typeid(other.get()))
                info->swap(data, other.data);
            else {
                auto&& info_other = cm_infos.at(typeid(other.get()));
                alignas(T) char tmp[sizeof(T)];
                info_other->move_construct(tmp, other.data);
                other.get().~T();
                info->move_construct(other.data, data);
                get().~T();
                info_other->move_construct(data, tmp);
                std::launder(reinterpret_cast<T*>(tmp))->~T();
            }
        }
        void swap(polymorph&& other) noexcept {
            swap(other);
        }
    
        T& get() noexcept { return *std::launder(reinterpret_cast<T*>(data)); }
        const T& get() const noexcept { return *std::launder(reinterpret_cast<const T*>(data)); }
    private:
        alignas(T) char data[sizeof(T)];
    };
    template <typename T> void swap(polymorph<T>& x, polymorph<T>& y) noexcept { return x.swap(y); }
    template <typename T> void swap(polymorph<T>&& x, polymorph<T>& y) noexcept { return x.swap(y); }
    template <typename T> void swap(polymorph<T>& x, polymorph<T>&& y) noexcept { return x.swap(y); }
    template <typename T> void swap(polymorph<T>&& x, polymorph<T>&& y) noexcept { return x.swap(y); }
    ////////////////////////////////////////////////////////////////////////////////////////////
    #include <iostream>
    #include <vector>
    struct base {
        virtual void foo() = 0;
        virtual ~base() {}
    };
    template <int I>
    struct derived : base
    {
        derived() { std::cout << (void*)this << " derived<" << I << ">::derived()\n"; }
        derived(const derived&) { std::cout << (void*)this << " derived<" << I << ">::derived(const derived&)\n"; }
        derived(derived&&) noexcept { std::cout << (void*)this << " derived<" << I << ">::derived(derived&&)\n"; }
        derived& operator=(const derived&) { std::cout << (void*)this << " derived<" << I << ">& derived<" << I << ">::operator=(const derived&)\n"; return *this; }
        derived& operator=(derived&&) noexcept { std::cout << (void*)this << " derived<" << I << ">& derived<" << I << ">::operator=(derived&&)\n"; return *this; }
        ~derived() noexcept { std::cout << (void*)this << " derived<" << I << ">::~derived()\n"; }
        virtual void foo() override { std::cout << (void*)this << " derived<" << I << ">::foo()\n"; }
    };
    
    int main() {
        std::vector<polymorph<base>> v;
        std::cout << "v.emplace_back(derived<0>{});\n";
        v.emplace_back(derived<0>{});
        std::cout << "v.push_back(derived<1>{});\n";
        v.push_back(derived<1>{});
        std::cout << "v.emplace_back(derived<2>{});\n";
        v.emplace_back(derived<2>{});
        std::cout << "***\n";
        for ( auto& x : v )
            x.get().foo();
        std::cout << "v[0] = std::move(v[0]);\n";
        v[0] = std::move(v[0]);
        std::cout << "v[0] = std::move(v[2]);\n";
        v[0] = std::move(v[2]);
        std::cout << "v[0] = v[0];\n";
        v[0] = v[0];
        std::cout << "v[0] = v[1];\n";
        v[0] = v[1];
        std::cout << "swap(v[0],v[0]);\n";
        swap(v[0],v[0]);
        std::cout << "swap(v[0],v[2]);\n";
        swap(v[0],v[2]);
        std::cout << "return 0;\n";
        std::cout << "***\n";
        for ( auto& x : v )
            x.get().foo();
    }
    


  • Das hast du jetzt einfach in paar Minuten geschrieben? Hab mir schon gedacht, dass es so ähnlich ausschauen würde, aber wollte was fertiges, bevor ich das selber anfange 🙂 Aber das schaut gut aus, danke!

    - auf std::launder kann ich in C++14 einfach verzichten?
    - Gibts einen Grund, dass du kein aligned_storage verwendest (ich hätt sogar vorausgesetzt, dass die Typen gleich aligned sein müssen).



  • PolyCollection schrieb:

    - auf std::launder kann ich in C++14 einfach verzichten?

    Im Prinzip ja. Besser wäre evtl. einfach ein eigenes launder-Template zu benutzen, dass das Funktionsargument zurückgibt. Dann musst du bei einer späteren Umstellung auf einen neuen Standard nicht den ganzen Code manuell durcharbeiten, sondern passt einfach das Template an oder führst ein automatische search&replace durch.

    PolyCollection schrieb:

    - Gibts einen Grund, dass du kein aligned_storage verwendest (ich hätt sogar vorausgesetzt, dass die Typen gleich aligned sein müssen).

    Dafür ist alignas da. aligned_storage existiert ja nur, weil es vor alignas/alignof eingeführt wurde.



  • Auch wenn es nur Beispielcode ist, globale Variablen, noch dazu mit nicht synchronisiertem Zugriff, finde ich schrecklich.



  • hustbaer schrieb:

    Auch wenn es nur Beispielcode ist, globale Variablen, noch dazu mit nicht synchronisiertem Zugriff, finde ich schrecklich.

    Warum ist die Map eigentlich im namespace scope? AFAICS könnte das auch ein inline static member sein?

    PolyCollection schrieb:

    - auf std::launder kann ich in C++14 einfach verzichten?

    Das Objektmodell ist momentan kaputt*, und sobald es durch P0593 und Konsorten gefixt wird, ist der Code auch ohne launder gültig, weil das char Array bereits ein Objekt des zugehörigen Typen beinhaltet. Ergo, du kannst auf launder in diesem Kontext sowieso verzichten. launder wird dort gebraucht, wo wir bspw. ein neues Objekt in den Speicher eines alten setzen, und auf einen konstanten oder Referenzmember zugreifen. Kurz gesagt, aber natürlich nicht formell, wir möchten gewisse Datenfluss-Analysen, speziell pointer analysis, unterbinden.

    ^* Es ist sowohl praktisch kaputt (wir können keine Container implementieren), als auch bzgl. Abwärtskompatibilität. Und Code als undefiniert zu deklarieren, der in C++14 wohldefiniert war, ist einfach völlig lachhaft und wird nicht von Implementierungen berücksichtigt werden, bis der Staub sich legt.^



  • Arcoth schrieb:

    hustbaer schrieb:

    Auch wenn es nur Beispielcode ist, globale Variablen, noch dazu mit nicht synchronisiertem Zugriff, finde ich schrecklich.

    Warum ist die Map eigentlich im namespace scope? AFAICS könnte das auch ein inline static member sein?

    Das ist ist im globalen Namensraum, damit sich jemand daran stört und den Code nicht einfach so ohne Nachdenken übernimmt.
    Synchronisation wurde angesprochen. Die ist dann und nur dann erforderlich, falls in mehreren Threads mit polymorph-Objekten gearbeitet wird und nicht alle Typen bereits zuvor registriert werden können.

    Und dann gibt es ja zwei offensichtliche Implementations-Alternativen, die bedacht werden sollten:
    1. man speichert einen entsprechenden Infozeiger mit jedem Objekt, Nachteil ist größerer Speicheraufwand, der bei großen Objekten durchaus irrelevant sein kann, oder
    2. man verlangt, dass Base ein entsprechendes virtuelles Interface besitzt, dass die nötigen Copy-/Move-operationen bereitstellt.



  • Variante mit virtuellem Interface (kombiniert beide alterntive Varianten oben) und C++14 kompatibel. Diese Implementation muss mit diesen Beschränkungen zu leben:
    1. die Typen der gespeicherten Objekten dürfen nicht final sein (sowieso meistens Unfug),
    2. die gespeicherten Objekte sind Basisklassensubobjekte, Code der typeid verwendet muss angepasst werden.
    3. falls die Basisklasse nicht von holder_base abgeleitet ist, wird ein zusätzlicher Zeiger pro Objekt gebraucht (zusätzlicher vtbl-Pointer wg. Mehrfachvererbung) - zudem evtl. langsamer, wegen cross-cast.

    #include <cassert>
    #include <new>
    #include <utility>
    #include <tuple>
    #include <typeinfo>
    
    class holder_base {
    public:
        virtual void holder_copy_construct(void* dst) const { assert(false); };
        virtual void holder_move_construct(void* dst) noexcept { assert(false); };
        virtual void holder_copy_assign(holder_base& dst) const { assert(false); };
        virtual void holder_move_assign(holder_base& dst) noexcept { assert(false); };
        virtual void holder_swap(holder_base& other) noexcept { assert(false); };
        virtual ~holder_base() noexcept {};
    };
    
    template <typename T, typename U, std::enable_if_t<
           std::is_convertible<int std::decay_t<U>::*, int std::decay_t<T>::*>{}
        || std::is_convertible<std::remove_reference_t<U>*, std::remove_reference_t<T>*>{}, int> = 0>
    T fast_cast(U&& v) noexcept { return static_cast<T>(std::forward<U>(v)); }
    template <typename T, typename U, std::enable_if_t<
           !std::is_convertible<int std::decay_t<U>::*, int std::decay_t<T>::*>{}
        && !std::is_convertible<std::remove_reference_t<U>*, std::remove_reference_t<T>*>{}, int> = 0>
    T fast_cast(U&& v) noexcept { return dynamic_cast<T>(std::forward<U>(v)); }
    
    template <typename T, bool B = std::is_base_of<holder_base, T>{}>
    struct holder_helper : T {
        template <typename U>
        holder_helper(U&& v) : T{std::forward<U>(v)} {}
    };
    template <typename T>
    struct holder_helper<T, false> : holder_base, T {
        template <typename U>
        holder_helper(U&& v) : holder_base{}, T{std::forward<U>(v)} {}
    };
    
    template <typename T>
    class holder : public holder_helper<T> {
    public:
        template <typename U>
        explicit holder(U&& v) noexcept(noexcept(std::decay_t<U>{std::forward<U>(v)}))
        : holder_helper<T>{std::forward<U>(v)}
        {}
    private:
        virtual void holder_copy_construct(void* dst) const final override {
            new(dst) holder{static_cast<const T&>(*this)};
        }
        virtual void holder_move_construct(void* dst) noexcept final override {
            new(dst) holder{static_cast<T&&>(*this)};
        }
        template <typename U, std::enable_if_t<std::is_nothrow_copy_constructible<U>{}, int> = 0>
        static void holder_copy_assign(holder_base& dst, const U& src) noexcept {
            dst.~holder_base();
            src.holder_copy_construct(&dst);
        }
        template <typename U, std::enable_if_t<!std::is_nothrow_copy_constructible<U>{}, int> = 0>
        static void holder_copy_assign(holder_base& dst, const U& src) {
            T tmp{static_cast<const T&>(src)};
            dst.~holder_base();
            new(static_cast<void*>(&dst)) holder{std::move(tmp)};
        }
        virtual void holder_copy_assign(holder_base& dst) const final override {
            if (typeid(dst) == typeid(holder))
                static_cast<T&>(fast_cast<holder&>(dst)) = static_cast<const T&>(*this);
            else
                holder_copy_assign(dst, *this);
        }
        virtual void holder_move_assign(holder_base& dst) noexcept final override {
            if (typeid(dst) == typeid(holder))
                static_cast<T&>(fast_cast<holder&>(dst)) = static_cast<T&&>(*this);
            else {
                dst.~holder_base();
                holder_move_construct(&dst);
            }
        }
        virtual void holder_swap(holder_base& other) noexcept final override {
            if (typeid(other) == typeid(holder)) {
                using std::swap;
                swap(static_cast<T&>(fast_cast<holder&>(other)), static_cast<T&>(*this));
            } else {
                T tmp{static_cast<T&&>(*this)};
                this->~holder();
                other.holder_move_construct(this);
                other.~holder_base();
                new(static_cast<void*>(&other)) holder{std::move(tmp)};
            }
        }
    };
    
    template <typename T, std::size_t max_size = sizeof(holder<T>), std::size_t alignment = alignof(holder<T>)>
    class polymorph {
        static_assert(std::is_polymorphic<T>{}, "Base must be polymorphic");
        static_assert(std::has_virtual_destructor<T>{}, "Base must have virtual destructor");
        static_assert(max_size >= sizeof(holder<T>), "specified size is unsufficient");
        static_assert(alignment >= alignof(holder<T>), "specified alignment not strict enough");
        static_assert(max_size % alignment == 0, "specified size and alignment inconsistent");
    public:
        template <typename U>
        polymorph(U&& src) noexcept(noexcept(std::decay_t<U>{std::forward<U>(src)})) {
            using src_type = std::decay_t<U>;
            static_assert(std::is_convertible<src_type*, T*>{}, "Base must be unambigous public base of Argument");
            assert(typeid(src_type) == typeid(src)); // argument needs to be of the most derived type
            static_assert(!std::is_final<src_type>{}, "argument type must not be final");
            static_assert(sizeof(holder<src_type>) <= max_size, "argument size too big");
            static_assert(alignof(holder<src_type>) <= alignment, "argument alignment too strict");
            static_assert(std::is_nothrow_move_constructible<src_type>{}, "argument must be nothrow move constructible");
            static_assert(std::is_nothrow_move_assignable<src_type>{}, "argument must be nothrow move assignable");
            static_assert(std::is_nothrow_destructible<src_type>{}, "argument must be nothrow destructible");
            // static_assert(std::is_nothrow_swappable_v<src_type>);
    
            auto p = new(static_cast<void*>(data)) holder<src_type>{std::forward<U>(src)};
            assert(static_cast<holder_base*>(p) == static_cast<void*>(p)); // sane layout
        }
        polymorph(const polymorph& other)              { other.holder_get().holder_copy_construct(data); }
        polymorph(polymorph&& other) noexcept          { other.holder_get().holder_move_construct(data); }
        polymorph& operator=(const polymorph& rhs)     { rhs.holder_get().holder_copy_assign(holder_get()); return *this; }
        polymorph& operator=(polymorph&& rhs) noexcept { rhs.holder_get().holder_move_assign(holder_get()); return *this; }
        ~polymorph() noexcept                          { this->holder_get().~holder_base(); }
    
        void swap(polymorph& other) noexcept           { holder_get().holder_swap(other.holder_get()); }
        void swap(polymorph&& other) noexcept          { swap(other); }
    
        T& get() noexcept                              { return fast_cast<T&>(holder_get()); }
        const T& get() const noexcept                  { return fast_cast<const T&>(holder_get()); }
    private:
        holder_base& holder_get() noexcept             { return reinterpret_cast<holder_base&>(data); }
        const holder_base& holder_get() const noexcept { return reinterpret_cast<const holder_base&>(data); }
        alignas(alignment) char data[max_size];
    };
    
    template <typename T, std::size_t S, std::size_t A> void swap(polymorph<T, S, A>& x, polymorph<T, S, A>& y) noexcept { return x.swap(y); }
    template <typename T, std::size_t S, std::size_t A> void swap(polymorph<T, S, A>&& x, polymorph<T, S, A>& y) noexcept { return x.swap(y); }
    template <typename T, std::size_t S, std::size_t A> void swap(polymorph<T, S, A>& x, polymorph<T, S, A>&& y) noexcept { return x.swap(y); }
    template <typename T, std::size_t S, std::size_t A> void swap(polymorph<T, S, A>&& x, polymorph<T, S, A>&& y) noexcept { return x.swap(y); }
    ////////////////////////////////////////////////////////////////////////////////////////////
    #include <iostream>
    #include <vector>
    struct base : holder_base {
        virtual void foo() = 0;
        virtual ~base() {}
    };
    template <int I>
    struct derived : base
    {
        derived() { std::cout << (void*)this << " derived<" << I << ">::derived()\n"; }
        derived(const derived&) { std::cout << (void*)this << " derived<" << I << ">::derived(const derived&)\n"; }
        derived(derived&&) noexcept { std::cout << (void*)this << " derived<" << I << ">::derived(derived&&)\n"; }
        derived& operator=(const derived&) { std::cout << (void*)this << " derived<" << I << ">& derived<" << I << ">::operator=(const derived&)\n"; return *this; }
        derived& operator=(derived&&) noexcept { std::cout << (void*)this << " derived<" << I << ">& derived<" << I << ">::operator=(derived&&)\n"; return *this; }
        ~derived() noexcept { std::cout << (void*)this << " derived<" << I << ">::~derived()\n"; }
        virtual void foo() override { std::cout << (void*)this << " derived<" << I << ">::foo()\n"; }
    };
    
    int main() {
        std::vector<polymorph<base>> v;
        std::cout << "v.emplace_back(derived<0>{});\n";
        v.emplace_back(derived<0>{});
        std::cout << "v.push_back(derived<1>{});\n";
        v.push_back(derived<1>{});
        std::cout << "v.emplace_back(derived<2>{});\n";
        v.emplace_back(derived<2>{});
        std::cout << "***\n";
        for ( auto& x : v )
            x.get().foo();
        std::cout << "v[0] = std::move(v[0]);\n";
        v[0] = std::move(v[0]);
        std::cout << "v[0] = std::move(v[2]);\n";
        v[0] = std::move(v[2]);
        std::cout << "v[0] = v[0];\n";
        v[0] = v[0];
        std::cout << "v[0] = v[1];\n";
        v[0] = v[1];
        std::cout << "swap(v[0],v[0]);\n";
        swap(v[0],v[0]);
        std::cout << "swap(v[0],v[2]);\n";
        swap(v[0],v[2]);
        std::cout << "***\n";
        for ( auto& x : v )
            x.get().foo();
        std::cout << "return 0;\n";
    }
    


  • camper schrieb:

    Arcoth schrieb:

    hustbaer schrieb:

    Auch wenn es nur Beispielcode ist, globale Variablen, noch dazu mit nicht synchronisiertem Zugriff, finde ich schrecklich.

    Warum ist die Map eigentlich im namespace scope? AFAICS könnte das auch ein inline static member sein?

    Das ist ist im globalen Namensraum, damit sich jemand daran stört und den Code nicht einfach so ohne Nachdenken übernimmt.

    Ich bin nicht so optimistisch was das Mitdenken von Leuten die Code aus nem Forum bzw. allgemein aus einem Beispiel übernehmen angeht. Daher hab' ich es angesprochen. Hab schon zu oft erlebt dass mir ein Kollege auf die Frage wo Abscheuliches Konstrukt X herkommt die Antwort gibt "dass habe ich 1:1 von ... übernommen". Und dann auch noch meint dass das völlig OK wäre.



  • camper schrieb:

    1. die Typen der gespeicherten Objekten dürfen nicht final sein (sowieso meistens Unfug)

    Die Frage ist jetzt ziemlich OT, aber warum hältst du final für "meistens Unfug"?

    Ich finde final toll. Ich arbeite aktuell in einer grösseren Firma und du glaubst ja nicht auf was für Ideen Leute kommen. final ist dann erstmal ne gute Bremse. Denn die Hemmschwelle ein final wegzulöschen welches sich in Code befindet den nicht ihr Team "owned" ist schon eher gross. Bzw. ist es auch viel auffälliger bei Reviews wenn da auf einmal ein Edit in einem File ist wo bloss ein final weggelöscht wurde. Dann fragt man nach wieso, und kommt drauf dass jemand Unfug bauen wollte.

    Dass damit ein paar Tricks - wie den von dir verwendeten - verhindert, ist schade, aber mMn. das kleinere Übel.



  • hustbaer schrieb:

    camper schrieb:

    1. die Typen der gespeicherten Objekten dürfen nicht final sein (sowieso meistens Unfug)

    Die Frage ist jetzt ziemlich OT, aber warum hältst du final für "meistens Unfug"?

    Ich finde final toll. Ich arbeite aktuell in einer grösseren Firma und du glaubst ja nicht auf was für Ideen Leute kommen. final ist dann erstmal ne gute Bremse. Denn die Hemmschwelle ein final wegzulöschen welches sich in Code befindet den nicht ihr Team "owned" ist schon eher gross. Bzw. ist es auch viel auffälliger bei Reviews wenn da auf einmal ein Edit in einem File ist wo bloss ein final weggelöscht wurde. Dann fragt man nach wieso, und kommt drauf dass jemand Unfug bauen wollte.

    Dass damit ein paar Tricks - wie den von dir verwendeten - verhindert, ist schade, aber mMn. das kleinere Übel.

    Das ist dann Social engineering und davon habe ich keine Ahnung - deshalb: "meistens". Wenn ich hier sage, final sei Unfug meine ich, dass es weder hilft, das Programm zu verstehen, noch für bessere Codegenerierung sorgt. Gleichzeitig werden bestimmte Implementierungstechniken verhindert.

    Statt:
    holder_base
    |
    base
    |
    derived
    |
    holder<derived>

    könnte eine Implentierung auch CRTP benutzen:
    base
    |
    copymove_base<base>
    |
    copymove<derived, base>
    |
    derived

    das funktioniert dann auch mit final Klassen und das typeid-Problem besteht nicht. Im Gegenzug muss jede einzelne abgeleitete Klasse geändert werden, statt nur die Basis einmal. Wenn man allerdings die Basis sowieso nicht ändern darf, ist das auch kein Nachteil.



  • camper schrieb:

    Wenn ich hier sage, final sei Unfug meine ich, dass es weder hilft, das Programm zu verstehen, noch für bessere Codegenerierung sorgt. Gleichzeitig werden bestimmte Implementierungstechniken verhindert.

    Devirtualization?



  • camper schrieb:

    Das ist ist im globalen Namensraum, damit sich jemand daran stört und den Code nicht einfach so ohne Nachdenken übernimmt.

    Keine Sorge, ich hatte das nicht übernommen. Ich hab mich hauptsächlich grob daran orientiert und habs dann noch etwas anders gemacht.



  • camper schrieb:

    Wenn ich hier sage, final sei Unfug meine ich, dass es weder hilft, das Programm zu verstehen, noch für bessere Codegenerierung sorgt.

    Hm. Ich finde schon dass es hilft das Programm zu verstehen. Vielleicht nicht mega viel, aber ich meine es hilft mir schon zu wissen dass eine Klasse oder Funktion die ich mir gerade angucke maximal in dieser "most derived" Form verwendet wird. Speziell wenn das Ding virtuelle Funktionen hat muss ich mir ab da keinen Kopf mehr machen ob die nochmal wo überschrieben werden.



  • Etwas mehr zum Thema:

    Ich hätte mir schon ein paar mal gewünscht sowas in der Art zu haben. Allerdings nicht beschränkt auf die Grösse der Basisklasse, sondern mit einer Maximalgrösse die man als Template-Argument angeben kann. Evtl. auch mit der Möglichkeit grössere Objekte dann auf dem Heap zu erzeugen - wobei dieser Teil maximal "nice to have" wäre.

    Gibt es schon Proposals für eine Klasse dieser Art? Oder etwas in der Boost?

    Und fändet ihr sowas praktisch oder ist das etwas was eurer Meinung nach so selten gebraucht/gefragt wird dass es keinen Sinn macht?

    EDIT: Natürlich wäre, wenn man sowas in Boost/dem Standard aufnehmen wollen würde, eine Variante sinnvoll die mit beliebigen Typen funktioniert. Mein Favorit wäre da die "zusätzlicher Zeiger" Variante, weil schnell, flexibel und mMn. vertretbarer Speicher-Overhead.



  • Also eine Art std::any mit SOO (small object optimization) ?

    Edit: std::any tut das bereits, nur kann man den für kleine Objekte verfügbaren Speicher nicht konfigurieren.



  • Ein std::any mit (idealerweise konfigurierbarer) SOO und der Möglichkeit einen Basisklassenzeiger zu bekommen (soweit ich weiss kann man aus std::any nur exakt das rausholen was man reingesteckt hat, aber eben nicht nen Zeiger/ne Referenz auf ein Basisklassensubobjekt).

    Also quasi ein small_polymorphic_any .



  • Das dürfte ohne zusätzlichen Compilersupport unmöglich sein.
    Man müsste quasi einen dynamic_cast haben, der auf type_info-Objekten anstatt statisch bekannten Typen beruht.