Search code examples
c++c++17multiple-inheritancertti

How to avoid virtual inheritance in C++17?


Let's see example classes. Base class is ITransport, transport class interface:

class ITransport {
  public:
    virtual void move(const Path& p) = 0;
    virtual double estimateTime(const Path& path) = 0;
    /*Some more methods.*/
};

Implementation:

class Transport : public ITransport { 
  public:
    virtual void move(const Path& p) override {
        currPoint_ = p.lastPoint(); 
    }
    /*Some more methods.*/
  private:
    Point currPoint_;
};

Let's also imagine we want to create a self moving transport class:

template <typename EnergySource>
class SelfMovingTransport : public Transport {
  /*Some special methods for self moving transport.*/
};

The simplest example of self-moving transport is car:

template <typename EnergySource>
class Car : public SelfMovingTransport <EnergySource> {
  public:
    virtual void visitCarService() = 0;
    /*Some more methods with logic for cars.*/
};

Also need to create car with internal combustion engine...

class ICECar : public Car<Petrol> {
  public:
    virtual void move(const Path& p) override { 
        Transport::move(p);
        /*Some special methods for ICECar.*/ 
    }
    virtual void visitCarService() override { 
      /*Visit closest ICECar service.*/ 
    } 
    /*Some special methods for ICECar.*/
  private:
    Petrol::Amount petrol_;
};

... and an electric car class.

class ElectricCar : public Car<Electriсity> {
  public:
    virtual void move(const Path& p) override { 
        Transport::move(p); 
        /*Some special methods for ElectricCar.*/ 
    }
    virtual void visitCarService() override { 
      /*Visit closest ElectricCar service.*/ 
    }
    /*Some special methods for ElectricCar.*/
  private:
    Electricity::Amount charge_; 
};

The continuation of this logic can be, for example, adding trains class and etc.:

template <typename EnergySource>
class Train : public SelfMovingTransport<EnergySource> { 
  /*Not interesting.*/ 
};

I use c++17 compiller (MS). Not less, not more.

I want to create an array (or std::vector<Car*>) of pointers to cars of different types and call some common methods for them. For example, to have a simple way to send them all to the service (see Car::visitCarServeice()).

I've tried tree ideas:

  • Create classes ISelfMovingTransport and ICar:
class ISelfMovingTransport : public virtual ITransport { 
 /*All the same.*/ 
};
class ICar : public virtual ISelfMovingTransport { 
 /*All the same.*/ 
};

Changed Transprot to:

class Transport : public virtual ITransport { 
 /* All the same. */
}

Changed SelfMovingTransport to:

template <typename EnergySource>
class SelfMovingTransport : public ISelfMovingTransport, 
                            public Transport<EnergySource> {};

Changed Car to:

template <typename EnergySource>
class Car: public ICar, public SelfMovingTransport<EnergySource> {
 /*All the same*/ 
};

In the end solution did not work, because static_cast can not be used to cast pointer to virtually derived class pointer (See pastebin link.). Example code can't be compiled (error: cannot convert from pointer to base class ‘ISelfMovingTransport’ to pointer to derived class ‘ElectricCar’ because the base is virtual). When I want to make actions with ElectricCar which is accessed as a pointer to a Car, I need dynamic_cast<ElectricCar*>(carPtr) where carPtr is of Car*. But dynamic_cast is not allowed, RTTI is turned off.

  • Use std::vector<Transport*> and cast objects to Car. It worked, but I did not like this solution, because it is hard to check if everything is correct.
  • Using std::variant<ICECar, ElectricCar> and std::visit. (std::visit([](auto& car) -> void { car.visitCarServeice(); }, carV)). (Now implemented with this method.).

In this example (which represents a problem in a real project) I don't want to change logic (especially classes from Transport level to Car level).

Is there a common way to do required things without RTTI and dynamic_cast?

Is std::variant 'OK' in this situation (assuming that car classes don't differ by size and/or memory is not important)?

Asked question, because don't know how to google that.

P.S. All examples are representation (analog, etc...) of a situation in real project. I ask you to imagine that energy type as parameter is really needed and not to think about complications (hybrid cars, etc.).

P.P.S. In the real project I need an only object of a "car" as a field of other class.


Solution

  • I believe this addresses your question but hard to tell. It is stripped of some of the details in your code but demonstrates the technique. It uses std::variant and std::visit.

    #include <iostream>
    #include <memory>
    #include <variant>
    
    class Car {
    public:
        Car( ) = default;
    };
    
    class ICECar : public Car {
    public:
        ICECar( ) {
        }
        void visitCarService( ) {
            std::cout << "ICE visitCarService\n";
        }
    };
    
    class ECar : public Car {
    public:
        ECar( ) {
        }
        void visitCarService( ) {
            std::cout << "E visitCarService\n";
        }
    };
    
    using car_variant = std::variant<
        std::shared_ptr<ICECar>,
        std::shared_ptr<ECar>>;
    
    template <size_t C>
    void visitCarService(std::array<car_variant, C>& cars) {
        for (auto& c : cars) {
            std::visit(
                [](auto&& arg) {
                    arg->visitCarService( );
                },
                c);
        }
    }
    
    int main(int argc, char** argv) {
    
        ICECar ice_car { };
        ECar e_car { };
    
        std::array<car_variant, 2> cars {
            std::make_shared<ICECar>(ice_car),
            std::make_shared<ECar>(e_car)
        };
    
        visitCarService(cars);
    
        return 0;
    }
    

    This compiled using GCC 11 using std=c++17 with -pedantic set. Presumably it should compile under MS. Here is a an online run of it.