overloading class method in virtual class situation

I am having difficulty overloading a class method....

I am programming a data collection system. I have an interface class, a base class, and then inherited classes. So for example iAllSensors... AllSensors... AnalogSensor... and the other types of sensors which are special cases of AnalogSensor, such as CurrentLoopSensor. There is also a PulseSensor which just counts pulses but uses inherited member data from AllSensors and AnalogSensor but just accumulates a count of pulses. There is a GetData method needed for all the data. In the case of most sensors, I need to return a float. In the case of the pulse sensors, I need to return an unsigned long. How can I override a virtual method in this scenario. I read that overloading virtual methods is not possible but, with most things CPP, I find it is worth asking if there is a way to accomplish what I am trying to get done.

Below I have sparsed my code down to the following near to illustrate the structure and highlight where I need to override one of the GetData methods.

Any help on an approach to overload the GetData methods would be appreciated.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
class iAllSensors{
    public:
    virtual float GetData(void) =0;    
};


class AllSensors : public iAllSensors
{
protected:
    uint16_t    m_SensorPort;
    
}; // class AllSensors

template <size_t arraysize>
class AnalogSensor : public AllSensors
{
protected:
    double m_DataArray[arraysize];
    uint32_t m_NumberOfSamples = 0;

public:
    AnalogSensor(uint8_t SensorPort)
    {
        m_SensorPort = SensorPort;
    } // end of constructor AnalogSensor

   // forward declarations
    virtual float GetData(void); // return single value of data after math is applied to array
   
}; // class AnalogSensor

template <size_t arraysize>
class CurrentLoopSensor :  public AnalogSensor<arraysize>
{
protected:
   float m_4maValue;
   float m_20maValue;

public:
    // constructor
    CurrentLoopSensor (uint8_t SensorPort, float a4maValue, float a20maValue):
                AnalogSensor<arraysize>(SensorPort)
    {
        m_4maValue = a4maValue;
        m_20maValue = a20maValue;
    }

    // forward declarations
    virtual float GetData(void);

}; // class CurrentLoopSensor : public AnalogSensor


template <size_t arraysize>
class PulseSensor : public AnalogSensor<arraysize>
{
protected:
    unsigned long m_Pulses;

public:
    // constructor
    PulseSensor (uint8_t SensorPort) :
                AnalogSensor<arraysize>(SensorPort)
    {

    }

    // forward declarations
    virtual float GetData(void);
   
}; // class PulseSensor : public AnalogSensor


template <size_t arraysize>
float AnalogSensor<arraysize>::DoMath(void)
// simplified version... returns median
{
   if (m_NumberOfSamples % 2 != 0)
     return (m_DataArray[m_NumberOfSamples / 2]);
   return (m_DataArray[(m_NumberOfSamples - 1) / 2] + m_DataArray[m_NumberOfSamples / 2] / 2.0);
}


template <size_t arraysize>
float AnalogSensor<arraysize>::GetData(void)
{
    return (DoMath()); // this data is a float
}


template <size_t arraysize>
void PulseSensor<arraysize>::AddData(void) // called through interrupt service routine
{
    m_Pulses++;  //  unsigned long rather 
}


template <size_t arraysize>
float PulseSensor<arraysize>::GetData(void)
{
    return (m_Pulses);  // we keep m_Pulses as an unsigned long, we want to return an unsigned long in this version of GetData
}
One of many alternatives is to change the return type of GetData to std::variant<float, unsigned long> or an equivalent.
Overloads of a function with the same name have to differ in their parameter types.

It is not possible to have multiple overloads of a function with the same name and same parameter types that only differ in return type!

(this has nothing to do with virtual functions or inheritance; it is a general rule for overloading)

⎯⎯⎯⎯⎯⎯⎯⎯

You could have separate GetDataAsULong() and GetDataAsFloat() functions. In the base class implementation those functions would throw an "unsupported" exception, requiring the derived class to overwrite the function that is supported by the concrete sensor.

Additionally, you could have a GetDataType() function, returning ULONG or FLOAT, form a suitable enum, to indicate the supported type.

Or, as mbozzi suggested, you could have a single GetData() function with return type std::variant<float, unsigned long>.
Last edited on
I was not familiar with the std::variant<float, unsigned long>. Approach. Thank you for that. Very helpful indeed.
It is not possible to have multiple overloads of a function with the same name and same parameter types that only differ in return type!


A member function type of const, &, && and volatile are also considered to be different from one without (if & or && are used then all must also have & or &&). So without volatile (rarely used for functions) there are 4 possibles. Note that the return types from these need not be the same - and all can be different:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include <iterator>

struct S {
	int f1() & {
		std::cout << "f1\n";
		return -1;
	}

	unsigned f1() const & {
		std::cout << "f1 const\n";
		return 2;
	}

