Search code examples
c++classinheritanceoperators

Use defined operators from base class in inherited class


Assume I have a base class

class Vector
{
public:
    Vector(const std::vector<float> &elements);

    static Vector add(const Vector &v1, const Vector &v2);

    Vector Vector::operator+(const Vector &v) const
    {
        return add(*this, v);
    }

    Vector &Vector::operator+=(const Vector &v)
    {
        *this = add(*this, v);
        return *this;
    }

    float operator[](size_t index) const;

private:
    std::vector<float> _elements;
}

And a derived class for a 3 element vector:

class Vector3 : public Vector
{
public:
    Vector3(float x, float y, float z);
};

What would I have to do to use the operators with the derived class?

My solution was to redefine the operators in the derived class like so:

Vector3 Vector3::operator+(const Vector3 &v) const
{
    Vector res = Vector::operator+(v);
    return Vector3(res[0], res[1], res[2]);
}

Vector3 &Vector3::operator+=(const Vector3 &v)
{
    Vector res = Vector::operator+=(v);
    *this = Vector3(res[0], res[1], res[2]);
    return *this;
}

That works so far, but I was wondering if this is the right way to go. For me it looks a lot like double work. Furthermore I don't fully understand why I even have to redefine the operators in the first place. Is it because I can't cast from Vector to Vector3? My simple assumption was Vector3 is the same as Vector with size 3, so in my mind there isn't a difference that would be relevant for the given operator task to be different. If someone could enlighten me whether there is a better approach that would be great.


Solution

  • Frame challenge: What you want to do here adds complexity, inheritance and the hoops (practical and conceptual) you have to jump through because of it, and reduces performance1. That's the kiss of death trade-off. Since you generally only want to suffer increased complexity in order to get increased performance, do something else.

    My suggestion, keeping most of what you had in mind is to

    1. Turn Vector into a template, with the size being a template argument (and potentially the data type stored so you can easily switch between floats, doubles, and ints)
    2. Turn Vector3 into a typedef. eg using Vector3 = Vector<3>;. Now Vector3 literaly is a Vector so there is no need for wresting with inheritance while providing exactly the behaviour you want.
    3. Make the operator overloading a bit more idiomatic.

    Here is a quick demo:

    #include <array>
    #include <cstddef>
    #include <functional>
    #include <algorithm>
    #include <iostream>
    
    template<size_t SIZE> // A more general <class TYPE, int SIZE> will be more useful
    class Vector
    {
    public:
        // initializer_list may make this easier to work with
        Vector(const std::array<float,SIZE> &elements): _elements(elements)
        {
    
        }
    
        Vector & operator+=(const Vector &v)
        {
            //just crapping out a simple summation here for demo purposes
            std::transform(_elements.begin(), _elements.end(),
                           v._elements.begin(),
                           _elements.begin(),
                           std::plus());
            return *this;
        }
    
        float operator[](size_t index) const;
        // consider adding the usual iterator -granting functions so 
        // you can use one of these in Standard algorithms
    
        // added for demo purposes to print out the result
        friend std::ostream & operator<<(std::ostream & out,
                                         const Vector & vec)
        {
            for (const auto & val:vec._elements)
            {
                out << val << ' ';
            }
            return out;
        }
    
    private:
        std::array<float, SIZE> _elements; // Look ma! No pointers! 
                                           // The Vector data is IN the Vector!
                                           // no extra look-ups and no extra 
                                           // cache misses
    };
    
    
    // now a free function to aid in decoupling 
    template <size_t SIZE>
    Vector<SIZE> operator+(Vector<SIZE> lhs, 
                           const Vector<SIZE> &rhs)
    {
        lhs += rhs;
        return lhs;
    }
    
    using Vector3 = Vector<3>;
    
    int main()
    {
        Vector3 a({1,2,3});
        Vector3 b({3,2,1});
        Vector3 c = a+b;
    
        std::cout << c << '\n';
    
        Vector<4> d({});
    
        //Vector3 e = c+d; // addition fails at compile time because the types 
                           // don't match. Much debug savings
    }
    

    1 std::vector points at its data, so the CPU almost always has to read the Vector and the vector data separately. For a small amount of data that could have been read in one shot, this can be an utterly murderous performance hit.