How can I create a polymorphic object on the stack?

前端 未结 12 1671
谎友^
谎友^ 2021-01-12 05:40

How do I allocate a polymorphic object on the stack? I\'m trying to do something similar to (trying to avoid heap allocation with new)?:

A* a = NULL;

switch         


        
相关标签:
12条回答
  • 2021-01-12 06:31

    You can't structure a single function to work like that, since automatic or temporary objects created inside a conditional block can't have their lifetimes extended into the containing block.

    I'd suggest refactoring the polymorphic behaviour into a separate function:

    void do_something(A&&);
    
    switch (some_var)
    {
    case 1:
        do_something(A());
        break;
    case 2:
        do_something(B()); // B is derived from A
        break;
    default:
        do_something(C()); // C is derived from A
        break;
    }
    
    0 讨论(0)
  • 2021-01-12 06:32

    I wrote a generic template to do it. Full code available here (it became too elaborate for the scope of this question). StackVariant object contains a buffer of the size of the biggest type out of the provided types, and biggest alignment as well. The Object is constructed on the stack using a 'placement new' and operator->() is used for polymorphic access to suggest the indirection. Also, it is important to make sure that if a virtual detor is defined, it should be called upon destruction of the object on the stack, so the template detor is doing just that using a SFINAE definition.

    (see usage example and output below):

    //  compile: g++ file.cpp -std=c++11
    #include <type_traits>
    #include <cstddef>
    
    // union_size()/union_align() implementation in gist link above
    
    template<class Tbaseclass, typename...classes>
    class StackVariant {
        alignas(union_align<classes...>()) char storage[union_size<classes...>()];
    public:
        inline Tbaseclass* operator->() { return ((Tbaseclass*)storage); }
        template<class C, typename...TCtor_params>
        StackVariant& init(TCtor_params&&...fargs)
        {
            new (storage) C(std::forward<TCtor_params>(fargs)...);      // "placement new"
            return *this;
        };
    
    
        template<class X=Tbaseclass>
        typename std::enable_if<std::has_virtual_destructor<X>::value, void>::type
        call_dtor(){
            ((X*)storage)->~X();
        }
    
        template<class X=Tbaseclass>
        typename std::enable_if<!std::has_virtual_destructor<X>::value, void>::type
        call_dtor() {};
    
        ~StackVariant() {
            call_dtor();
        }
    };
    

    Usage example:

    #include <cstring>
    #include <iostream>
    #include "StackVariant.h"
    
    class Animal{
    public:
        virtual void makeSound() = 0;
        virtual std::string name() = 0;
        virtual ~Animal() = default;
    };
    
    class Dog : public Animal{
    public:
        void makeSound() final { std::cout << "woff" << std::endl; };
        std::string name() final { return "dog"; };
        Dog(){};
        ~Dog() {std::cout << "woff bye!" << std::endl;}
    };
    
    class Cat : public Animal{
        std::string catname;
    public:
        Cat() : catname("gonzo") {};
        Cat(const std::string& _name) : catname(_name) {};
        void makeSound() final { std::cout << "meow" << std::endl; };
        std::string name() final { return catname; };
    };
    
    using StackAnimal = StackVariant<Animal, Dog, Cat>;
    
    int main() {
        StackAnimal a1;
        StackAnimal a2;
        a1.init<Cat>("gonzo2");
        a2.init<Dog>();  
        a1->makeSound();
        a2->makeSound();
        return 0;
    }
    //  Output:
    //  meow
    //  woff
    //  woff bye!
    

    Few things to note:

    1. I wrote it while trying to avoid heap allocations in performance critical functions and it did the job - 50% speed gains.
    2. I wrote it to utilize C++'s own polymorphic mechanisms. Before that my code was full of switch-cases like the previous suggestions here.
    0 讨论(0)
  • 2021-01-12 06:33

    You can't create a polymorphic local variable

    You can't create a polymorphic local variable, since a subclass B of A might have more attributes than A, thus take more place, so the compiler would have to reserve enough space for the largest subclass of A.

    1. In case you have dozens of subclasses, and one of them has a large number of attributes, this would waste a lot of space.
    2. In case you put in the local variable an instance of a subclass of A you received as a parameter, and you put your code in a dynamic library, then the code linking with it could declare a subclass larger than those in your library, so the compiler wouldn't have allocated enough space on the stack anyway.

    So allocate space for it yourself

    Using placement new, you can initialize the object in a space you allocated through some other means:

    • alloca, but seeing this SO question it seems it's not the best option.
    • A Variable Length Array, with which comes some (non-)portability fun, since it works under GCC but isn't in the C++ standard (not even in C++11)
    • aligned_union<A, B, C>::type, as suggested by R. Martinho Fernandes in a comment to this answer

    However, these techniques may use a lot of extra space, and don't work if you are given a reference (pointer) to an unknown-at-compile-time subclass of A that is larger than the types you accounted for.

    The solution I propose is to have a kind of factory method on each subclass, that calls a supplied function with a pointer to a stack-allocated instance of the given subclass. I added an extra void* parameter to the supplied function's signature, so one can pass it arbitrary data.

    @MooingDuck suggested this implementation using templates and C++11 in a comment below. In case you need this for code that can't benefit from C++11 features, or for some plain C code with structs instead of classes (if struct B has a first field of type struct A, then it can be manipulated somewhat like a "substruct" of A), then my version below will do the trick (but without being type-safe).

    This version works with newly defined subclasses, as long as they implement the ugly factory-like method, and it will use a constant amount of stack for the return address and other informations required by this intermediate function, plus the size of an instance of the requested class, but not the size of the largest subclass (unless you choose to use that one).

    #include <iostream>
    class A {
        public:
        int fieldA;
        static void* ugly(void* (*f)(A*, void*), void* param) {
            A instance;
            return f(&instance, param);
        }
        // ...
    };
    class B : public A {
        public:
        int fieldB;
        static void* ugly(void* (*f)(A*, void*), void* param) {
            B instance;
            return f(&instance, param);
        }
        // ...
    };
    class C : public B {
        public:
        int fieldC;
        static void* ugly(void* (*f)(A*, void*), void* param) {
            C instance;
            return f(&instance, param);
        }
        // ...
    };
    void* doWork(A* abc, void* param) {
        abc->fieldA = (int)param;
        if ((int)param == 4) {
            ((C*)abc)->fieldC++;
        }
        return (void*)abc->fieldA;
    }
    void* otherWork(A* abc, void* param) {
        // Do something with abc
        return (void*)(((int)param)/2);
    }
    int main() {
        std::cout << (int)A::ugly(doWork, (void*)3);
        std::cout << (int)B::ugly(doWork, (void*)1);
        std::cout << (int)C::ugly(doWork, (void*)4);
        std::cout << (int)A::ugly(otherWork, (void*)2);
        std::cout << (int)C::ugly(otherWork, (void*)11);
        std::cout << (int)B::ugly(otherWork, (void*)19);
        std::cout << std::endl;
        return 0;
    }
    

    By then, I think we might have outweighed the costs of a simple malloc, so you might wand to use that after all.

    0 讨论(0)
  • 2021-01-12 06:34

    trying to avoid heap allocation with new)?

    Well in that case you create object on stack as usual and assign address to the base pointer. But remember, if this is done inside a function, don't pass the address as return value, because stack will unwind after the function call returns.

    So this is bad.

    A* SomeMethod()
    {
        B b;
        A* a = &b; // B inherits from A
        return a;
    }
    
    0 讨论(0)
  • 2021-01-12 06:38

    Disclaimer: I definitely don't think this is a good solution. The good solutions are to either rethink the design (maybe OO polymorphism is not warranted here given that there is a bounded number of possibilities?), or to use a second function to pass along said polymorphic object by reference.

    But since other folks mentioned this idea, but got details wrong, I'm posting this answer to show how to get it right. Hopefully I get it right.

    It is clear the the number of possible types is bounded. This means that a discriminated union, like boost::variant could solve the problem, even if it's not pretty:

    boost::variant<A, B, C> thingy = 
        some_var == 1? static_cast<A&&>(A())
        : some_var == 2? static_cast<A&&>(B())
        : static_cast<A&&>(C());
    

    The fact that now you can use things like static visitors is one if the things that keeps making me think this isn't a good use of OO polymorphism.

    If instead of a ready-made solution, you want to use placement new by hand as suggested in other answers, there are a number of things that need care because we lose some of the properties of regular automatic objects in the process:

    • the compiler no longer gives us the right size and alignment;
    • we no longer get an automatic call to the destructors;

    In C++11, these are both easy to fix with aligned_union and unique_ptr, respectively.

    std::aligned_union<A, B, C>::type thingy;
    A* ptr;
    switch (some_var)
    {
    case 1:
        ptr = ::new(&thingy.a) A();
        break;
    case 2:
        ptr = ::new(&thingy.b) B();
        break;
    default:
        ptr = ::new(&thingy.c) C();
        break;
    }
    std::unique_ptr<A, void(*)(A*)> guard { ptr, [](A* a) { a->~A(); } };
    // all this mechanism is a great candidate for encapsulation in a class of its own
    // but boost::variant already exists, so...
    

    For compilers that don't support these features, you can get alternatives: Boost includes aligned_storage and alignment_of traits which can be used to build aligned_union; and unique_ptr can be replaced with some kind of scope guard class.

    Now that that is out of the way, just so it's clear, don't do this and simply pass a temporary along to another function, or revisit the design altogether.

    0 讨论(0)
  • 2021-01-12 06:39

    To strictly answer your question - what you have now does just that - i.e. a = A(); and a = B() and a = C(), but these objects are sliced.

    To achieve polymorphic behavior with the code you have, I', afraid that's not possible. The compiler needs to know the size beforehand of the object. Unless you have references or pointers.

    If you use a pointer, you need to make sure it doesn't end up dangling:

    A* a = NULL;
    
    switch (some_var)
    {
    case 1:
        A obj;
        a = &obj;
        break;
    }
    

    won't work because obj goes out of scope. So you're left with:

    A* a = NULL;
    A obj1;
    B obj2;
    C obj3;
    switch (some_var)
    {
    case 1:
        a = &obj1;
        break;
    case 2:
        a = &obj2;
        break;
    case 3:
        a = &obj3;
        break;
    }
    

    This of course is wasteful.

    For references it's a bit trickier because they have to be assigned on creation, and you can't use temporaries (unless it's a const reference). So you'll probably need a factory that returns a persistent reference.

    0 讨论(0)
提交回复
热议问题