-2

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()
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
MatthewR
  • 3
  • 3
  • You're looking for virtual functions. Use virtual functions to access the subclass values. – Drew Dormann May 08 '23 at 19:24
  • Side note: since each of the subclasses is the same structure and behaviour with only different values in the members, inheritance is a bit of a waste. – user4581301 May 08 '23 at 19:30
  • 1
    @MatthewR How is this any different than your [previous question](https://stackoverflow.com/questions/76201885/)? – Remy Lebeau May 08 '23 at 19:31

2 Answers2

2

Your example is too much, hence I will use a simpler one.

The pattern is known as Template method pattern. The name is a bit unfortunate, because it has nothing to do with C++ templates.

struct base {
      virtual int& fuel() = 0;
      void burn(int amount) { fuel() -= amount; }
};

That's already all. The base class declares the virtual method to be implemented by derived classes, and then uses it to compose the actual method (which is not virtual). Derived classes only need to override the virtual method, but the burn() implementation is always that of the base class.

 struct derived : base {
       int fuel_ = 42;
       int& fuel() override { return fuel_; }
 };

 int main() {
       derived d;
       d.burn(5);
 }

PS: You are overusing static members too much. After you get rid of them (explained in this answer), the above might be of some use.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
2

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) {}
};
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Regarding the _CRTP_, maybe it's even worth mentioning the [_Self registering Factory Method_](https://stackoverflow.com/questions/44465134/how-to-self-register-class-instances-using-the-crtp) pattern (just invented the name right now). – πάντα ῥεῖ May 08 '23 at 21:06