1

We're experimenting with having bounded primitive types in our project where we can check that instances of derived classes have a data value within valid range (min and max member variables, protected in base class) for that derived class.

My question is, is there a way I can statically initialise the derived class's min and max variables, just once per derived class, rather than every time I instantiate the derived class.

In C# this would be in a static initialisation block, but I'm not sure how to do it in C++.

I know I could initialise them in the derived class constructor, but this seems wasteful to do it each time.

I think I'm looking for abstract data members, declared in a base class, but subequently defined statically in derived classes.

class BoundedFloat
{
public:
    BoundedFloat(const float v) : Value(v) {}

    // some common methods that use Min and Max
    // prefer to implement in base class rather than in each derived class

    bool withinBounds();
    bool breachedLowerThreshold();
    bool breachedUupperThreshold();


protected:
    const float Min;
    const float Max;
    float Value;
}


bool BoundedFloat::withinBounds()
{
    return ((Value >= Min) && (Value<= Max));
}

bool BoundedFloat::breachedLowerThreshold()
{
    return (Value < Min);
}

bool BoundedFloat::breachedUupperThreshold()
{
    return (Value > Max);
}

class Temperature : public BoundedFloat
{
public:
   Temperature(const float v) : BoundedFloat(v) {}

   // seems wasteful to do this each time, when min and max only need 
   // initialised once per derived class
   // Temperature(const float v) : BoundedFloat(v, -40.0f, 80.0f)

   // statically initialise Temperature's Min and Max in base class here somehow?

private:
    // I know this is wrong, but it indicates the functionality I'm looking for.
    override static float Min;
    override static float Max;
}
Artie Leech
  • 347
  • 2
  • 14
  • It's not possible to do in C++. If you want to initialize non-static members, it has to be done each construction. – Some programmer dude Apr 03 '19 at 12:37
  • its not really adding much (if any) overhead, if the members are non-static they need to be initialized for each instance anyhow. Perhaps you want to make them `static` ? – 463035818_is_not_an_ai Apr 03 '19 at 12:44
  • @user463035818, good point. Yes, I do want them static in the derived class, and defined once in each derived class. But I want them initially declared in the base class, where I'd put common methods that use them. – Artie Leech Apr 03 '19 at 12:52

4 Answers4

2

No, you can't.

