A couple of years ago I wrote a pair of class templates to encapsulate specifications which have one or more of a minimum, typical, and/or maximum value, e.g. on a datasheet like this one for the 741 op amp:
That code required me to re-invent the wheel a bit since I was using an old compiler, and I had not discovered boost::optional
/std::optional
yet. I've re-written that code to use a single class template called Spec
, C++11 and C++14 features, and boost::optional
or std::optional
. I've also included some additional utilities (inserting a Spec
into a std::ostream
, clamping Spec
values, computing spec limits based on a value and a % tolerance, etc.). I have not added yet support for guardbands as in the previous implementation, but I plan to do so later.
Here is the implementation using boost::optional
. My compiler is Visual Studio 2017 (which can use C++17's std::optional
), but I'm only using C++14 features in the project this will be used in. I've provided commented-out code below to use std::optional
instead:
#ifndef SPEC_H
#define SPEC_H
#include <ostream>
#include <stdexcept>
// To use std::optional, if supported by compiler
//#include <optional>
// To use boost::optional
#include <boost/none.hpp> // boost::none_t
#include <boost/optional.hpp>
// If using std::optional
/*typedef std::nullopt_t nullspec_type;
inline constexpr nullspec_type nullspec = std::nullopt;*/
// Using boost::optional
/** \brief Type definition for an empty class type which indicates that a Spec value is undefined. */
typedef boost::none_t nullspec_type;
/** \brief A constant which can be used as an argument to a Spec constructor with a nullspec_type
parameter.
*/
const nullspec_type nullspec = boost::none;
/** \brief Exception class for attempts to access non-existent Spec values. */
class bad_spec_access : public std::logic_error {
public:
/** \brief Constructs a Spec bad access error */
bad_spec_access(const std::string& s) : std::logic_error(s) {}
};
/** \brief Encapsulates the specified minimum, typical, and maximum value of a quantity.
The minimum, typical value, and/or maximum value may all be left undefined (but at least one
must be defined). This permits the construction of specifications which are unbounded (e.g.
have no minimum value) or which do not have a typical value.
The maximum is allowed to be less than the minimum, and the typical value need not be between
the minimum and maximum.
\tparam T an arithmetic type or a type which behaves like an arithmetic type (has arithmetic
operators like + and - defined).
*/
template<typename T> class Spec {
typedef boost::optional<T> optional_type;
//typedef std::optional<T> optional_type; std::optional alternative
optional_type minimum;
optional_type typ;
optional_type maximum;
public:
typedef T value_type;
Spec(value_type typ) : minimum(), typ(typ), maximum() {}
Spec(value_type minimum, value_type maximum) : minimum(minimum), typ(), maximum(maximum) {}
Spec(value_type minimum, value_type typ, value_type maximum) : minimum(minimum), typ(typ), maximum(maximum) {}
Spec(nullspec_type minimum, value_type maximum) : minimum(), typ(), maximum(maximum) {}
Spec(value_type minimum, nullspec_type maximum) : minimum(minimum), typ(), maximum(maximum) {}
Spec(nullspec_type minimum, value_type typ, value_type maximum) : minimum(minimum), typ(typ), maximum(maximum) {}
Spec(value_type minimum, value_type typ, nullspec_type maximum) : minimum(minimum), typ(typ), maximum(maximum) {}
/** \brief A Spec must be constructed with at least one value (minimum, typical or maximum). */
Spec() = delete;
constexpr bool has_min() const noexcept { return bool(minimum); }
constexpr bool has_typical() const noexcept { return bool(typ); }
constexpr bool has_max() const noexcept { return bool(maximum); }
constexpr value_type min() const {
if (minimum) return *minimum;
else throw bad_spec_access("attempted to access a non-existent minimum spec");
}
constexpr value_type typical() const {
if (typ) return *typ;
else throw bad_spec_access("attempted to access a non-existent typical spec");
}
constexpr value_type max() const {
if (maximum) return *maximum;
else throw bad_spec_access("attempted to access a non-existent maximum spec");
}
/** \brief Returns the minimum value if it exists, or the supplied value if not.
\tparam V a type which can be cast to \ref value_type.
\param[in] default_value the value to return if the Spec does not have a defined minimum.
*/
template<typename V>
constexpr value_type min_or(const V& default_value) const {
return minimum.value_or(default_value);
}
/** \brief Returns the typical value if it exists, or the supplied value if not.
\tparam V a type which can be cast to \ref value_type.
\param[in] default_value the value to return if the Spec does not have a defined typical value.
*/
template<typename V>
constexpr value_type typical_or(const V& default_value) const {
return typ.value_or(default_value);
}
/** \brief Returns the maximum value if it exists, or the supplied value if not.
\tparam V a type which can be cast to \ref value_type.
\param[in] default_value the value to return if the Spec does not have a defined maximum.
*/
template<typename V>
constexpr value_type max_or(const V& default_value) const {
return maximum.value_or(default_value);
}
/** \brief Returns the minimum value if it exists and is greater than the supplied clamp value,
otherwise the supplied clamp value.
Similar to min_or(), except that the return value is guaranteed to be greater than or equal to the
clamp value. The argument to min_or() may be used to clamp its return value to a specified value if
the Spec minimum exists, but not if the Spec has a minimum which is less than the argument to min_or().
*/
template<typename V>
constexpr value_type clamped_min(const V& clamp_minimum) const {
// Acquire the minimum value, or use clamp_minimum if the minimum has no value
// Clamp that value (if minimum has a value it might be less than clamp_minimum)
return clamp_min(static_cast<value_type>(minimum.value_or(clamp_minimum)),
static_cast<value_type>(clamp_minimum));
}
/** \brief Returns the minimum value if it exists and is within the range specified by the clamp values.
If the minimum value exists but is outside the range specified by the clamp values then the clamp value
closest to the minimum is returned. If the minimum value doesn't exist then the supplied clamp minimum
is returned.
Similar to min_or(), except that the return value is guaranteed to be within the range of the clamp
values. The argument to min_or() may be used to clamp its return value to a specified value if
the Spec minimum exists, but not if the Spec has a minimum which is less than the argument to min_or().
*/
template<typename V>
constexpr value_type clamped_min(const V& clamp_minimum, const V& clamp_maximum) const {
// Acquire the minimum value, or use clamp_minimum if the minimum has no value
// Clamp that value (if minimum has a value it might not be within the range of clamp_minimum to clamp_maximum)
return clamp(static_cast<value_type>(minimum.value_or(clamp_minimum)),
static_cast<value_type>(clamp_minimum), static_cast<value_type>(clamp_maximum));
}
/** \brief Clamps the typical value if it exists to within the range specified by the supplied
clamp values.
Similar to typical_or(), except that the return value is guaranteed to be within the range specified
by the supplied clamp values. The argument to typical_or() may be used to clamp its return value to a
specified value if the Spec typical exists, but not to a range of values and not if the Spec has a
typical which is outside a clamp range.
*/
template<typename V>
constexpr value_type clamped_typical(const V& clamp_minimum, const V& clamp_maximum) const {
if (typ) return clamp(*typ, static_cast<value_type>(clamp_minimum), static_cast<value_type>(clamp_maximum));
else throw bad_spec_access("cannot clamp a non-existent typical value");
}
/** \brief Clamps the typical value if it exists to within the range specified by the supplied
clamp values, or returns the specified default value.
Similar to clamped_typical(const V&, const V&), except that a default value is returned in the
case where clamped_typical(const V&, const V&) throws an exception. Also similar to typical_or(),
except that the return value is guaranteed to be within the range specified by the supplied clamp
values if the Spec typical is defined.
*/
template<typename V>
constexpr value_type clamped_typical(const V& clamp_minimum, const V& clamp_maximum, const V& default_value) const {
if (typ) return clamp(*typ, static_cast<value_type>(clamp_minimum), static_cast<value_type>(clamp_maximum));
else return default_value;
}
/** \brief Returns the maximum value if it exists and is less than the supplied clamp value,
otherwise the supplied clamp value.
Similar to max_or(), except that the return value is guaranteed to be less than or equal to the
clamp value. The argument to max_or() may be used to clamp its return value to a specified value if
the Spec maximum exists, but not if the Spec has a maximum which is greater than the argument to max_or().
*/
template<typename V>
constexpr value_type clamped_max(const V& clamp_maximum) const {
// Acquire the maximum value, or use clamp_maximum if the maximum has no value
// Clamp that value (if maximum has a value it might be greater than clamp_maximum)
return clamp_max(static_cast<value_type>(maximum.value_or(clamp_maximum)),
static_cast<value_type>(clamp_maximum));
}
/** \brief Returns the maximum value if it exists and is within the range specified by the clamp values.
If the maximum value exists but is outside the range specified by the clamp values then the clamp value
closest to the maximum is returned. If the maximum value doesn't exist then the supplied clamp maximum
is returned.
Similar to max_or(), except that the return value is guaranteed to be within the range of the clamp
values. The argument to max_or() may be used to clamp its return value to a specified value if
the Spec maximum exists, but not if the Spec has a maximum which is outside the clamp range.
*/
template<typename V>
constexpr value_type clamped_max(const V& clamp_minimum, const V& clamp_maximum) const {
// Acquire the maximum value, or use clamp_maximum if the maximum has no value
// Clamp that value (if maximum has a value it might not be within the range of clamp_minimum to clamp_maximum)
return clamp(static_cast<value_type>(maximum.value_or(clamp_maximum)),
static_cast<value_type>(clamp_minimum), static_cast<value_type>(clamp_maximum));
}
/** \brief Determines if two Specs are equal.
Two Specs are considered equal if all the following are true:
1. Both Specs have equal minimum values or both Specs have an undefined minimum.
2. Both Specs have equal typical values or both Specs have an undefined typical value.
3. Both Specs have equal maximum values or both Specs have an undefined maximum.
\tparam U a value_type which can be cast to type `T`.
*/
template<typename T, typename U>
friend inline bool operator==(const Spec<T>& lhs, const Spec<U>& rhs);
template<typename T, typename U>
friend inline bool operator!=(const Spec<T>& lhs, const Spec<U>& rhs);
/** \brief Inserts a Spec into a `std::ostream`.
Inserts a string of the form '[min, typical, max]', where 'min', 'typical', and 'max' are the minimum,
typical, and maximum values of the Spec, respectively. If a minimum value is not defined then the
string "-Inf" is used (to symbolize negative infinity). If a maximum value is not defined then the
string "Inf" is used (to symbolize positive infinity). If a typical value is not defined then the
substring 'typical, ' is omitted -- the inserted string has the form '[min, max]'.
Square brackets are used to denote closed intervals if `spec` has a defined minimum and/or maximum,
and parentheses are used to denote open intervals if `spec` has an undefined minimum and/or maximum.
For example, the string for a `spec` with a minimum of 0 and an undefined maximum is '[0, Inf)`.
\tparam T a type for which operator<<(std::ostream, T) is defined.
*/
template<typename T>
friend inline std::ostream& operator<<(std::ostream& os, const Spec<T>& spec);
};
template<typename T, typename U>
inline bool operator==(const Spec<T>& lhs, const Spec<U>& rhs) {
// Two optionals are equal if:
// 1. they both contain a value and those values are equal
// 2. neither contains a value
return (lhs.minimum == rhs.minimum) && (lhs.typ == rhs.typ) && (lhs.maximum == rhs.maximum);
}
template<typename T, typename U>
inline bool operator!=(const Spec<T>& lhs, const Spec<U>& rhs) { return !(lhs == rhs); }
template<typename T>
inline std::ostream& operator<<(std::ostream& os, const Spec<T>& spec) {
if (spec.has_min()) os << '[' << spec.min();
else os << '(' << "-Inf";
os << ", ";
if (spec.has_typical()) os << spec.typical() << ", ";
if (spec.has_max()) os << spec.max() << ']';
else os << "Inf" << ')';
return os;
}
/** \brief Clamps a value to within a specified range.
Based on std::clamp() in C++17: http://en.cppreference.com/w/cpp/algorithm/clamp.
\throws std::logic_error if `min > max`
*/
template<typename T>
constexpr const T& clamp(const T& value, const T& min, const T& max) {
if (min > max) throw std::logic_error("cannot clamp between a min and max value when min > max");
return value < min ? min : value > max ? max : value;
}
/** \brief Clamps a value to greater than or equal to a specified minimum. */
template<typename T>
constexpr const T& clamp_min(const T& value, const T& min) {
return value < min ? min : value;
}
/** \brief Clamps a value to less than or equal to a specified maximum. */
template<typename T>
constexpr const T& clamp_max(const T& value, const T& max) {
return value > max ? max : value;
}
/** \brief Clamps the values held by a Spec to within a specified range.
\return a new `Spec<T>`, `s`, with its minimum, typical, and maximum values clamped to the range specified
by `min` and `max`. If `spec` does not have a defined minimum then `s` has its minimum set to `min`.
If `spec` does not have a defined maximum then `s` has its maximum set to `max`. If `spec` does not
have a defined typical then `s` does not have a defined typical, either. If `spec` has a defined typical
then the defined typical for `s` is either equal to that of `spec` (if the typical is within the clamp
limits) or set to the closest clamp value (e.g. if `spec.typical() > max` then `s.typical() = max`).
\throws std::logic_error if `min > max`
*/
template<typename T>
constexpr Spec<T> clamp(const Spec<T>& spec, const T& min, const T& max) {
if (min > max) throw std::logic_error("cannot clamp between a min and max value when min > max");
auto clamped_min = spec.clamped_min(min);
auto clamped_max = spec.clamped_max(max);
if (spec.has_typical()) {
auto clamped_typical = clamp(spec.typical(), min, max);
return Spec<T>(clamped_min, clamped_typical, clamped_max);
} else return Spec<T>(clamped_min, clamped_max);
}
/** \brief Determines if a value is between the limits of a Spec.
\return `true` if `value` is greater than or equal to the Spec argument's minimum and less than or
equal to the Spec argument's maximum, `false` otherwise. If the Spec argument does not have a minimum
then `value` is considered greater than the Spec argument's minimum. If the Spec argument does not
have a maximum then `value` is considered less than the Spec argument's maximum. For example, if `spec`
has a minimum but no maximum then `true` is returned if `value >= spec.min()`, `false` otherwise. If
`spec` has neither a minimum nor maximum then `true` is returned for any `value`.
*/
template<typename V, typename T>
inline bool pass(const V& value, const Spec<T>& spec) {
if (spec.has_min() && value < spec.min()) return false;
if (spec.has_max() && value > spec.max()) return false;
return true;
}
template<typename V, typename T>
inline bool fail(const V& value, const Spec<T>& spec) {
return !pass(value, spec);
}
/** \brief Calculates Spec limits based on an expected value and a % tolerance.
\return a Spec with `value_type` set to the same type as `value`, a minimum equal to
`(1 - tolerance / 100) * value`, a typical equal to `value`, and a maximum equal to
`(1 + tolerance / 100) * value`. For example, for a 1% tolerance the minimum is equal to 99% of `value`
and the maximum is equal to 101% of `value`.
*/
template<typename V, typename T>
inline Spec<V> tolerance_spec(const V& value, const T& tolerance) {
return Spec<V>((1 - tolerance / 100.0) * value, value, (1 + tolerance / 100.0) * value);
}
/** \brief Calculates Spec limits based on an expected value and a % tolerance and a clamping range.
\return a Spec with `value_type` set to the same type as `value`, a typical equal to `value`, a
minimum set to `(1 - tolerance / 100) * value` or `clamp_min` (whichever is greater), and a maximum
set to `(1 + tolerance / 100) * value` or `clamp_max` (whichever is less). For example, for a 1%
tolerance the minimum is equal to 99% of `value` and the maximum is equal to 101% of `value` unless
the minimum and/or maximum exceed the clamp range (in which case the minimum and/or maximum are set
to the clamp values).
*/
template<typename V, typename T>
inline Spec<V> tolerance_spec(const V& value, const T& tolerance, const V& clamp_minimum, const V& clamp_maximum) {
// Min/max based on tolerance calculation
V tol_min = (1 - tolerance / 100.0) * value;
V tol_max = (1 + tolerance / 100.0) * value;
// Clamp the min/max based on the tolerance calculation to the minimum/maximum
clamp(tol_min, clamp_minimum, clamp_maximum);
clamp(tol_max, clamp_minimum, clamp_maximum);
return Spec<V>(tol_min, value, tol_max);
}
#endif
Here's a demo program of Spec
and the related utilities:
#include <iostream>
#include <chrono>
#include <stdexcept>
#include <vector>
#include <fstream>
#include "Spec.h"
using namespace std::chrono_literals;
template<typename T>
void print_unchecked_accesses(std::ostream& os, Spec<T> spec) {
try {
auto min = spec.min();
} catch (const bad_spec_access& err) {
os << err.what() << '\n';
}
try {
auto typ = spec.typical();
} catch (const bad_spec_access& err) {
os << err.what() << '\n';
}
try {
auto max = spec.max();
} catch (const bad_spec_access& err) {
os << err.what() << '\n';
}
os << '\n';
}
template<typename T>
void print_checked_accesses(std::ostream& os, Spec<T> spec) {
if (spec.has_min()) auto min = spec.min();
else os << "non-existent min\n";
if (spec.has_typical()) auto typ = spec.typical();
else os << "non-existent typical\n";
if (spec.has_max()) auto max = spec.max();
else os << "non-existent max\n";
os << '\n';
}
template<typename T, typename V>
void print_access_or(std::ostream& os, Spec<T> spec, V default_value) {
os << "Accesses with default value " << default_value << " provided: ";
os << spec.min_or(default_value) << ", ";
os << spec.typical_or(default_value) << ", ";
os << spec.max_or(default_value) << "\n\n";
}
template<typename T, typename V>
void test_clamps(std::ostream& os, Spec<T> spec, V clamp_min, V clamp_max) {
os << spec << " limits clamped to " << clamp(spec, clamp_min, clamp_max)
<< " with clamps " << clamp_min << " and " << clamp_max << '\n';
V default_value = 0;
os << "Clamping typical with default value " << default_value << ": "
<< spec.clamped_typical(clamp_min, clamp_max, default_value) << '\n';
os << "Clamping typical without a default value: ";
try {
os << spec.clamped_typical(clamp_min, clamp_max);
} catch (const bad_spec_access& err) {
os << err.what();
}
os << '\n';
}
int main() {
std::ofstream ofs("demo.txt");
double clamp_min = 0;
double clamp_max = 15;
std::vector<Spec<double> > specs = {
Spec<double>(0, 5), // double-sided
Spec<double>(-5, nullspec), // min only
Spec<double>(nullspec, 298), // max only
Spec<double>(90), // typical only
Spec<double>(-79, 235, 89235), // min, typical, and max
Spec<double>(tolerance_spec<double, int>(5, 10)),
Spec<double>(tolerance_spec<double, int>(15, 10, clamp_min, clamp_max))
};
for (auto spec : specs) {
ofs << "Spec: " << spec << '\n';
ofs << "Exceptions caught due to unchecked accesses:\n";
print_unchecked_accesses(ofs, spec);
ofs << "Non-existent values determined from checked accesses:\n";
print_checked_accesses(ofs, spec);
print_access_or(ofs, spec, -1);
test_clamps(ofs, spec, clamp_min, clamp_max);
ofs << "--------------------------------------------------------------------------------\n";
}
ofs << "Testing equality:\n";
using TimeSpec = Spec<std::chrono::microseconds>;
TimeSpec::value_type max = 5us;
TimeSpec t1(-15us, 0us, max);
TimeSpec t2 = t1;
TimeSpec t3(-10us, 0us, max); // unequal min
TimeSpec t4(-15us, 1us, max); // unequal typical
TimeSpec t5(-15us, 0us, 6us); // unequal max
std::vector<TimeSpec> timespecs{t1, t2, t3, t4, t5};
ofs << std::boolalpha;
for (std::size_t t = 1; t < timespecs.size(); t++) {
ofs << "t1 == t" << t + 1 << "?: " << (t1 == timespecs[t]) << '\n';
}
ofs << '\n';
Spec<double> spec(-15, 0, 5);
ofs << "Testing pass/fail with Spec " << spec << "\n";
for (auto value : {-16, -15, 0, 5, 10}) {
ofs << value << " passes?: " << pass(value, spec) << '\n';
}
return 0;
}
The demo program outputs:
Spec: [0, 5]
Exceptions caught due to unchecked accesses:
attempted to access a non-existent typical spec
Non-existent values determined from checked accesses:
non-existent typical
Accesses with default value -1 provided: 0, -1, 5
[0, 5] limits clamped to [0, 5] with clamps 0 and 15
Clamping typical with default value 0: 0
Clamping typical without a default value: cannot clamp a non-existent typical value
--------------------------------------------------------------------------------
Spec: [-5, Inf)
Exceptions caught due to unchecked accesses:
attempted to access a non-existent typical spec
attempted to access a non-existent maximum spec
Non-existent values determined from checked accesses:
non-existent typical
non-existent max
Accesses with default value -1 provided: -5, -1, -1
[-5, Inf) limits clamped to [0, 15] with clamps 0 and 15
Clamping typical with default value 0: 0
Clamping typical without a default value: cannot clamp a non-existent typical value
--------------------------------------------------------------------------------
Spec: (-Inf, 298]
Exceptions caught due to unchecked accesses:
attempted to access a non-existent minimum spec
attempted to access a non-existent typical spec
Non-existent values determined from checked accesses:
non-existent min
non-existent typical
Accesses with default value -1 provided: -1, -1, 298
(-Inf, 298] limits clamped to [0, 15] with clamps 0 and 15
Clamping typical with default value 0: 0
Clamping typical without a default value: cannot clamp a non-existent typical value
--------------------------------------------------------------------------------
Spec: (-Inf, 90, Inf)
Exceptions caught due to unchecked accesses:
attempted to access a non-existent minimum spec
attempted to access a non-existent maximum spec
Non-existent values determined from checked accesses:
non-existent min
non-existent max
Accesses with default value -1 provided: -1, 90, -1
(-Inf, 90, Inf) limits clamped to [0, 15, 15] with clamps 0 and 15
Clamping typical with default value 0: 15
Clamping typical without a default value: 15
--------------------------------------------------------------------------------
Spec: [-79, 235, 89235]
Exceptions caught due to unchecked accesses:
Non-existent values determined from checked accesses:
Accesses with default value -1 provided: -79, 235, 89235
[-79, 235, 89235] limits clamped to [0, 15, 15] with clamps 0 and 15
Clamping typical with default value 0: 15
Clamping typical without a default value: 15
--------------------------------------------------------------------------------
Spec: [4.5, 5, 5.5]
Exceptions caught due to unchecked accesses:
Non-existent values determined from checked accesses:
Accesses with default value -1 provided: 4.5, 5, 5.5
[4.5, 5, 5.5] limits clamped to [4.5, 5, 5.5] with clamps 0 and 15
Clamping typical with default value 0: 5
Clamping typical without a default value: 5
--------------------------------------------------------------------------------
Spec: [13.5, 15, 16.5]
Exceptions caught due to unchecked accesses:
Non-existent values determined from checked accesses:
Accesses with default value -1 provided: 13.5, 15, 16.5
[13.5, 15, 16.5] limits clamped to [13.5, 15, 15] with clamps 0 and 15
Clamping typical with default value 0: 15
Clamping typical without a default value: 15
--------------------------------------------------------------------------------
Testing equality:
t1 == t2?: true
t1 == t3?: false
t1 == t4?: false
t1 == t5?: false
Testing pass/fail with Spec [-15, 0, 5]
-16 passes?: false
-15 passes?: true
0 passes?: true
5 passes?: true
10 passes?: false
I'm looking for suggestions regarding the class design, naming, etc. Also, a few specific concerns that I'd appreciate reviewers to comment on:
- I'm still learning how to use the full power of C++11 and C++14 since I was using a pre-C++11 compiler for a long time, so are there any C++11 or C++14 features I forgot to use which would improve the code?
- I'm not very comfortable with move semantics yet. Are there some optimizations I've missed because of that?
- I've provided no functions to modify the internal data of the
Spec
class (it is immutable). This simplifies the class design a bit (e.g. I don't have to check that at least one of the minimum/typical/maximum values are defined when modifying the instance -- I just have to delete the default constructor). However, in my usage it has occasionally been inconvenient to not be able to modify aSpec
instance after it's constructed (e.g. to acquire aSpec
instance with limits taken from another instance but clamped -- I have to copy it instead). Was my decision to make the class immutable a good one, or does it limit its usefulness? - It would be nice to detect if the code is being compiled by a compiler that supports
std::optional
and usestd::optional
if so, or try to fall back onboost::optional
if not. Is there a way to do this without too much difficulty?