	double f1() && {
		std::cout << "f1 rvalue\n";
		return 3.3;
	}

	float f1() const && {
		std::cout << "f1 const rvalue\n";
		return 4.5;
	}
};

int main() {
	S s;

	s.f1();

	const S sc;

	sc.f1();

	std::move(s).f1();
	std::move(sc).f1();
}



f1
f1 const
f1 rvalue
f1 const rvalue

Last edited on
In the case of most sensors, I need to return a float. In the case of the pulse sensors, I need to return an unsigned long.


Why float and not double?

For pulse, would the max value returned not be able to be represented accurately as a double? If it could, it would make your usage of GetData() much easier. If you have a return value of std::variant (or the poor person's union) then the caller has to determine the type of the returned and deal with it as appropriate.
a great deal of hardware still uses floats, its very common.
you can upcast it, of course. I usually do if the target is a PC, and keep as float if its embedded.


--------
It is not possible to have multiple overloads of a function with the same name and same parameter types that only differ in return type!

you can work around this, eg a couple of namespace and default parameters, but there really isnt a use case where anything you do is more useful or better looking than just adding another unused parameter.
Last edited on
Another way would be to specify the return type as a class template. For an example, consider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <iostream>
#include <typeinfo>

using AT = double;
using PT = unsigned long long;

template<typename R = AT>
class iAllSensors {
public:
	virtual R GetData(void) const = 0;
};

template<typename R = AT>
class AllSensors : public iAllSensors<R> {
protected:
	uint16_t m_SensorPort {};

public:
	virtual R GetData() const = 0;
};

template <size_t arraysize, typename R = AT>
class AnalogSensor : public AllSensors<R> {
protected:
	R m_DataArray[arraysize] {};
	uint32_t m_NumberOfSamples {};

	R DoMath() const ;

public:
	AnalogSensor(uint8_t SensorPort) {
		this->m_SensorPort = SensorPort;
	}

	R GetData(void) const override;

};

template <size_t arraysize, typename R = AT>
class CurrentLoopSensor : public AnalogSensor<arraysize, R> {
protected:
	R m_4maValue {};
	R m_20maValue {};

public:
	CurrentLoopSensor(uint8_t SensorPort, R a4maValue, R a20maValue) :
		AnalogSensor<arraysize, R>(SensorPort), m_4maValue(a4maValue), m_20maValue(a20maValue) {}

	R GetData(void) const override;
};

template <size_t arraysize, typename R = PT>
class PulseSensor : public AnalogSensor<arraysize, R> {
protected:
	R m_Pulses {};

	void AddData();

public:
	PulseSensor(uint8_t SensorPort) : AnalogSensor<arraysize, R>(SensorPort) {}

	R GetData(void) const override;
};

template <size_t arraysize, typename R>
R AnalogSensor<arraysize, R>::DoMath() const {
	if (m_NumberOfSamples % 2 != 0)
		return m_DataArray[m_NumberOfSamples / 2];

	return m_DataArray[(m_NumberOfSamples - 1) / 2] + m_DataArray[m_NumberOfSamples / 2] / 2.0;
}

template <size_t arraysize, typename R>
R AnalogSensor<arraysize, R>::GetData() const {
	return DoMath();
}

template <size_t arraysize, typename R>
void PulseSensor<arraysize, R>::AddData() {
	++m_Pulses;
}

template <size_t arraysize, typename R>
R PulseSensor<arraysize, R>::GetData() const {
	return m_Pulses;
}

int main() {
	AnalogSensor<2> as{1};
	PulseSensor<3> ps{2};

	auto ar { as.GetData() };
	auto ap { ps.GetData() };

	std::cout << "type of as.GetData() is " << typeid(ar).name() << '\n';
	std::cout << "type of ps.getData() is " << typeid(ap).name() << '\n';
}


which displays:


type of as.GetData() is double
type of ps.getData() is unsigned __int64

And, finally, double functions can return integers precisely up to a rather large value. If your int functions are returning relatively small integers that fit this, or if you do not mind being slightly off in your result, you can save a lot of trouble by using double for all of it. If you need precision, you will have use one of these ideas. Templates is the 'right' way, but the code is so small, one of the tricks seems to save a great deal of infrastructure code for a simple need.
I asked that question several posts above... :) :)
At the risk of stating the obvious, a downside of making the interface class a template is that there is no longer just one interface class.
Last edited on
Thanks to all of you!

seeplus: your approach is exactly what I am looking for. It turns out that I will also need to return other types and creating the type class template exactly nails what I need. Thank you.

As to others questions about why not use double for everything, this application will be using small burst satellite communications so I have to optimize the size of the data sent.

