I've written some classes to encapsulate the limits given for datasheet parameters, e.g. for the classic 741 op amp:
As the above example datasheet snippet shows, there are some challenges for representing the values in the "min", "typ" (typical), and "max" columns -- including cases where there is no typical value, and single-sided limits (i.e. there is no minimum and/or no maximum).
Additionally, I need to support limits which require exact integer values (e.g. for representing the limits for an analog-to-digital converter (ADC), which outputs discrete values). Consequently, I've written my classes as templates to support both integral and floating point types (the former is better suited to digital parameters, the latter to analog).
I also provided support for guardbands for tightening the limits in a test/quality control environment.
My compiler at work is the aging VS2005 compiler, so I don't have some of the useful C++11 std
utilities like std::numeric_limits<T>::lowest()
. I've implemented a few that I need:
/** \file mycpp11std.h */
#include <limits> // std::numeric_limits
namespace mycpp11std {
/** Implementation of enable_if from C++11 `std`, taken from
http://en.cppreference.com/w/cpp/types/enable_if.
*/
template<bool B, typename T = void>
struct enable_if {};
/** Specialization of enable_if from C++11 `std` for `B = true`.
Taken from http://en.cppreference.com/w/cpp/types/enable_if.
*/
template<class T>
struct enable_if<true, T> { typedef T type; };
/** Implementation of numeric_limits<T>::lowest from C++11 `std`. Returns the most
negative value that can be represented by the type T. */
template <typename T, typename Enable = void>
struct numeric_limits {
static T lowest() {
return -std::numeric_limits<T>::max();
}
};
/** Specialization of numeric_limits<T>::lowest for integers. */
template <typename T>
struct numeric_limits<T,
typename enable_if<std::numeric_limits<T>::is_integer >::type> {
static T lowest() {
return std::numeric_limits<T>::min();
}
};
} // end namespace mycpp11std
On to the actual class templates:
/** \file range.h */
#include <limits> // std::numeric_limits
#include "mycpp11std.h"
namespace range {
/** \brief Encapsulates a range of acceptable values by setting a lower and upper
limit.
The lower limit may be greater than the upper limit. Single-sided limits can also
be represented by using the `static` functions `no_minimum()` and `no_maximum()`,
which return the minimum and maximum values, respectively, that can be represented
by the type `T`. Guardbands are also supported.
*/
template <typename T> class Range {
T lgb; /**< \brief the lower guardband */
T ugb; /**< \brief the upper guardband */
/** \brief Make sure lower guardband is positive */
void validate_lgb() { lgb = std::abs(lgb); }
/** \brief Make sure upper guardband is positive */
void validate_ugb() { ugb = std::abs(ugb); }
public:
T lowerlimit; /**< \brief the specified lower limit */
T upperlimit; /**< \brief the specified upper limit */
/** \brief Set `lowerlimit` to this value in order to indicate that
there is no lower limit
*/
static T no_minimum() {
/* std::numeric_limits<T>::min() is the smallest value, not necessarily
the most negative. */
return mycpp11std::numeric_limits<T>::lowest();
}
/** \brief Set `upperlimit` to this value in order to indicate that
there is no upper limit
*/
static T no_maximum() {
return std::numeric_limits<T>::max();
}
/** \brief Construct a `Range` with lower and upper limits, and zero
guardbands. */
Range(T lowerlimit, T upperlimit) :
lowerlimit(lowerlimit), upperlimit(upperlimit), lgb(0), ugb(0) {}
/** \brief Construct a `Range` with lower and upper limits as well as
guardbands. */
Range(T lowerlimit, T upperlimit, T lowerguardband, T upperguardband) :
lowerlimit(lowerlimit), upperlimit(upperlimit),
lgb(lowerguardband), ugb(upperguardband) {
validate_lgb();
validate_ugb();
}
/** \brief The minimum passing value, defined as the lower limit
plus the lower guardband. */
T min() const { return lowerlimit + lgb; }
/** \brief The maximum passing value, defined as the upper limit
minus the upper guardband. */
T max() const { return upperlimit - ugb; }
/** \brief Returns the currently set lower guardband value */
T lowerguardband() const { return lgb; }
/** \brief Returns the currently set upper guardband value */
T upperguardband() const { return ugb; }
/** \brief Sets the lower guardband.
\param[in] lgb the value to set the lower guardband to. The lower guardband
will be set to the absolute value of `lgb` if it is not positive.
*/
void lowerguardband(T lgb) {
this->lgb = lgb;
validate_lgb();
}
/** \brief Sets the upper guardband.
\param[in] ugb the value to set the upper guardband to. The upper guardband
will be set to the absolute value of `ugb` if it is not positive.
*/
void upperguardband(T ugb) {
this->ugb = ugb;
validate_ugb();
}
};
/** \brief Encapsulates a range of acceptable values by setting a lower and upper
limit, as well as a typical value for a parameter.
*/
template <typename T> class Parameter : public Range<T> {
public:
T typical; /**< \brief the typical value of the Parameter */
/** \brief Construct a `Parameter` with lower and upper limits and a typical
value, but zero guardbands.
*/
Parameter(T lowerlimit, T typical, T upperlimit) :
Range(lowerlimit, upperlimit), typical(typical) {}
/** \brief Construct a `Parameter` with lower and upper limits and a typical
value, as well as guardbands.
*/
Parameter(T lowerlimit, T typical, T upperlimit,
T lower_guardband, T upper_guardband) :
Range(lowerlimit, upperlimit, lower_guardband, upper_guardband),
typical(typical) {}
};
/** \brief Determines if `value` is within the (possibly guardbanded) limits
(inclusive) set by `range`.
*/
template <typename T> bool pass(T value, const Range<T>& range) {
return (value >= range.min()) && (value <= range.max());
}
} // end namespace range
The Range
class doesn't need to do sanity checking on the limits (e.g. to ensure lowerlimit <= upperlimit
) so I left those members public
to avoid having to implement pointless getters and setters. I did make the guardband members lgb
and ugb
private so that I could ensure they are positive (which is necessary to make sure the guardbands always tighten the limits). The Range
member functions min()
and max()
provide the (possibly guardbanded) limits.
To support single-sided limits, I provided the static
member functions no_minimum()
and no_maximum()
, and this is where I needed to implement a few C++11 utilities. The idea is to set lowerlimit
and/or upperlimit
to the lowest or maximum value, respectively, that could be represented by the type T
.
The Parameter
class just inherits from Range
and adds the public
member typical
. There's no meaningful value for typical
for datasheet parameters which don't have a typical value specified, so such parameters would use Range
. Datasheet parameters which do have a typical value would use Parameter
.
I've also provided a function template pass()
to determine if a value is within the limits of a Range
or Parameter
.
Here's some code to demonstrate the use of Range
and Parameter
:
#include <iostream>
#include <fstream>
#include "range.h"
using namespace range;
void check_guardbands(std::ostream& os) {
Parameter<double> Isc(10, 25, 35);
os << "Isc guardbands: " << Isc.lowerguardband() << " and " << Isc.upperguardband() << "\n";
os << "Isc min/max: " << Isc.min() << " and " << Isc.max() << "\n";
Isc.lowerguardband(-1); Isc.upperguardband(-1);
os << "New Isc guardbands: " << Isc.lowerguardband() << " and " << Isc.upperguardband() << "\n";
os << "New Isc min/max: " << Isc.min() << " and " << Isc.max() << "\n";
os << "Isc typical: " << Isc.typical << "\n";
os << "\n";
}
void check_singlesided(std::ostream& os) {
Parameter<int> CMRR(80, 95, Parameter<int>::no_maximum());
Parameter<int> Power(Parameter<int>::no_minimum(), 80, 150);
Range<unsigned int> Power_hot(Range<unsigned int>::no_minimum(), 135);
Parameter<double> Isupply(Parameter<double>::no_minimum(), 1.7, 2.8);
os << "CMRR (int type, no max):\n";
os << "minimum = " << CMRR.min() << ", ";
os << "typical = " << CMRR.typical << ", ";
os << "maximum = " << CMRR.max() << "\n";
os << "\n";
os << "Power consumption, ambient (int type, no min):\n";
os << "minimum = " << Power.min() << ", ";
os << "typical = " << Power.typical << ", ";
os << "maximum = " << Power.max() << "\n";
os << "\n";
os << "Power consumption, hot (unsigned int type, no min):\n";
os << "minimum = " << Power_hot.min() << ", ";
os << "maximum = " << Power_hot.max() << "\n";
os << "\n";
os << "Isupply (double type, no minimum):\n";
os << "minimum = " << Isupply.min() << ", ";
os << "typical = " << Isupply.typical << ", ";
os << "maximum = " << Isupply.max() << "\n";
os << "\n";
}
void check_pass(std::ostream& os) {
const Range<double> Isc_temp(10, 40);
const Range<int> zero(0, 0);
Parameter<double> Isc(10, 25, 35);
Parameter<double> Isupply(Parameter<double>::no_minimum(), 1.7, 2.8);
Parameter<int> CMRR(80, 95, Parameter<int>::no_maximum());
os << "Testing pass():\n";
double val = 38;
os << val << " is between " << Isc_temp.min() << " and " << Isc_temp.max()
<< "?: " << pass<double>(val, Isc_temp) << "\n";
os << val << " is between " << Isc.min() << " and " << Isc.max()
<< "?: " << pass<double>(val, Isc) << "\n";
val = 5;
os << val << " is between " << Isc.min() << " and " << Isc.max()
<< "?: " << pass<double>(val, Isc) << "\n";
val = 10;
os << val << " is between " << Isc.min() << " and " << Isc.max()
<< "?: " << pass<double>(val, Isc) << "\n";
val = 2.81;
os << val << " is between " << Isupply.min() << " and " << Isupply.max()
<< "?: " << pass<double>(val, Isupply) << "\n";
val = 0.1;
os << val << " is between " << Isupply.min() << " and " << Isupply.max()
<< "?: " << pass<double>(val, Isupply) << "\n";
val = 0;
os << val << " is between " << Isupply.min() << " and " << Isupply.max()
<< "?: " << pass<double>(val, Isupply) << "\n";
val = -1e3;
os << val << " is between " << Isupply.min() << " and " << Isupply.max()
<< "?: " << pass<double>(val, Isupply) << "\n";
val = 80;
os << val << " is between " << CMRR.min() << " and " << CMRR.max()
<< "?: " << pass<int>((int)val, CMRR) << "\n";
val = 0.1;
os << val << " (double cast to int) is between " << zero.min() << " and " << zero.max()
<< "?: " << pass<int>((int)val, zero) << "\n";
val = 0;
os << val << " (double cast to int) is between " << zero.min() << " and " << zero.max()
<< "?: " << pass<int>((int)val, zero) << "\n";
int intval = 0;
os << intval << " (int type) is between " << zero.min() << " and " << zero.max()
<< "?: " << pass<int>(intval, zero) << "\n";
os << "\n";
}
int main() {
std::fstream fs("output.txt");
std::ostream& os = fs; // or std::cout
os << std::boolalpha;
check_guardbands(os);
check_singlesided(os);
check_pass(os);
fs.close();
return 0;
}
The output is:
Isc guardbands: 0 and 0 Isc min/max: 10 and 35 New Isc guardbands: 1 and 1 New Isc min/max: 11 and 34 Isc typical: 25 CMRR (int type, no max): minimum = 80, typical = 95, maximum = 2147483647 Power consumption, ambient (int type, no min): minimum = -2147483648, typical = 80, maximum = 150 Power consumption, hot (unsigned int type, no min): minimum = 0, maximum = 135 Isupply (double type, no minimum): minimum = -1.79769e+308, typical = 1.7, maximum = 2.8 Testing pass(): 38 is between 10 and 40?: true 38 is between 10 and 35?: false 5 is between 10 and 35?: false 10 is between 10 and 35?: true 2.81 is between -1.79769e+308 and 2.8?: false 0.1 is between -1.79769e+308 and 2.8?: true 0 is between -1.79769e+308 and 2.8?: true -1000 is between -1.79769e+308 and 2.8?: true 80 is between 80 and 2147483647?: true 0.1 (double cast to int) is between 0 and 0?: true 0 (double cast to int) is between 0 and 0?: true 0 (int type) is between 0 and 0?: true
Some specific questions:
- Any suggestions for better names? I struggled to come up with good names, especially for
Range
andParameter
, and I'm still not totally happy with what I came up with. I also consideredInterval
,Limit
, orLimits
forRange
, andSpecification
forParameter
. - Was my decision to make
lowerlimit
andupperlimit
public the correct one? - Did I handle the implementation of
numeric_limits<T>::lowest()
correctly?