Search code examples
c++c++11polymorphismstdvectorvirtual-functions

Can I have a vector of polymorphic values under a fixed sized assumption?


Consider the below code, a derived class replaces a virtual member function with a variant of the function, but does not add any new member variables. Values of Base and Derived are added to a common container, std::vector and as expected the Derived value is sliced. However by copying the representation in memory of the Derived value into the container the value is in effect only partially sliced.

#include <iostream>
#include <vector>

class Base {
public:
    Base() = default;
    Base(float arg) : a{ arg } {};

    virtual float doSomething(float b) const { return a + b; }

    float a;
};

class Derived : public Base {
public:
    Derived() = default;
    Derived(float a) : Base{ a } {};

    float doSomething(float b) const { return a - b; }
};

int main()
{
    Base b{ 1.0f };
    Derived d{ 1.0f };

    std::cout << sizeof(b) << ", " << sizeof(d) << '\n';  // 8, 8

    std::vector<Base> v{ b, d };  // d is sliced
    std::cout << v[0].doSomething(2.0f) << '\n';  // 3
    std::cout << v[1].doSomething(2.0f) << '\n';  // 3 as d was sliced

    memcpy(&v[1], &d, sizeof(d));  // Copy the representation of d over to v[1]
    std::cout << v[1].doSomething(2.0f) << '\n';  // now -1
}

The size of the values is 8 due to the pointer to the virtual function table and this is how to above polymorphism is being realised. The type of v[1] is always Base, so if Derived added a new member function it wouldn't be possible to call it. In effect v[1] is still sliced to Base but with the reimplemented member functions of Derived.

Under the assumption that Base is essentially POD but with added virtual member functions, all of which are const i.e. memory copyable, and that Derived only reimplements those member functions:

  1. Does the above code fall into undefined behaviour?
  2. If so is there a way to implement this without the memcpy or equivalent in a way that would be defined behaviour?
  3. If this is a common pattern, what is it called?

Solution

  • Starting with your questions:

    1. Does the above code fall into undefined behaviour?

    Yes. Use of memcpy on non-trivially copyable objects is undefined behavior.

    1. If so is there a way to implement this without the memcpy or equivalent in a way that would be defined behaviour?

    Yes, there is. It will still use polymorphism - not for the object that you store but rather for its field(s).

    1. If this is a common pattern, what is it called?

    Yes. The proposed solution has a name. It is called Strategy-Pattern or State-Pattern (depending on what is exactly the purpose of what you are trying to achieve).

    Here is an equivalent code (in a way) to what you try to achieve:

    The different strategies

    class Base {
    public:
        virtual ~Base() {}
        virtual float doSomething(float a, float b) const { return a + b; }
    };
    
    class Derived : public Base {
    public:
        float doSomething(float a, float b) const override { return a - b; }
    };
    

    The Actual Type to be stored

    class RealType {
        float a;
        const Base* strategy;
    public:
        // just for the example, could be implemented in other ways
        const static Base BaseStrategy;
        const static Derived DerivedStrategy;
    
        RealType(float val, const Base& s): a(val), strategy(&s) {}
        float doSomething(float b) const { return strategy->doSomething(a, b); }
    };
    
    const Base RealType::BaseStrategy {};
    const Derived RealType::DerivedStrategy {};
    

    Usage Example

    int main()
    {
        RealType b{ 1.0f, RealType::BaseStrategy };
        RealType d{ 1.0f, RealType::DerivedStrategy };
    
        std::cout << sizeof(b) << ", " << sizeof(d) << '\n';  // size of pointer
    
        std::vector<RealType> v{ b, d };  // no slicing
        std::cout << v[0].doSomething(2.0f) << '\n';  // 3
        std::cout << v[1].doSomething(2.0f) << '\n';  // -1 as no slicing
    
        v[0] = v[1]; // copies both the value stored in v[1] as well as the strategy
        std::cout << v[0].doSomething(2.0f) << '\n';  // now -1 with v[0]
    }
    

    Code: http://coliru.stacked-crooked.com/a/e1b102bc70427177