Search code examples
c++oopinheritancedynamicpolymorphism

How to define a base class method only once, that when subclassed, uses members of the subclass and not the base class?


I have a base class here:

typedef float Pounds;
typedef int Seconds;
typedef float MilliJoulesPerLiter;
typedef float MilliJoules;
typedef int Liters;

class Fuel {
    protected:
        static const Pounds carbonDioxideEmmissionsPerLiter;
        static const Seconds burnTime;
        static const MilliJoulesPerLiter energyYield; 
        Liters volume {10};
        MilliJoules totalMilliJoules {0};
        
    public:
        Fuel() : Fuel {10} {};
        
        Fuel(int vol) : volume(vol) {
            totalMilliJoules = volume * energyYield;
        }

        virtual void burn() {
            cout << "Burning fuel..." << endl;
            int quantity = volume < burnTime ? volume : burnTime;
            Sleep(quantity);
            volume -= quantity;
            totalMilliJoules /= volume * energyYield;
            totalEmmissions += carbonDioxideEmmissionsPerLiter * quantity;
            cout << "Done burning fuel. Total millijoules burnt: " << quantity * energyYield << endl;
        }
};

As you can see, I have a burn() method that uses variables that the base class itself defines. That burn() method should work the same exact way for all subclasses, the only difference is that the subclasses will have different values for those members.

So, here are my subclasses:

class Gasoline : public Fuel {
    protected:
        static const Pounds carbonDioxideEmmissionsPerLiter {5.07};
        static const Seconds burnTime {10};
        static const MilliJoulesPerLiter energyYield {34.8};
        Liters volume {0};
        MilliJoules totalMilliJoules {0};

    public:
        Gasoline() : Fuel {} {};
        Gasoline(int vol) : Fuel {vol} {};
};

class Coal : public Fuel {
    protected:
        static const Pounds carbonDioxideEmmissionsPerLiter {2.42};
        static const Seconds burnTime {5};
        static const MilliJoulesPerLiter energyYield {23.9};
        Liters volume {0};
        MilliJoules totalMilliJoules {0};
    public:
        Coal() : Fuel {} {};
        Coal(int vol) : Fuel {vol} {};
};

class Propane : public Fuel {
    protected:
        static const Pounds carbonDioxideEmmissionsPerLiter {3.17};
        static const Seconds burnTime {20};
        static const MilliJoulesPerLiter energyYield {25};
        Liters volume {0};
        MilliJoules totalMilliJoules {0};
    public:
        Propane() : Fuel {} {};
        Propane(int vol) : Fuel {vol} {};
};

Every time I make a derived class object out of these and I call burn(), I get the base class values. I do know exactly why this is happening; obviously if I don't explicitly override the method burn(), then it will default to the base class version.

However, burn() is of rather nontrivial length, and so copy/pasting it throughout all the subclasses seems very repetitive.

Is there a way to write a method only once in a base class, where the ONLY difference is what the values of the class attributes are, and it uses those?

Essentially, we use virtual functions to resolve what version of the function we use at runtime, for virtual function calls. Are there virtual member accesses too?

  • Tried making the burn() function virtual, even though I know that's only for functions, but it was worth a try. Still uses base class version without an override.
  • Tried using this->, to no avail.
  • Tried adding virtual keyword to member attributes, which is not valid syntax.
  • Initialized subclass attributes with an initializer list, changed from static/constant to non-static and non-constant, but the common denominator is not overriding burn().
  • Had subclasses define their own constructors without using base class constructor, but still not overriding burn()

