7
\$\begingroup\$

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:

741 datasheet examples 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:

  1. 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?
  2. I'm not very comfortable with move semantics yet. Are there some optimizations I've missed because of that?
  3. 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 a Spec instance after it's constructed (e.g. to acquire a Spec 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?
  4. It would be nice to detect if the code is being compiled by a compiler that supports std::optional and use std::optional if so, or try to fall back on boost::optional if not. Is there a way to do this without too much difficulty?
\$\endgroup\$
2
\$\begingroup\$

Naming:

Be consistent, and preferably avoid abbreviations. The members are minimum, typ and maximum, but then there's has_min(), has_typical() and has_max(), etc. It might be neater to just spell the whole thing out every time.

Constructors:

Uh... that's a lot of constructors. IMHO, it would be much neater to make the optional_type typedef public, and have just one constructor taking the optional type for all three parameters:

Spec(optional_type const& minimum, optional_type const& typical, optional_type const& maximum):
    minimum(minimum), typ(typical), maximum(maximum) { }

You can still pass value_type or nullspec directly (at least with std::optional, I haven't checked boost::optional), but now it's instantly obvious what's going on, because you have to define all three parameters every time:

Spec<double>(0.0, nullspec, 5.0);

There's no need to explicitly delete the default constructor, since you've defined non-default constructors.

Other:

  • There's no need to static cast the results of minimum.value_or() in clamped_min(), because std::optional::value_or() will returns value_type already. Same with the maximum version.
  • You might like to check for a negative tolerance in tolerance_spec() (although the docs say max can be lower than min, so maybe not...).
  • (BUG?:) In tolerance_spec, you don't seem to be using the result of the clamp operations!

Your Questions:

  1. There's no heap-allocated members, so there's nothing that would be faster to move than copy. The compiler will generate a move constructor for you anyway.
  2. If set_foo() member functions would be useful, there's no reason not to provide them. If an instance mustn't be altered, that's what const and const& are for. Similar to the constructor, these can take an optional_type, rather than the value_type.
\$\endgroup\$
  • \$\begingroup\$ The problem with the single constructor taking three optionals is that a Spec can be constructed with no minimum/typical/maximum by calling the constructor with three optionals that don't have a value. Additionally, many specifications don't have all three parameters so it would be annoying to always have to provide nullspec arguments in those cases. While a single constructor is attractive for its simplicity, as a user of the class I've found it convenient to have additional constructors so that I don't have to use nullspec arguments in most cases. \$\endgroup\$ – Null Feb 7 '18 at 20:14
  • \$\begingroup\$ I explicitly deleted default constructor in part for documenting the fact that users shouldn't try to construct a Spec without at least one of the minimum/typical/maximum. \$\endgroup\$ – Null Feb 7 '18 at 20:16
  • \$\begingroup\$ I'm on the fence about checking for a negative tolerance since the min is allowed to be lower than the max...but I wouldn't expect users to get a Spec with min > max that way. The reason I allow min > max is that it's possible for that to happen once I add guardbands to the class and if the guardbands are high enough to invert the min/max. It's something I'll have to think about some more. \$\endgroup\$ – Null Feb 7 '18 at 20:21
  • \$\begingroup\$ tolerance_spec() indeed has a bug in which it doesn't use the clamps. I think I changed the definition of clamp() from taking the value as a reference to a const reference, which created a bug in tolerance_spec(). Good catch. \$\endgroup\$ – Null Feb 7 '18 at 20:30
1
\$\begingroup\$

Constructors

With regard to the long list of different constructors, I think you wrote this with somebody manually writing the constructor calls. I.e. you're trying to minimize the "handwriting" effort, assuming that most of these values would come out of some file descriptions, look at how that is read and which of these is used the most. Think whether the other ones are worth keeping. Something like

Spec spec1{none, 12, none};  
Spec spec2{0.5, 12, none}; 
... 

is also inherently more readable, as you don't have to trace back what form of the constructor was invoked. Also IIRC, at least for boost::any<T> T implicity convert to boost::any<T>. I.e. you can call

 void func(boost::any<double> x); 

via func(12.0) this could also help cutting the amount of constructors down.

Mutablity

With regard to mutability you could consider making your class mutable but utilizing const variable to indicate immutable values. To do this I'd suggest changing the name of your min\typical\max accessors to min_value, typical_value, etc. this lets you introduce two other accessors

 boost::any<V>& min() {return minimum};
 const boost::any<V>& min() const {return minimum};

Depending on the style that you like this might let you get rid of the has_value functions, as you could now do

 if (spec.min()) { ... }

The same would go for the default functions ...

 spec.min().value_or(default);

yes this exposes the underlying type but sometimes (sometimes) this isn't necessarily a bad thing, epsecially when you find yourself writing a lot of functions that just wrap the interface of the internal class. It all depends on the goals.

Thoughts ...

I stared at your interface for a while, not sure if I've come to a good conclusion, the whole series of clamping call just seem very complex from the outside, but I also really couldn't find a better structure for it. We only see the interface of the class not the requirements that created this interface. Maybe the ideas from mutability could change that ...

Detection

With regard to compiler support, if you're using cmake you can check for the existence of include files, otherwise you might have to just go and check for specific compiler versions, I think the latest releases of the big 3 clang, gcc and msvc all have std:any implemented.

Other

Looks like the ostream functor doesn't need to be a friend that is a good thing.

The core guidelines suggest preferring using over typedef if you're just heading back to c++ it's a good habit to pick up. using is better in dealing with templates as well.

\$\endgroup\$
  • \$\begingroup\$ Since you are the second person to advocate a three-argument constructor approach I will reconsider the constructors. Perhaps as a compromise I will provide additional constructors which accept two nullspec_types so that users which want to show all three arguments explicitly can do so, and users that prefer a shorter argument list when possible can use the existing constructors. My use cases involve both "handwriting" the constructor calls as well as reading values from a file (the former prefers fewer arguments, the latter would use a consistent number of arguments). Thanks for the review. \$\endgroup\$ – Null Feb 12 '18 at 16:43
  • \$\begingroup\$ Oh, and thanks for the link to the core guidelines preferring using over typedef. \$\endgroup\$ – Null Feb 12 '18 at 16:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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