I want to perform a set of tests in my code that are similar but change only depending on a parameter.
I could write this using a switch statement:
bool doTest(EnumSensorFamily family, const StructSensorProposal& proposed)
{
switch (family)
{
case FAM1:
return (ExpectedFam1 == proposed.Fam1SensorId);
break;
case FAM2:
return (ExpectedFam2 == proposed.Fam2SensorId);
break;
case FAM3:
return (ExpectedFam3 == proposed.Fam3SensorId);
break;
default:
ERROR ("Unexpected family");
return false;
}
}
I was thinking of doing this with template specialisations
template <EnumSensorFamily family>
bool doTest(const StructSensorProposal& proposed);
template<>
bool doTest<FAM1> (const StructSensorProposal& proposed)
{
return (ExpectedFam1 == proposed.Fam1SensorId);
}
template<>
bool doTest<FAM2> (const StructSensorProposal& proposed)
{
return (ExpectedFam2 == proposed.Fam2SensorId);
}
template<>
bool doTest<FAM3> (const StructSensorProposal& proposed)
{
return (ExpectedFam3 == proposed.Fam3SensorId);
}
Is there any benefit of doing this apart from avoiding a switch statement containing near identical cases?
Ideally I would like to be able to write single method to reduce the maintenance overhead.
thanks
Building off Andrew's answer...
Note that the EnumSensorFamily family
must be known at compile time. If it is not known until run time, then you'll have to write a switch
to choose the template, putting you back where you started.
Another way to do this is with the Traits pattern:
template <EnumSensorFamily family>
struct SensorTraits;
template <>
struct SensorTraits<FAM1>
{
const EnumSensorFamily kFamilyID = ExpectedFam1;
};
template <>
struct SensorTraits<FAM2>
{
const EnumSensorFamily kFamilyID = ExpectedFam2;
};
template <>
struct SensorTraits<FAM3>
{
const EnumSensorFamily kFamilyID = ExpectedFam3;
};
template <EnumSensorFamily family>
bool doTest(const StructSensorProposal& proposed)
{
return (SensorTraits<family>::kFamilyID == proposed.Fam1SensorId);
}
If you try to use doTest
with a sensor family that lacks a traits specialization, you get a compile error. Also note that you never instantiate a traits object, you just use its definitions.
This lets you reuse constants, typedefs, whatever in several functions. Additionally, adding a new family does not involve combing through all the code looking for every switch
statement that cares. All you have to do is create a new SensorTraits
specialization.
EDIT: You can make the field dependent on the sensor family with a pointer to member:
template <>
struct SensorTraits<FAM1>
{
const EnumSensorFamily kFamilyID = ExpectedFam1;
int StructSensorProposal::*proposalField = &StructSensorProposal::fam1field;
};
// ...
template <EnumSensorFamily family>
int getProposedField(const StructSensorProposal& proposed)
{
return proposed.*SensorTraits<family>::proposalField;
}
You can also put in, say, a typedef
for the sensor's data type:
template <>
struct SensorTraits<FAM1>
{
const EnumSensorFamily kFamilyID = ExpectedFam1;
typedef uint16_t data_type;
data_type StructSensorProposal::*proposalField = &StructSensorProposal::fam1field;
};
// ...
template <EnumSensorFamily family>
SensorTraits<family>::data_type getProposedField(const StructSensorProposal& proposed)
{
return proposed.*SensorTraits<family>::proposalField;
}
I haven't tested these; you might need a const
or static
in there.
The benefit is a very small potential performance improvement if the compiler fails to optimize the switch
properly (i.e. if it doesn't generate the same code as for the template solution, which is doable for a modern, inlining compiler). Of course only if family
is a compile-time constant - otherwise templates are not applicable and the best optimization that the compiler could find for the switch
would be a computed jump or a jump table.
If your code always looks like return (ExpectedFamN == proposed.FamNSensorId);
, I'd rather use arrays for the expected values and the sensor IDs and index those based on the family
.
It is impossible to use templates in the following case:
const EnumSensorFamily familyCompileTime = FAM3; // Compile time constant
EnumSensorFamily family = GetFimilyInRunTime(); // Run time variable
doTest1(family, proposed); // ok
doTest2<family>(proposed); // error;
doTest2<familyCompileTime >(proposed); // OK;
Well in a way... you are moving processing away from run time to compile time. The compiler will create the functions and use them accordingly instead of at run time having to go through the switch statement.
Also, it will result in cleaner code.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With