How to avoid virtual inheritance in C++17?

Viewed 409

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.

2 Answers

I want to create an array of cars of different types.

You cannot. Arrays are homogeneous in C++. All elements always have the same type.

Use std::vector<Transport*> and cast objects to Car

If the vector elements are supposed to point to cars only, then it seems like it would make more sense to use pointers to ICar instead. Furthermore, if the vector is supposed to own the pointed objects, then you should use smart pointers. And you would need to make the destructors virtual.

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

Typically yes: Virtual functions.

Is std::variant 'OK' in this situation

It can be, if the number of variants is a small constant. By contrast, inheritance hierarchy allows addition of arbitrarily many subclasses.

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.

Related