Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 901
Георгий Гуминов Avatar asked Jul 29 '21 23:07

Георгий Гуминов


People also ask

What happens if we don't use virtual function in inheritance?

If you don't use virtual functions, you don't understand OOP yet. Because the virtual function is intimately bound with the concept of type, and type is at the core of object-oriented programming, there is no analog to the virtual function in a traditional procedural language.

How can you overcome the diamond problem in inheritance?

The solution to the diamond problem is to use the virtual keyword. We make the two parent classes (who inherit from the same grandparent class) into virtual classes in order to avoid two copies of the grandparent class in the child class.

In which situation do we need to implement virtual inheritance?

Virtual inheritance is used when we are dealing with multiple inheritance but want to prevent multiple instances of same class appearing in inheritance hierarchy. From above example we can see that “A” is inherited two times in D means an object of class “D” will contain two attributes of “a” (D::C::a and D::B::a).

What happens when we inherit a class virtually?

Instead, if classes B and C inherit virtually from class A , then objects of class D will contain only one set of the member variables from class A . This feature is most useful for multiple inheritance, as it makes the virtual base a common subobject for the deriving class and all classes that are derived from it.

Why do we use virtual inheritance in C++?

Fortunately, C++ allows us to solve this problem by using virtual inheritance. In order to prevent the compiler from giving an error we use the keyword virtual when we inherit from the base class storable in both derived classes: ... ... When we use virtual inheritance, we are guaranteed to get only a single instance of the common base class.

How to prevent inheritance in C++?

The concept of preventing the inheritance is known as final class. In Java or C#, we can use final classes. In C++ there are no such direct way. Here we will see how to simulate the final class in C++.

What is the last object destroyed in virtual inheritance?

In other words, the virtual base class will be the last object destroyed, because it is the first object that is fully constructed. A powerful technique that arises from using virtual inheritance is to delegate a method from a class in another class by using a common abstract base class. This is also called cross delegation.

What are the different types of inheritance in C++?

Inheritance in C++ 1. Single Inheritance: In single inheritance, a class is allowed to inherit from only one class. i.e. one sub class is... 2. Multiple Inheritance: Multiple Inheritance is a feature of C++ where a class can inherit from more than one classes. 3. Multilevel Inheritance: In this type ...


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.

like image 181
eerorika Avatar answered Oct 28 '22 17:10

eerorika


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.

like image 35
rm1948 Avatar answered Oct 28 '22 16:10

rm1948