1

I have a large class, Base, that implements several different binary operations, some overloading the operators like *=, *, / etc, and some extra ones. The typical return type involves Base explicitly, for example:

class Base {
    private:
        std::vector<unsigned int> _arr;

    public:
         // These operations do something with _arr
         Base& operator *= (const Base& rhs); 
         friend Base operator *(Base lhs, const Base& rhs);
         std::vector<Base> myoperation(const Base& rhs);
};

I have a derived class, Derived, which simply adds extra structure, so it looks like this:

class Derived : public Base {
    private:
        std::vector<int> _arr_derived;

    public: 
        // This operation does something with _arr and _arr_derived;
        Derived& my_derived_operation(const Derived& rhs);

        // These operations are exactly the same implementation as the one in Base!
        Derived& operator *= (const Derived& rhs) {
            Base::operator*=(rhs);
            return *this;
        }
        friend Derived operator *(Derived lhs, const Derived& rhs) {
            lhs *= rhs;
            return lhs; 
        }
        std::vector<Derived> myoperation(const Derived& rhs) // <---- what to do here?
};

What do I need to do with the operation myoperation? That returns a vector. There is no extra logic on all operations that are inherited, the only difference is in the return type. Is there any way to avoid the code duplication? Even the declaration of the operations is bothersome.

I thought perhaps defining the base operations as template functions returning the same type as the argument, and requiring the template parameter to be a derived class:

class Base {

    // on C++20
    template<class C> requires std::is_base_of_v<Base,C>
    C& operator*=(const C& rhs); 
}

Does this work? Even if it does, is there a better mechanism?

Edit to reply to Remy Lebeau's comment: I want to emphasize that all operations that appear both in Derived and in Base are meant to be the same, there is not extra logic except the returning type. The example of operator *= already shows this, just calling the Base version. A possible implementation, albeit stupid, of what I mean by myoperation could be this:

std::vector<Base> Base::myoperation(const Base& rhs) {
    std::vector<Base> ret{};
    ret.push_back(rhs*rhs);
    return ret;
}

std::vector<Derived> Derived::myoperation(const Derived& rhs) {
    std::vector<Derived> ret{}
    ret.push_back(rhs*rhs);
    return ret;
}

With the templated version I could write only

class Base {

    teplate<class C> requires std::is_base_of_v<Base,C>
    auto myoperation(const C& rhs) {
        std::vector<C> ret;
        ret.push_back(rhs*rhs);
        return ret;
    }
};

And that's what I meant by the templated solution above, so that I do not need to even declare myoperation on Derived.

Reimundo Heluani
  • 918
  • 9
  • 18
  • 3
    What does `Derived::myoperation()` do that is different than `Base::myoperation()`? Is `Derived::myoperation()` meant to *override* `Base::myoperation()`, or *overload* it? If *override*, then `Base::myoperation()` would need to be `virtual`. But you can't return `Derived` objects in a `vector` container, they would [get sliced](https://stackoverflow.com/questions/274626/). It would really help to see a [mcve] of what you are actually trying to accomplish with `Derived`. – Remy Lebeau Aug 24 '21 at 19:03
  • @RemyLebeau Does the edit help? – Reimundo Heluani Aug 24 '21 at 19:26
  • `std::vector myoperation(const Base& rhs);` this is going to slice. Is this what you want/expect ? – Richard Critten Aug 24 '21 at 19:28
  • @RichardCritten That's why I'm writing either a templated version returning `C` in `Base` or duplicating code explicitly rewriting the logic returning `Derived` in the `Derived::myoperation`. What I am asking is if there's a way to avoid code duplication and/of if this templated version works as it could lead to a myriad of problems that I haven't thought – Reimundo Heluani Aug 24 '21 at 19:32
  • 1
    Just use `std::vector> myoperation(const Base& rhs);` – Richard Critten Aug 24 '21 at 19:36

1 Answers1

0

I went with the templated version, although using pointers is standard in polymorphism, I have too many vector valued operations and are complicated enough to change the logic, and also wouldn't want to have to deal with recasting to Derived classes when necessary. Templating also has the advantage that no code needs to be written on the derived classes. So on base all operations that transform the object in place are left as they were and the ones that produce new ones are templated. I appreciate if I could be pointed to pitfalls of this approach

#include <iostream>
#include <vector>

class Base {
    int _a;

  public:
    Base(int a) : _a{a} {};
    Base& operator *= (const Base& rhs) { 
        _a *= rhs.a();
        return *this;
    }

    template <class C> requires std::is_base_of_v<Base,C>
    friend C operator *(C lhs, const Base& rhs) {
        lhs *= rhs;
        return lhs;
    }

    template <class C> requires std::is_base_of_v<Base,C>
    std::vector<C> myop (const C& rhs) {
        std::vector<C> ret {};
        ret.push_back(rhs*rhs);
        return ret;
    }

    int a() const { return _a; }
};

class Derived : public Base {
    int _b;

    public:
        Derived(int a, int b) : Base{a}, _b {b} {}

        int b() const {return _b;}
};
    
int main() {

    Derived C{9,5};
    Derived D{4,7};

    auto E = C*D;
    auto F = D.myop(C);
    std::cout << E.a() << ", " << E.b() << std::endl;
    std::cout << F.front().a() << ", " << F.front().b() << std::endl;

    return 0;
}

OUTPUT:

36, 5
81, 5
Reimundo Heluani
  • 918
  • 9
  • 18