Creating a custom int that will be enforced always in a certain range; how to go through whole overflows?

As in the title. As an exercise, I wanted to create an int that would provide restrictions on its value and forbid setting values ​​outside the specified range.

This is how I tried to approach this:

#include <cassert> #include <cstdint> #include <iostream> using namespace std; int main(); template<typename valtype, valtype minval, valtype maxval> class ConstrainedValue { valtype val; static bool checkval (valtype val) { return minval <= val && val <= maxval; } public: ConstrainedValue() : val{minval} // so that we're able to write ConstrainedValue i; { assert(checkval(val)); } ConstrainedValue(valtype val) : val{val} { assert(checkval(val)); } ConstrainedValue &operator = (valtype val) { assert(checkval(val)); this->val = val; return *this; } operator const valtype&() // Not needed here but can be; safe since it returns a const reference { return val; } friend ostream &operator << (ostream& out, const ConstrainedValue& v) // Needed because otherwise if valtype is char the output could be bad { out << +v.val; return out; } friend istream &operator >> (istream& in, ConstrainedValue& v) // this is horrible ugly; I'd love to know how to do it better { valtype hlp; auto hlp2 = +hlp; in >> hlp2; assert(checkval(hlp2)); v.val = hlp2; return in; } }; int main() { typedef ConstrainedValue<uint_least8_t, 0, 100> ConstrainedInt; ConstrainedInt i; cin >> i; cout << i; return 0; } 

The problem is ... this does not work. If this custom integer is given values ​​that overwhelm its base type, it simply sets the erroneous values.

For example, suppose we have a range limit of [0; 100], and the base type is uint_least8_t, as in the example above. uint_least8_t evaluate char or unsigned char, I'm not sure which one. Try downloading this program with different values:

 10 10 

