0

There is a Student class inherited from Person. And there is Student class inherited from University. I want to change the parent class Person, University based on the option without rewriting Student such as Student1 and Student2 (because student class is very complicated). Here is the example code.

class Person {
  void f() {printf("I'm person")}
};

class University {
  void f() {printf("I'm university")}
};

class Student1 : public Person {
  void g() {f()}
};
class Student2 : public University {
  void g() {f()} // I don't wan't to rewrite this!
};

if (option.person) {
  Student1 student;
}
else {
  Student2 student;
}
fnclovers
  • 119
  • 9
  • Is `option.person` known at compile-time? – paolo May 13 '22 at 16:00
  • Also, what version of C++ are you using? C++20 has a whole host of useful things for this. – radj307 May 13 '22 at 16:10
  • I'm using c++ 17, but I can use c++ 20. please share if there is any optinion. – fnclovers May 13 '22 at 16:22
  • option.person is unkown at compile-time – fnclovers May 13 '22 at 16:22
  • It doesn’t make sense to have separate classes for each person – Daniel A. White May 13 '22 at 17:10
  • 1
    I don't think inheritance is what you want here. Inheritance is only useful in narrow subsets of "Is-A" relationships. A `Student` is *not* a `University`. Even if that was some other class that made more sense, I would recommend [using composition instead of inheritance](https://stackoverflow.com/questions/49002/prefer-composition-over-inheritance). – MHebes May 13 '22 at 17:18
  • 1
    If you find yourself struggling with your inheritance hierarchy and end up trying to do weird/impossible things with it, that may be a sign to step back and re-evaluate whether your class structure truly makes sense - in this case, the question of whether a Student is a University. – TheUndeadFish May 13 '22 at 17:21

1 Answers1

1

Since we can't know what option.person is at compile-time, we need to find a way to work around that at runtime.

One option for doing so is std::variant, which can store any number of different types; but does so at the cost of always having the same size as the largest templated type.

As an example, if I did this:

std::variant<char, int> myVariant = '!';

Even though myVariant holds a char (1 byte), it uses 4 bytes of RAM because an int is 4 bytes.

Using Variants

Rather than inheriting from different objects that do not share a common base at compile-time, we can maintain the 'base' type as a variable within Student instead.

#include <iostream>
#include <variant>
#include <concepts>

class Person {
public:
    void f()
    {
        std::cout << "I'm a person!\n";
    }
};
class University {
public:
    void f()
    {
        std::cout << "I'm a university!\n";
    }
};

class Student {
public:
    using variant_t = std::variant<Person, University>;
    variant_t base;

    // Here we accept an rvalue of any type, then we move it to the 'base' variable.
    //  if the type is not a Person or University, a compiler error is thrown.
    Student(auto&& owner) : base{ std::move(owner) } {}

    void g()
    {
        // METHOD 1:  Using std::holds_alternative & std::get
        //             This has the advantage of being the simplest & easiest to understand.
        if (std::holds_alternative<Person>(base))
            std::get<Person>(base).f();
        else if (std::holds_alternative<University>(base))
            std::get<University>(base).f();


        // METHOD 2:  Using std::get_if
        //             This has the advantage of being the shortest.
        if (auto* person = std::get_if<Person>(&base))
            person->f();
        else if (auto* university = std::get_if<University>(&base))
            university->f();


        // METHOD 3:  Using std::visit
        //             This has the advantage of throwing a meaningful compiler error if-
        //             -we modify `variant_t` and end up passing an unhandled type.
        std::visit([](auto&& owner) {
            using T = std::decay_t<decltype(owner)>;
            if constexpr (std::same_as<T, Person>)
                owner.f(); //< this calls  `Person::f()`
            else if constexpr (std::same_as<T, University>)
                owner.f(); //< this calls  `University::f()`
            else static_assert(false, "Not all potential variant types are handled!");
        }, base);
    }
};

In this example, I showed 3 different methods of accessing the underlying value of base.
As a result, the output is:
output

Further reading:

radj307
  • 439
  • 4
  • 11
  • While this is a good simple example of how `std::variant` works, I'm not sure it's the right tool for the job here. You could, for instance, just make `Student` a template and give it a member variable of the templated type. – MHebes May 13 '22 at 17:21
  • This is true, but it will prevent you from storing it in containers like `std::vector` that only allow one type. I do agree that templates are the preferable solution here if at all possible though. – radj307 May 13 '22 at 17:28
  • Indeed, but OP's original question also forbids you from storing `Student1` and `Student2` in the same container. If that is necessary, you can still have a base class that the class templates inherit from, or use another technique like type erasure or having `University`/`Person` inherit from the same class and store a pointer to `base`. – MHebes May 13 '22 at 17:42