How to create uniform interface of C++ classes without virtual methods?
https://softwareengineering.stackexchange.com/questions/422741
-
23-03-2021 - |
题
I have been developing control software in C++. My hardware consists of a microcontroller with an integrated a/d converter and an external on board a/d converter. Both of these a/d converters have different features but there are some commonalities (both of them are capable to offer value of given analog input).
// analog inputs
enum class Input
{
Channel_00,
Channel_01,
Channel_02,
Channel_04,
Channel_05,
Channel_06,
Channel_07
};
// driver of the internal a/d converter
class AdcInt
{
public:
float getValue(Input input);
};
// driver of the external a/d converter
class AdcExt
{
public:
float getValue(Input input);
};
Based on the drivers for the a/d converters I am going to start building the application layer. One of the building stones of my design is the AnalogInput
class. This class is intended to exploit the drivers and offers some additional services e.g. conversion the raw value into the physical units.
class AnalogInput
{
public:
float getConvertedValue();
}
I have been thinking about how to ensure that the AnalogInput
class can operate with both the driver objects (AdcInt
, AdcExt
) which is necessary because the analog inputs can be connected to any of the a/d converters and I need to work with all the analog inputs in uniform manner.
- First idea
My first idea how to do that is based on common interface for all the a/d converter drivers let's say AdcDriver
and defining the virtual
getValue
method as a part of this interface. The AnalogInput
class would then receive pointer to the AdcDriver
interface in its constructor. The drawback of this idea is that the getValue
method is virtual which is unsuitable for usage in the interrupt service routine (which is my requirement).
- Possible solution
Due to the drawback of my first idea I have started to look for another approach how to create common interface AdcDriver
without virtual method. I have found the so called curiously recurring template pattern (CRTP).
template<class T>
class AdcDriver
{
public:
float getValue(Input input)
{
return static_cast<T*>(this)->getValue(input);
}
}
class AdcInt : public AdcDriver<AdcInt>
{
public:
float getValue(Input input)
{
// ... implementation specific for AdcInt
}
}
class AdcExt : public AdcDriver<AdcExt>
{
public:
float getValue(Input input)
{
// ... implementation specific for AdcExt
}
}
template<class T>
class AnalogInput
{
public:
AnalogInput(T& _driver, Input _input_id) : driver(_driver)
{
input_id = _input_id;
}
float getConvertedValue()
{
return convert(driver.getValue());
}
private:
T& driver;
Input input_id;
float convert(float);
}
int main(int argc, char** argv) {
AdcInt internal_adc;
AdcExt external_adc;
AnalogInput<AdcInt> analog_input_01(internal_adc, Input::Channel_01);
AnalogInput<AdcExt> analog_input_02(external_adc, Input::Channel_02);
analog_input_01.getConvertedValue();
analog_input_02.getConvertedValue();
return 0;
}
Do you think that the second approach which I have described above is appropriate solution of my problem? If you don't think so can you recommend me better idea?
解决方案
You don't need AdcDriver
at all. It does nothing. AdcInt
and AdcExt
both expose the same interface.
If you want to have an object that accepts AdxInt
or AdcExt
based on runtime information, you will need virtual somewhere. If not, you can use a simpler template.
class AdcInt
{
public:
float getValue(Input input)
{
// ... implementation specific for AdcInt
}
}
class AdcExt
{
public:
float getValue(Input input)
{
// ... implementation specific for AdcExt
}
}
template<class T>
class AnalogInput
{
public:
AnalogInput(T& _driver, Input _input_id) : driver(_driver), input_id(_input_id)
{
}
float getConvertedValue()
{
return convert(driver.getValue());
}
private:
T& driver;
Input input_id;
float convert(float);
}
int main(int argc, char** argv) {
AdcInt internal_adc;
AdcExt external_adc;
AnalogInput<AdcInt> analog_input_01(internal_adc, Input::Channel_01);
AnalogInput<AdcExt> analog_input_02(external_adc, Input::Channel_02);
analog_input_01.getConvertedValue();
analog_input_02.getConvertedValue();
return 0;
}
This is the same kind of idea as the iterator / algorithm interface in the standard library. There is no common ancestor of std::vector<int>::iterator
and std::deque<int>::iterator
, but they can be used by the same algorithms, because they present the same behaviour.
其他提示
As you intend to use the getValue()
method from the "end of conversion" interrupt, I am going to propose a completely different architecture.
Conceptually, the interrupt handler for the "end of conversion" interrupt of the internal ADC is part of the AdcInt
driver and the corresponding interrupt handler for the external ADC is part of the AdcExt
driver. So, the interrupt handlers should use those drivers without any layer in-between.
Then the driver can call a callback function or call a member of AnalogInput
to inform them that a new value is available.
This would make the classes look something like
class AnalogInput
{
public:
float getConvertedValue() const;
void setRawValue(float aValue);
private:
float raw_value;
};
class AdcInt
{
public:
void registerCallback(Input input_id, AnalogInput* callback);
void handle_interrupt()
{
// determine current input
// read value from ADC
callbacks[current_input]->setRawValue(value);
}
};
class AdcExt
{
public:
void registerCallback(Input input_id, AnalogInput* callback);
void handle_interrupt()
{
// determine current input
// read value from ADC
callbacks[current_input]->setRawValue(value);
}
};
I am not sure, but I think here you could use a pimpl pattern?