Nice. Works.

 101 test: test.cpp:52: std::istream& operator>>(std::istream&, ConstrainedValue<unsigned int, 0u, 100u>&): Assertion `checkval(hlp2)' failed. Aborted 

Haha Exactly what I wanted.

But:

 257 1 

Yes. Overflow, truncation, wrong value, failed to check range correctly.

How to solve this problem?

+6
source share
4 answers

I think you have a problem with the specification, which, unfortunately, the implementation was not solved automatically.

Once you write: ConstrainedValue(valtype val) : val{val} , you lose hope that you can detect overflow, because the conversion to valtype happens before the code is called. Because if uint_least8_t translates to an unsigned char , which seems to happen in your (and mine) implementation, (uint_least8_t) 257 2 .

To be able to detect overflows, you must use larger integral types in your constructors and operator = methods.

IMHO, you should use the template constructor, operator = and checkval :

 template<typename valtype, valtype minval, valtype maxval> class ConstrainedValue { valtype val; template<typename T> static bool lt(valtype v, T other) { if (v <= 0) { if (other >= 0) return true; else return static_cast<long>(v) <= static_cast<long>(other); } else { if (other <= 0) return false; else return static_cast<unsigned long>(v) <= static_cast<unsigned long>(other); } } template <typename T> static bool checkval (T val) { return lt(minval, val) && (! lt(maxval, val)); } public: ConstrainedValue() : val{minval} // so that we're able to write ConstrainedValue i; { assert(checkval(val)); } template<typename T> ConstrainedValue(T val) : val{val} { assert(checkval(val)); } template<typename T> ConstrainedValue &operator = (T val) { assert(checkval(val)); this->val = val; return *this; } operator const valtype&() // Not needed here but can be; safe since it returns a const reference { return val; } 

Thus, the compiler will automatically select the correct type to avoid early overflow: you use the original type in checkval and use the best of long long and unsigned long long for comparisons, with caution to compare with a signature / unsigned (no compilation warning)!

In fact, lt could be written easier if you accept a possible (harmless) warning about a mismatch with the signature / unsigned:

  template<typename T> static bool lt(valtype v, T other) { if (v <= 0) && (other >= 0) return true; else if (v >= 0) && (other <= 0) return false; else return v <= other; } } 

A warning may occur if one of valtype and T is signed and the other is unsigned. This is harmless because cases where v and others have opposite signs are explicitly handled, and if both are negative, they must be signed. Thus, this can only happen when it is signed and the other unsigned, but both are positive. In this case, sentence 5 (5 expressions from the standard for the C ++ programming language, Β§ 10) guarantees that the largest type with unsigned priority will be used, which means that it will be correct for positive values. And this avoids the possible useless conversion to unsigned long long .

But there is still a case that I cannot handle properly: an injector. Until you decrypt it, you cannot be sure whether to enter the input value in long long or in unsigned long long (provided that they are the largest possible integral types). The cleanest way I can imagine is to get the value as a string and decode it manually. Since there are many angular cases, I would advise you:

  • get it as a string first
  • if the first char is minus - decodes it to long long
  • else decodes it unsigned long long

It will still give strange results for really big numbers, but this is the best I can:

 friend std::istream &operator >> (std::istream& in, ConstrainedValue& v) { std::string hlp; in >> hlp; std::stringstream str(hlp); if (hlp[0] == '-') { long long hlp2; str >> hlp2; assert(checkval(hlp2)); v.val = static_cast<valtype>(hlp2); } else { unsigned long long hlp2; str >> hlp2; assert(checkval(hlp2)); v.val = static_cast<valtype>(hlp2); } return in; } 
+3
source

Use the largest possible version for comparison. Suppose it is intmax_t . Change your code as below:

 template<typename valtype, intmax_t minval, intmax_t maxval> class ConstrainedValue { valtype val; // In special case of `uintmax_t` the comparison should happen with `uintmax_t` only, in other cases it will be `intmax_t` using Compare = typename std::conditional<std::is_same<valtype, uintmax_t>::value, uintmax_t, intmax_t>::type; static bool checkval (valtype val) { // this will cover all the scenarios of smaller integer values return Compare(minval) <= Compare(val) && Compare(val) <= Compare(maxval); } ... 

This should solve the int size problem. I also see other issues with another piece of code that deserve a new question.

+1
source

I don’t think it is possible to detect overflow on the way to your constructor, because it is executed to execute your constructor argument, so when it reaches your constructor body, it is already overflow.

A possible workaround would be to accept large types in your interface, and then perform validation. For example, you can take long int in your interface and then save them inside valtype . Since you still do border checks, it should be safe enough.

0
source

I made a decision from Serge Balesta and from my research on how operator >> works. It looks like this:

 #include <cassert> #include <cstdint> #include <iostream> using namespace std; int main(); template<typename valtype, valtype minval, valtype maxval> class ConstrainedValue { valtype val; template<typename T> static bool lt(valtype v, T other) { if (v <= 0) { if (other >= 0) return true; else return static_cast<long>(v) <= static_cast<long>(other); } else { if (other <= 0) return false; else return static_cast<unsigned long>(v) <= static_cast<unsigned long>(other); } } template <typename T> static bool checkval (T val) { return lt(minval, val) && (! lt(maxval, val)); } public: ConstrainedValue() : val{minval} { assert(checkval(val)); } template <typename T> ConstrainedValue(T val) : val{val} { assert(checkval(val)); } template <typename T> ConstrainedValue &operator = (T val) { assert(checkval(val)); this->val = val; return *this; } operator const valtype&() { return val; } friend ostream &operator << (ostream& out, const ConstrainedValue& v) { out << +v.val; return out; } friend istream &operator >> (istream& in, ConstrainedValue& v) { auto hlp = +v.val; // I think it safe to assume that hlp will have at least as much precision as v.val? in >> hlp; assert(in.good()); // In case of input overflow this fails. assert(checkval(hlp)); v.val = hlp; return in; } }; int main() { typedef ConstrainedValue<uint_least8_t, 0, 100> ConstrainedInt; ConstrainedInt i; cin >> i; cout << i; return 0; } 

I think it covers both issues: passing overflows to the constructor or operator = and overflowing the input. In the latter case, according to http://www.cplusplus.com/reference/locale/num_get/get/ , the input will be written numeric_limits::max() or numeric_limits::lowest() in v.val , which should cover most scenarios ; but if maxval is equal to numeric_limits::max() or minval is equal to numeric_limits::lowest() , we can check in.good() , which must necessarily give false in such situations. Of course, there is always a problem that v.val may be of type char , in which case operator >> will actually write in auto hlp = +v.val , which will have a larger type, which could prevent in.good() detecting overflow . However, such cases will be handled by Serge with the improved checkval() function.

This, hopefully, works by assuming that auto hlp = +v.val will necessarily have at least the same large type as v.val . If the standard says otherwise, or if I missed some possible scenarios, please correct me.

0
source

All Articles