26

Suppose I have a LimitedValue class which holds a value, and is parameterized on int types 'min' and 'max'. You'd use it as a container for holding values which can only be in a certain range. You could use it such:

LimitedValue< float, 0, 360 > someAngle( 45.0 );
someTrigFunction( someAngle );

so that 'someTrigFunction' knows that it is guaranteed to be supplied a valid input (The constructor would throw an exception if the parameter is invalid).

Copy-construction and assignment are limited to exactly equal types, though. I'd like to be able to do:

LimitedValue< float, 0, 90 > smallAngle( 45.0 );
LimitedValue< float, 0, 360 > anyAngle( smallAngle );

and have that operation checked at compile-time, so this next example gives an error:

LimitedValue< float, -90, 0 > negativeAngle( -45.0 );
LimitedValue< float, 0, 360 > postiveAngle( negativeAngle ); // ERROR!

Is this possible? Is there some practical way of doing this, or any examples out there which approach this?

user23434
  • 425
  • 2
  • 5
  • 8
  • Possible duplicate of [How can I specialize a C++ template for a range of integer values?](http://stackoverflow.com/questions/11019232/how-can-i-specialize-a-c-template-for-a-range-of-integer-values) – Ciro Santilli OurBigBook.com May 21 '17 at 19:26

9 Answers9

20

OK, this is C++11 with no Boost dependencies.

Everything guaranteed by the type system is checked at compile time, and anything else throws an exception.

I've added unsafe_bounded_cast for conversions that may throw, and safe_bounded_cast for explicit conversions that are statically correct (this is redundant since the copy constructor handles it, but provided for symmetry and expressiveness).

Example Use

#include "bounded.hpp"

int main()
{
    BoundedValue<int, 0, 5> inner(1);
    BoundedValue<double, 0, 4> outer(2.3);
    BoundedValue<double, -1, +1> overlap(0.0);

    inner = outer; // ok: [0,4] contained in [0,5]

    // overlap = inner;
    // ^ error: static assertion failed: "conversion disallowed from BoundedValue with higher max"

    // overlap = safe_bounded_cast<double, -1, +1>(inner);
    // ^ error: static assertion failed: "conversion disallowed from BoundedValue with higher max"

    overlap = unsafe_bounded_cast<double, -1, +1>(inner);
    // ^ compiles but throws:
    // terminate called after throwing an instance of 'BoundedValueException<int>'
    //   what():  BoundedValueException: !(-1<=2<=1) - BOUNDED_VALUE_ASSERT at bounded.hpp:56
    // Aborted

    inner = 0;
    overlap = unsafe_bounded_cast<double, -1, +1>(inner);
    // ^ ok

    inner = 7;
    // terminate called after throwing an instance of 'BoundedValueException<int>'
    //   what():  BoundedValueException: !(0<=7<=5) - BOUNDED_VALUE_ASSERT at bounded.hpp:75
    // Aborted
}

Exception Support

This is a bit boilerplate-y, but gives fairly readable exception messages as above (the actual min/max/value are exposed as well, if you choose to catch the derived exception type and can do something useful with it).

#include <stdexcept>
#include <sstream>

#define STRINGIZE(x) #x
#define STRINGIFY(x) STRINGIZE( x )

// handling for runtime value errors
#define BOUNDED_VALUE_ASSERT(MIN, MAX, VAL) \
    if ((VAL) < (MIN) || (VAL) > (MAX)) { \
        bounded_value_assert_helper(MIN, MAX, VAL, \
                                    "BOUNDED_VALUE_ASSERT at " \
                                    __FILE__ ":" STRINGIFY(__LINE__)); \
    }

template <typename T>
struct BoundedValueException: public std::range_error
{
    virtual ~BoundedValueException() throw() {}
    BoundedValueException() = delete;
    BoundedValueException(BoundedValueException const &other) = default;
    BoundedValueException(BoundedValueException &&source) = default;

    BoundedValueException(int min, int max, T val, std::string const& message)
        : std::range_error(message), minval_(min), maxval_(max), val_(val)
    {
    }

    int const minval_;
    int const maxval_;
    T const val_;
};

template <typename T> void bounded_value_assert_helper(int min, int max, T val,
                                                       char const *message = NULL)
{
    std::ostringstream oss;
    oss << "BoundedValueException: !("
        << min << "<="
        << val << "<="
        << max << ")";
    if (message) {
        oss << " - " << message;
    }
    throw BoundedValueException<T>(min, max, val, oss.str());
}

Value Class

template <typename T, int Tmin, int Tmax> class BoundedValue
{
public:
    typedef T value_type;
    enum { min_value=Tmin, max_value=Tmax };
    typedef BoundedValue<value_type, min_value, max_value> SelfType;

    // runtime checking constructor:
    explicit BoundedValue(T runtime_value) : val_(runtime_value) {
        BOUNDED_VALUE_ASSERT(min_value, max_value, runtime_value);
    }
    // compile-time checked constructors:
    BoundedValue(SelfType const& other) : val_(other) {}
    BoundedValue(SelfType &&other) : val_(other) {}

    template <typename otherT, int otherTmin, int otherTmax>
    BoundedValue(BoundedValue<otherT, otherTmin, otherTmax> const &other)
        : val_(other) // will just fail if T, otherT not convertible
    {
        static_assert(otherTmin >= Tmin,
                      "conversion disallowed from BoundedValue with lower min");
        static_assert(otherTmax <= Tmax,
                      "conversion disallowed from BoundedValue with higher max");
    }

    // compile-time checked assignments:
    BoundedValue& operator= (SelfType const& other) { val_ = other.val_; return *this; }

    template <typename otherT, int otherTmin, int otherTmax>
    BoundedValue& operator= (BoundedValue<otherT, otherTmin, otherTmax> const &other) {
        static_assert(otherTmin >= Tmin,
                      "conversion disallowed from BoundedValue with lower min");
        static_assert(otherTmax <= Tmax,
                      "conversion disallowed from BoundedValue with higher max");
        val_ = other; // will just fail if T, otherT not convertible
        return *this;
    }
    // run-time checked assignment:
    BoundedValue& operator= (T const& val) {
        BOUNDED_VALUE_ASSERT(min_value, max_value, val);
        val_ = val;
        return *this;
    }

    operator T const& () const { return val_; }
private:
    value_type val_;
};

Cast Support

template <typename dstT, int dstMin, int dstMax>
struct BoundedCastHelper
{
    typedef BoundedValue<dstT, dstMin, dstMax> return_type;

    // conversion is checked statically, and always succeeds
    template <typename srcT, int srcMin, int srcMax>
    static return_type convert(BoundedValue<srcT, srcMin, srcMax> const& source)
    {
        return return_type(source);
    }

    // conversion is checked dynamically, and could throw
    template <typename srcT, int srcMin, int srcMax>
    static return_type coerce(BoundedValue<srcT, srcMin, srcMax> const& source)
    {
        return return_type(static_cast<srcT>(source));
    }
};

template <typename dstT, int dstMin, int dstMax,
          typename srcT, int srcMin, int srcMax>
auto safe_bounded_cast(BoundedValue<srcT, srcMin, srcMax> const& source)
    -> BoundedValue<dstT, dstMin, dstMax>
{
    return BoundedCastHelper<dstT, dstMin, dstMax>::convert(source);
}

template <typename dstT, int dstMin, int dstMax,
          typename srcT, int srcMin, int srcMax>
auto unsafe_bounded_cast(BoundedValue<srcT, srcMin, srcMax> const& source)
    -> BoundedValue<dstT, dstMin, dstMax>
{
    return BoundedCastHelper<dstT, dstMin, dstMax>::coerce(source);
}
Useless
  • 64,155
  • 6
  • 88
  • 132
  • In Your first(explicit) constructor shouldn't be `T runtime_value` instead of `int runtime_value`? – Quest Nov 11 '14 at 21:06
  • Assumes an internal type...OP wants real number support. – martin.dowie Nov 05 '15 at 13:31
  • Do you mean an _integral_ type? The storage type is a template param and works fine with double. The _bounds_ are integers, as shown in the question. – Useless Nov 05 '15 at 14:51
  • 1
    `constexpr` would be a better choice than `enum` since this is supposed to be a `C++11` solution. – Pharap May 06 '17 at 14:17
18

You can do this using templates -- try something like this:

template< typename T, int min, int max >class LimitedValue {
   template< int min2, int max2 >LimitedValue( const LimitedValue< T, min2, max2 > &other )
   {
   static_assert( min <= min2, "Parameter minimum must be >= this minimum" );
   static_assert( max >= max2, "Parameter maximum must be <= this maximum" );

   // logic
   }
// rest of code
};
Emil Laine
  • 41,598
  • 9
  • 101
  • 157
Kasprzol
  • 4,087
  • 22
  • 20
  • 3
    Note that this is wrong. The last example shows a limit that doesn't fit (i.e. -90 to 0 versus 0 to 360,) however an angle of 0 satisfy both variable types. Thus if the angle was 0 instead of -45 you would not get the error marked in that last example. – Alexis Wilke Oct 28 '11 at 11:40
  • This answer is completely wrong. OP specified a runtime check that the given parameter will fit in the lhs type. static_assert is for compile time checking, which may also be helpful, but does not answer the question. Additionally, the genericity is gone since the min and max must be specified as int. – TamaMcGlinn Aug 08 '19 at 11:01
  • 2
    This answer is not wrong. OP is asking for _static_ (compile-time) checks. Says "guaranteed to be supplied a valid input" and "have that operation checked at compile-time". The requirement is that code guarantee that _every_ possible value of `other` be assignable to `*this`. – Pablo H Nov 27 '19 at 12:49
5

The Boost Constrained Value library(1) allows you to add constrains to data types.

But you have to read the advice "Why C++'s floating point types shouldn't be used with bounded objects?" when you like to use it with float types (as illustrated in your example).

(1) The Boost Constrained Value library is not an official Boost library yet.

malat
  • 12,152
  • 13
  • 89
  • 158
jk.
  • 6,388
  • 2
  • 26
  • 21
4

The bounded::integer library does what you want (for integer types only). http://doublewise.net/c++/bounded/

(In the interests of full disclosure, I am the author of this library)

It differs from other libraries that attempt to provide "safe integers" in a significant way: it tracks integer bounds. I think this is best shown by example:

auto x = bounded::checked_integer<0, 7>(f());
auto y = 7_bi;
auto z = x + y;
// decltype(z) == bounded::checked_integer<7, 14>
static_assert(z >= 7_bi);
static_assert(z <= 14_bi);

x is an integer type that is between 0 and 7. y is an integer type between 7 and 7. z is an integer type between 7 and 14. All of this information is known at compile time, which is why we are able to static_assert on it, even though the value of z is not a compile-time constant.

z = 10_bi;
z = x;
static_assert(!std::is_assignable<decltype((z)), decltype(0_bi)>::value);

The first assignment, z = 10_bi, is unchecked. This is because the compiler can prove that 10 falls within the range of z.

The second assignment, z = x, checks that the value of x is within the range of z. If not, it throws an exception (the exact behavior depends on the type of integer you use, there are many policies of what to do).

The third line, the static_assert, shows that it is a compile-time error to assign from a type that has no overlap at all. The compiler already knows this is an error and stops you.

The library does not implicitly convert to the underlying type, as this can cause many situations where you try to prevent something but it happens due to conversions. It does allow explicit conversion.

David Stone
  • 26,872
  • 14
  • 68
  • 84
  • 1
    That is amazing work! I consider using your library in my code. However since the impact of doing so would be heavy, I am a bit concerned what happens in case you are forced to drop development of the library. Have you considered adding it to boost or something to make sure the library is maintained by more than one person? – Silicomancer Jan 03 '19 at 11:05
  • 1
    I have been working on getting concepts and class types as non-type template parameters in C++ in a form usable by my library before I put it somewhere that people will have an expectation of stability. Fortunately, we have both of these in C++20. I am waiting for either gcc to fix some bugs where it crashes when compiling my library, or for clang to integrate the concepts branch. At that point, I will look into submitting it to a larger collection (most likely boost). – David Stone Jan 03 '19 at 23:48
  • Ok, thanks for the information. Please let us know (maybe by comment here) if you consider your work stable and applicable. – Silicomancer Jan 04 '19 at 00:01
3

This is actually a complex matter and I have tackled it for a while...

Now I have a publicly available library that will allow you to limit floating points and integers in your code so you can make more sure that they are valid at all time.

Not only that you can turn off the limits in your final release version and that means the types pretty much become the same as a typedef.

Define your type as:

typedef controlled_vars::limited_fauto_init<float, 0, 360> angle_t;

And when you don't define the CONTROLLED_VARS_DEBUG and CONTROLLED_VARS_LIMITED flags, you get pretty much the same as this:

typedef float angle_t;

These classes are generated so they include all the necessary operators for you to not suffer too much when using them. That means you can see your angle_t nearly as a float.

angle_t a;
a += 35;

Will work as expected (and throw if a + 35 > 360).

http://snapwebsites.org/project/controlled-vars

I know this was posted in 2008... but I don't see any good link to a top library that offers this functionality!?


As a side note for those who want to use this library, I've noticed that in some cases the library will silently resize values (i.e. float a; double b; a = b; and int c; long d; c = d;) and that can cause all sorts of issues in your code. Be careful using the library.

Alexis Wilke
  • 19,179
  • 10
  • 84
  • 156
2

I wrote a C++ class that imitates the functionality of Ada's range.

It is based on templates, similar to the solutions provided here.

If something like this is to be used in a real project, it will be used in a very fundamental way. Subtle bugs or misunderstandings can be disastrous.

Therefore, although it is a small library without a lot of code, in my opinion provision of unit tests and clear design philosophy are very important.

Feel free to try it and please tell me if you find any problems.

https://github.com/alkhimey/ConstrainedTypes

http://www.nihamkin.com/2014/09/05/range-constrained-types-in-c++/

Artium
  • 5,147
  • 8
  • 39
  • 60
1

At the moment, that is impossible in a portable manner due to the C++ rules on how methods (and by extension, constructors) are called even with constant arguments.

In the C++0x standard, you could have a const-expr that would allow such an error to be produced though.

(This is assuming you want it to throw an error only if the actual value is illegal. If the ranges do not match, you can achieve this)

workmad3
  • 25,101
  • 4
  • 35
  • 56
1

One thing to remember about templates is that each invocation of a unique set of template parameters will wind up generating a "unique" class for which comparisons and assignments will generate a compile error. There may be some meta-programming gurus that might know how to work around this but I am not one of them. My approach would be to implement these in a class with run-time checks and overloaded comparison and assignment operators.

Jon Trauntvein
  • 4,453
  • 6
  • 39
  • 69
1

I'd like to offer an alternate version for Kasprzol's solution: The proposed approach always uses bounds of type int. You can get some more flexibility and type safety with an implementation such as this:

template<typename T, T min, T max>
class Bounded {
private:
    T _value;
public:
    Bounded(T value) : _value(min) {
        if (value <= max && value >= min) {
            _value = value;
       } else {
           // XXX throw your runtime error/exception...
       }
    }
    Bounded(const Bounded<T, min, max>& b)
        : _value(b._value){ }
};

This will allow the type checker to catch obvious miss assignments such as:

Bounded<int, 1, 5> b1(1);
Bounded<int, 1, 4> b2(b1); // <-- won't compile: type mismatch

However, the more advanced relationships where you want to check whether the range of one template instance is included within the range of another instance cannot be expressed in the C++ template mechanism.

Every Bounded specification becomes a new type. Thus the compiler can check for type mismatches. It cannot check for more advanced relationships that might exist for those types.

VoidPointer
  • 17,651
  • 15
  • 54
  • 58
  • 2
    This proposed solution would only work for int and integral type ranges as a template parameterised on the value of any other type won't work (you can't parameterise a template on a float for example). – workmad3 Sep 29 '08 at 15:41
  • >"It cannot check for more advanced relationships that might exist for those types." I'm pretty sure you can compare compile-time integers at compile time. Isn't it all that's needed? – DeltA Jan 06 '21 at 10:16