Also, regarding the complexity of adding the class template type, it is really not a big deal. The example I posted is dramatically simplified from the actual application and existing code. As I mentioned in the post, I tried to provide only enough here to provide adequate context and example of the code.
seeplus: I created the interface class of course so I could step through the instances of the sensors. I normally would set this up, (referring to your code above), this way:
1
2
3
4
iAllSensors *const ListOfSensors[] =
{
   &AnalogSensor, &PulseSensor
}


Now trying it that way, I get the following error:
invalid use of template-name 'iAllSensors' without an argument list

I have tried many approaches (for over an hour now) to address that error but seem to be missing a fundamental concept(s) here to get it right. Could you guide me to a reference on this or otherwise help me through the knot hole?

Thank you.
I alluded to this above:
A downside of making the interface class a template is that there is no longer just one interface class.


A class template is not a class. Instead it is a purely compile-time construct that defines a family of classes.
iAllSensors<float> is a class.
iAllSensors<unsigned long> is also a class.
These classes are not related at all.

This moves the problem. Now you need two lists:
1
2
iAllSensors<float> *const list_of_float_sensors[] = { &AnalogSensor; };
iAllSensors<unsigned long> *const list_of_ul_sensors[] = { &PulseSensor; };
Mbozzi: thank you. I can see some utility in that. I also see utility in having all the sensors in on list that I could loop through. Maybe I ought to try harder with the std::variant approach…
OK. Using std::variant, consider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <iostream>
#include <typeinfo>
#include <variant>

using AT = double;
using PT = unsigned long long;
using R = std::variant<AT, PT>;

class iAllSensors {
public:
	virtual R GetData(void) const = 0;
};

class AllSensors : public iAllSensors {
protected:
	uint16_t m_SensorPort {};
};

template <size_t arraysize>
class AnalogSensor : public AllSensors {
protected:
	AT m_DataArray[arraysize] {};
	uint32_t m_NumberOfSamples {};

	AT DoMath() const;

public:
	AnalogSensor(uint8_t SensorPort) {
		this->m_SensorPort = SensorPort;
	}

	R GetData() const override;
};

template <size_t arraysize>
class CurrentLoopSensor : public AnalogSensor<arraysize> {
protected:
	AT m_4maValue {};
	AT m_20maValue {};

public:
	CurrentLoopSensor(uint8_t SensorPort, AT a4maValue, AT a20maValue) :
		AnalogSensor<arraysize, R>(SensorPort), m_4maValue(a4maValue), m_20maValue(a20maValue) {}

	R GetData() const override;
};

template <size_t arraysize>
class PulseSensor : public AnalogSensor<arraysize> {
protected:
	PT m_Pulses {};

	void AddData();

public:
	PulseSensor(uint8_t SensorPort) : AnalogSensor<arraysize>(SensorPort) {}

	R GetData() const override;
};

template <size_t arraysize>
AT AnalogSensor<arraysize>::DoMath() const {
	//if (m_NumberOfSamples % 2 != 0)
		//return m_DataArray[m_NumberOfSamples / 2];

	//return m_DataArray[(m_NumberOfSamples - 1) / 2] + m_DataArray[m_NumberOfSamples / 2] / 2.0;
	return 3.0;
}

template <size_t arraysize>
R AnalogSensor<arraysize>::GetData() const {
	return DoMath();
}

template <size_t arraysize>
void PulseSensor<arraysize>::AddData() {
	++m_Pulses;
}

template <size_t arraysize>
R PulseSensor<arraysize>::GetData() const {
	return m_Pulses;
}

int main() {
	AnalogSensor<2> as { 1 };
	PulseSensor<3> ps { 2 };

	const iAllSensors* ListOfSensors[] { &as, &ps };

	for (const auto& s : ListOfSensors)
		std::visit([](auto&& arg) {std::cout << "type of .GetData() is " << typeid(arg).name() << ". Data is " << arg << '\n'; }, s->GetData());
}



type of .GetData() is double. Data is 3
type of .GetData() is unsigned __int64. Data is 0


You may find this article of interest:
https://www.cppstories.com/2020/04/variant-virtual-polymorphism.html/
Last edited on
seeplus: Thank you! I very much appreciate your assistance, the code you posted, and the referred article. This code is simply elegant and exactly what I was seeking. While it might look complex in the context of the two methods and two objects constructed, when put into play with the many methods and objects (sensors) I have in my code/system, this dramatically simplifies many things for me. Now... if I can just get Visual Studio Code/PlatformIO to compile it for the processors I am using... but I will get through that also. Again, many thanks!
My code compiles OK with VS2022 as C++/preview for X64.

std::variant/std::visit needs at least a C++17 compiler...
Thanks again. I needed to just adjust some of my platformio.ini settings and indeed the code is compiling for me now. I still am trying to get c/c++ intellisense to not show namespace errors for std::variant and std::visit even though I have the default set to c++17... but I will get that sorted out.
Topic archived. No new replies allowed.