3
\$\begingroup\$

I've written some classes to encapsulate the limits given for datasheet parameters, e.g. for the classic 741 op amp:

741 datasheet examples

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:

  1. Any suggestions for better names? I struggled to come up with good names, especially for Range and Parameter, and I'm still not totally happy with what I came up with. I also considered Interval, Limit, or Limits for Range, and Specification for Parameter.
  2. Was my decision to make lowerlimit and upperlimit public the correct one?
  3. Did I handle the implementation of numeric_limits<T>::lowest() correctly?
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Was my decision to make lowerlimit and upperlimit public the correct one?

Since lowerlimit and upperlimit are only written by constructor. you can make then const. So even though they are public access, still safe.

Did I handle the implementation of numeric_limits<T>::lowest() correctly?

You are right. For float, double, they are symmetrical in sign. the lowest for float and double are:

float   -FLT_MAX
double  -DBL_MAX
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.