Moreover static members in base class will not work anyways if you want different Min/Max values for different derived classes. (If you don't you can initialize in the base statically). Each derived class will share the same instances of Min and Max. So, once you have changed / initialized (non statically) values in derived class they will be changed for the base class and other derived classes as well. See this link.

I think you need Min, Max virtual functions. Like this:

class BoundedFloat{
public:
    BoundedFloat(const float v) : Value(v) {}
    virtual float Min() { return -10; }
    virtual float Max() { return 10; }
protected:
    float Value;
}

class Temperature : public BoundedFloat
{
public:
    Temperature(const float v) : BoundedFloat(v) {}
    virtual float Min() override { return -40; }
    virtual float Max() override { return 80; }
}
MojoRisin
  • 35
  • 5
  • thanks for your response. I don't necessarily want to expose Min and Max like this. But I do want them declared in the base class, along with methods that use them, but _defined_ in each deriving class with values appropriate for that particular derived class. – Artie Leech Apr 03 '19 at 13:34
2

I don't think there's any way to do precisely what you want, except with a bit of redesign. Remember that for a member not marked static, every object of the class type has its own copy of those members. If you have multiple objects of type Temperature, each has its own Min and Max, so all of those need to be initialized at some point. Also, the base class BoundedFloat can't know which of several possible derived classes it's being used in, unless you somehow let it know, and just passing in the min and max values is probably the simplest way of "letting it know".

But the "waste" associated with this (coding and maintenance effort? CPU runtime? memory used by the member variables?) doesn't seem all that large to me.

Still, there are a few redesign options you might want to consider:

  • Change the data from members to virtual getter functions.

    class BoundedFloat
    {
    public:
        BoundedFloat(const float v) : Value(v) {}
        virtual ~BoundedFloat() = default;
    
    protected:
        BoundedFloat(const BoundedFloat&) = default;
        BoundedFloat(BoundedFloat&&) = default;
        BoundedFloat& operator=(const BoundedFloat&) = default;
        BoundedFloat& operator=(BoundedFloat&&) = default;
    
        float Min() const = 0;
        float Max() const = 0;
        float Value;
    };
    
    class Temperature : public BoundedFloat
    {
    public:
       Temperature(const float v) : BoundedFloat(v) {}
    
       float Min() const override { return -40.0f; }
       float Max() const override { return 80.0f; }
    };
    

    Now each derived class is solely responsible for its min/max values. Though this approach has its own costs, which might or might not be important to you.

  • The Flyweight Pattern

    class BoundedFloat
    {
    protected:
        struct Data {
            float Min;
            float Max;
        };
    
        BoundedFloat(const float v, const Data& data_in)
            : Value(v), data(data_in) {}
    
    protected:
        BoundedFloat(const BoundedFloat&) = default;
        BoundedFloat(BoundedFloat&&) = default;
        BoundedFloat& operator=(const BoundedFloat&) = default;
        BoundedFloat& operator=(BoundedFloat&&) = default;
    
        float Value;
        const Data& data;
    };
    
    class Temperature : public BoundedFloat
    {
    public:
        Temperature(const float v) : BoundedFloat(v, my_data) {}
    
    private:
        static const Data my_data;
    };
    
    const Temperature::Data Temperature::my_data{ -40.0f, 80.0f };
    

    Here there's just one initialization of the derived class data values, and they're almost naturally "in" the base class; just you need to call them data.Min and data.Max. This pattern is commonly used when there's a lot of data which is constant per-class, but you can certainly use it with just a few pieces of data too. The coding cost here is comparable to passing in individual base values, or maybe less, and the program's runtime costs might go up or down a bit, depending.

aschepler
  • 70,891
  • 9
  • 107
  • 161
  • Thanks for your suggestion, @aschepler. By "waste" I meant that it shouldn't be necessary to supply the Min and Max values for each class instantiation, and could look misleading to less-experienced developers. To me, it just seems more correct that the Min/Max initialisation takes place once, statically, per Derived class, leaving the per-instance constructor to just initialise the Value itself. – Artie Leech Apr 04 '19 at 07:44
1

You can kind of get what you want using a template for the base class and using the derived type as the type for the base class template. This is used to enable static polymorphism and is know as the Curiously recurring template pattern (CRTP) but in this case I am using it just as a tag, so each derived class can have it's own set of static data members in the base class. Changing the base class to

template<typename Derived>
class BoundedFloat
{
public:
    BoundedFloat(const float v) : Value(v) {}
    static float getMax() { return Max; }
    static float getMin() { return Min; }
protected:
    static const float Min;
    static const float Max;
    float Value;
};

Means that BoundedFloat<Derived> has it's own Min and Max. So we derive from BoundedFloat like

class Temperature : public BoundedFloat<Temperature>
{
public:
   Temperature(const float v) : BoundedFloat(v) {}
};

And then we have to define the values we want like

template<>
const float BoundedFloat<Temperature>::Min = -40.0f;
template<>
const float BoundedFloat<Temperature>::Max = 80.0f;

You can see the code working in this live example


Do note that if you wan to use the base class static member in your derived class you need to do soin out of line defenitions. This has to be done like that because if the functions body was in the derived class then it would need to implicitly instantiate static member and we can't have that happen since the explicit instantiation is after that class.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • Or, you might skip the member declarations in the base class, require each derived class to have those member names, and code in the base class can do `static_cast(this)->Min`. – aschepler Apr 03 '19 at 13:08
  • This looks really close to what I want, @NathanOliver. However, I wouldn't be looking to have methods like getMax in each derived class. I'd want common methods using Min, Max to be in the base class - I've updated the original post to show what I mean. – Artie Leech Apr 03 '19 at 13:28
  • @ArtieLeech I've updated the answer with how that would work/look like. – NathanOliver Apr 03 '19 at 13:40
  • @NathanOliver, should the first line above say "template", rather than Derived? Because this would be the 'base' bounded float class from which the rest Derive? – Artie Leech Apr 04 '19 at 08:39
  • @ArtieLeech I call it `Derived` because the type that is used there should be derived from `BoundedFloat` – NathanOliver Apr 04 '19 at 12:26
  • But the line "template" is above the base class itself - BoundedFloat, from which the other classes derive. – Artie Leech Apr 04 '19 at 12:32
  • @ArtieLeech Yes, `BoundedFloat` derives from the template type so it is a base class. I call it `Derived` though because the type you pass to it should be derived from `BoundedFloat`. So `Temperature` is the base class of `BoundedFloat` and is also derived from `BoundedFloat`. It's weird but in my mind it makes sense to call it `Derived` so I remember the base class must derive from the class doing the inheriting. – NathanOliver Apr 04 '19 at 12:37
  • @NathanOliver, okay - so my solution below, based on yours above has the notion of BoundedFloat as the base class, with Temperature and SensorReading both deriving from BoundedFloat. Does that look right to you? – Artie Leech Apr 04 '19 at 12:39
0

Based on the suggestion by @NathanOliver, here's what I've currently got.

It satisfies what I was looking for:

  • common values declared as const in base class
  • common values subsequently defined in derived classes, only once, which keeps the constructor cleaner
  • common methods in the base class that can use the derived const values

Is there anything badly wrong with this approach?

BaseTypes.h

    namespace Common
    {
        template<typename Base>
        class BoundedFloat
        {
        public:
            BoundedFloat(const float v) : Value(v) {}

            // Common methods that read Min, Max, Value

            void displaySummary() const;
            bool withinBounds() const;
            // bool breachedLowerThreshold() const;
            // bool breachedUpperThreshold() const

        protected:
            static const float Min;
            static const float Max;
            float Value;
        };

        template <typename Base>
        void BoundedFloat<Base>::displaySummary() const
        {
            std::cout <<
                "Min = " << Min << ", "
                "Max = " << Max << ", "
                "Value = " << Value << std::endl;

            if (withinBounds())
            {
                cout << "within bounds" << endl;
            }
            else
            {
                cout << "outwith bounds" << endl;
            }
        }

        template <typename Base>
        bool BoundedFloat<Base>::withinBounds() const
        {
            return ((Value >= Min) && (Value <= Max));
        }
    }

SensorReading.h

#pragma once

#include "BaseTypes.h"

namespace Common
{
    class SensorReading: public BoundedFloat<SensorReading>
    {
    public:
        SensorReading(const float v) : BoundedFloat(v) {}
    };

    template<>
    const float BoundedFloat<SensorReading>::Min = -2.0f;
    template<>
    const float BoundedFloat<SensorReading>::Max = 7.5f;
}

Temperature.h

#pragma once

#include "BaseTypes.h"

namespace Common
{
    class Temperature : public BoundedFloat<Temperature>
    {
    public:
        Temperature(const float v) : BoundedFloat(v) {}
    };

    template<>
    const float BoundedFloat<Temperature>::Min = -40.0f;
    template<>
    const float BoundedFloat<Temperature>::Max = 80.0f;
}

main.cpp

int main()
{
    Temperature temperature{ 80.0f };
    temperature.displaySummary();

    SensorReading sensorReading{ 72.5f };
    sensorReading.displaySummary();

    return 0;
}
Artie Leech
  • 347
  • 2
  • 14