How to avoid virtual inheritance in C++17?
Asked Answered
H

2

6

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.

Hildegard answered 29/7, 2021 at 23:41 Comment(19)
Unfortunately as far as C++ is concerned ICECar and ElectricCar are completely unrelated classes and what (if anything) they inherit is their implementation detail. ICECar can be an empty class and that would be valid.Devitrify
SelfMovingTransport doesn't make much sense, as not all cars are self-moving, so Car should not derive from SelfMovingTransport. But you might have SelfMovingCar that derives from Car instead. Also, using a template for the energy type doesn't make sense either. For instance, a Car that has has been upgraded from a Petrol engine to an Electric engine is still a Car. Making Car a template just makes it harder to group different types of Cars together. Much of what you are trying to use inheritance for should probably be using encapsulation or Dependency Injection instead.Mellissamellitz
@RemyLebeau Example is a representatin of a problem in a real project. In it I can't erase template parameters, or change strucure in existing "parent" classes (Imagine, template parameter in Car is really needed etc..).Harville
You don't really want to mix a polymorphic hierarchy with std::variantSupercharge
@Supercharge What's then?Harville
It seems like you're asking "How do I implement RTTI manually". The variant stores information about which type is stored in it currently. Which begs the question of why not just use the RTTI.Supercharge
@ГеоргийГуминов Your design seems complex with multiple inheritance, virtual inheritance, templates (with virtual functions), std::variant... In the long terme, it will probably become a maintenace nightmare. For example, your design is already broken with hybrid cars. Engine and energy should be member of the class. I would recommand you to completly forget about existing design as the existing design has many flaws that would require to rethink the whole class hierarchy. Inheritance is a very strong link thus complex hierarchy are rarely desirable.Polley
In my experience, the "classic" examples used to teach object-oriented programming do not generally represent real-world problems and so end up teaching the wrong thing. Polymorphism really only works when every subtype, truly, is a base type. Or at the very least exhibits exactly the same behaviours. Like, for instance, communication protocols that all do the exact same thing in different ways. Using inheritance to build Frankenstein Classes is just not very useful and tends to create more problems than it solves.Hydrated
Your use of templates is both /* not interesting */ and a source of significant design constraints. You are confusing pointers/references and objects. You mention RTTI/dynamic cast as a problem but do not demonstrate it occurring. Your example code is full of typos, does not demonstrate the problem, so we cannot use it to solve or understand your actual problem.Moussorgsky
How about IVisitService declare visitService() and let all derived Car implement it? Then maintain a list of pointers point to instances implementing IVisitService so you can call them all in a loop.Nurse
@Supercharge "How do I implement RTTI manually?": Yeah, I had this feeling, but wanted to find some professional cheating ideas, if they exist. "Which begs the question of why not just use the RTTI." It is turned off for the project and I guess can't be turned on only for this my part.Harville
@LouisGo "IVisitService". It seems to be the most correct method, but not the simplest to implement (one additional class).Harville
In the end solution did not work, because RTTI is not allowed: When I want to make actions with ElectricCar which is accessed as a pointer to a Car, I need dynamic_cast<ElectricCar*>(...). -- but why can't you use static_cast here?Tellurate
@Alex Guteniev I've made corrections in the question to show the problem with static_cast better.Harville
We might be able to give a better advise if you explain what you're actually building, not in terms of unrelated abstractions. There might be an entirely different solution possible. Why no RTTI? Is it an embedded project?Savoy
@Savoy I am working on an office app (ERP system). More precisely, interface components, drown with Microsoft GDI+ lib.Harville
Do you meant gui? Even gui could have rtti. Please update your question to real world case.Nurse
@Louis Go RTTI is prohibited in all the project.Harville
use reinterpret_cast instead hahaCrim
C
1

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.

Chancechancel answered 4/8, 2021 at 2:29 Comment(0)
H
7

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.

Hawkins answered 30/7, 2021 at 0:17 Comment(0)
C
1

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.

Chancechancel answered 4/8, 2021 at 2:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.