Solution

  • You can't override a base class's static members in derived classes, C++ simply does not work that way.

    Your base class data members should not be static to begin with. Make them non-static instead, so that each object instance carries its own values. And then either:

    1. have the derived class constructors assign values to the data members, either directly if possible, otherwise through the base class constructor, eg:
    class Fuel {
        protected:
            const Pounds carbonDioxideEmmissionsPerLiter;
            const Seconds burnTime;
            const MilliJoulesPerLiter energyYield; 
    
            Liters volume {10};
            MilliJoules totalMilliJoules {0};
            
        public:
            Fuel(Pounds carbonDioxideEmmissionsPerLiter, Seconds burnTime, const MilliJoulesPerLiter energyYield, Liters volume = 10) :
                carbonDioxideEmmissionsPerLiter(carbonDioxideEmmissionsPerLiter),
                burnTime(burnTime),
                energyYield(energyYield),
                volume(volume)
            {
                totalMilliJoules = volume * energyYield;
            } 
    
            void burn() {
                cout << "Burning fuel..." << endl;
                int quantity = volume < burnTime ? volume : burnTime;
                Sleep(quantity);
                volume -= quantity;
                totalMilliJoules /= volume * energyYield;
                totalEmmissions += carbonDioxideEmmissionsPerLiter * quantity;
                cout << "Done burning fuel. Total millijoules burnt: " << quantity * energyYield << endl;
            }
    };
    
    class Gasoline : public Fuel {
        public:
            Gasoline(Liters volume = 10) : Fuel(5.07, 10, 34.8, volume) {}
    };
    
    class Coal : public Fuel {
        public:
            Coal(Liters volume = 10) : Fuel(2.42, 5, 23.9, volume} {}
    };
    
    class Propane : public Fuel {
        public:
            Propane(Liters volume = 10) : Fuel(3.17, 20, 25, volume) {}
    };
    
    1. define virtual getter/setter methods in the base class to access the values, and then have the derived classes override those methods, eg:
    class Fuel {
        protected:
            virtual Pounds get_carbonDioxideEmmissionsPerLiter() const = 0;
            virtual Seconds get_burnTime() = 0;
            virtual MilliJoulesPerLiter get_energyYield() = 0; 
    
            Liters volume {10};
            
        public:
            Fuel(Liters volume = 10) : volume(volume) {} 
    
            // can't calculate this value in the constructor anymore,
            // since a base class constructor can't call virtual methods
            // on a derived class...
            MilliJoules get_totalMilliJoules() const { return volume * get_energyYield(); }
    
            void burn() {
                cout << "Burning fuel..." << endl;
                int quantity = volume < get_burnTime() ? volume : get_burnTime();
                Sleep(quantity);
                volume -= quantity;
                totalEmmissions += get_carbonDioxideEmmissionsPerLiter() * quantity;
                cout << "Done burning fuel. Total millijoules burnt: " << quantity * get_energyYield() << endl;
            }
    };
    
    class Gasoline : public Fuel {
        protected:
            Pounds get_carbonDioxideEmmissionsPerLiter() const override { return 5.07; }
            Seconds get_burnTime() const override { return 10; }
            MilliJoulesPerLiter get_energyYield() const override { return 34.8; }
    
        public:
            Gasoline(Liters volume = 10) : Fuel(volume) {}
    };
    
    class Coal : public Fuel {
        protected:
            Pounds get_carbonDioxideEmmissionsPerLiter() const override { return 2.42; }
            Seconds get_burnTime() const override { return 5; }
            MilliJoulesPerLiter get_energyYield() const override { return 23.9; }
    
        public:
            Coal(Liters volume = 10) : Fuel(volume) {}
    };
    
    class Propane : public Fuel {
        protected:
            Pounds get_carbonDioxideEmmissionsPerLiter() const override { return 3.17; }
            Seconds get_burnTime() const override { return 20; }
            MilliJoulesPerLiter get_energyYield() const override { return 25; }
        public:
            Propane(Liters volume = 10) : Fuel(volume) {}
    };
    

    On the other hand, if you really want to make use of static data members in the derived classes, you can use CRTP (Curiously Recurring Template Pattern), eg:

    class Fuel {
        protected:
            Liters volume {10};
            MilliJoules totalMilliJoules {0};
            
        public:
            Fuel(Liters volume = 10) : volume(volume) {}
    
            virtual void burn() = 0;
    };
    
    template<typename FuelType>
    class FuelT : public Fuel {
        public:
            FuelT(Liters volume = 10) : Fuel(volume) {
                totalMilliJoules = volume * FuelType::energyYield;
            }
    
            void burn() override {
                cout << "Burning fuel..." << endl;
                int quantity = volume < FuelType::burnTime ? volume : FuelType::burnTime;
                Sleep(quantity);
                volume -= quantity;
                totalMilliJoules /= volume * FuelType::energyYield;
                totalEmmissions += FuelType::carbonDioxideEmmissionsPerLiter * quantity;
                cout << "Done burning fuel. Total millijoules burnt: " << quantity * FuelType::energyYield << endl;
            }
    };
    
    class Gasoline : public FuelT<Gasoline> {
        public:
            static const Pounds carbonDioxideEmmissionsPerLiter {5.07};
            static const Seconds burnTime {10};
            static const MilliJoulesPerLiter energyYield {34.8};
    
            Gasoline(Liters volume = 10) : FuelT<Gasoline>(volume) {}
    };
    
    class Coal : public FuelT<Coal> {
        public:
            static const Pounds carbonDioxideEmmissionsPerLiter {2.42};
            static const Seconds burnTime {5};
            static const MilliJoulesPerLiter energyYield {23.9};
    
            Coal(Liters volume = 10) : FuelT<Coal>(volume) {}
    };
    
    class Propane : public FuelT<Propane> {
        public:
            static const Pounds carbonDioxideEmmissionsPerLiter {3.17};
            static const Seconds burnTime {20};
            static const MilliJoulesPerLiter energyYield {25};
    
            Propane(Liters volume = 10) : FuelT<Propane>(volume) {}
    };