patterncppMinor
Constrained number template
Viewed 0 times
numbertemplateconstrained
Problem
There are a lot of times where you need to ensure that a number stays within some constraints and I got annoyed of inventing a distinct class for every case so I decided to solve the problem once and for all. So I wrote a template which has semantics the same as a number except it checks the predicate each time its value is about to change.
Here are some design decisions:
Declarations:
```
/// \file
/// \brief Header file that describes the CheckedNumber class template.
/// \author Lyberta
/// \copyright GNU GPLv3 or any later version.
#pragma once
#include
#include
namespace ftz
{
namespace General
{
/// \brief A concept for a predicate.
template
concept bool Predicate()
{
return requires()
{
typename T::ValueType;
} &&
requires (typename T::ValueType value)
{
{T::Check(value)} -> void;
};
}
/// \brief A checked number.
/// \details Checked number is a number which can't have some values which are
/// otherwise possible for the underlying type. For example, a floating point
/// number which can't be negative. This class takes a predicate class which
/// defines the underlying type of the value and a static function to check the
/// value. Predicate must throw std::domain_error if the value is illegal.
/// \tparam T Type of the predicate.
template
class CheckedNumber
{
public:
using ValueType = typename T::ValueType; ///
CheckedNumber& operator+=(const U& val);
/// \brief
Here are some design decisions:
- Compile time binding of predicate. I don't need runtime polymorphism.
- The violation of predicate should throw exception.
- The template will decay to underlying type as early as possible to allow intermediate values violate the predicate.
- It should be usable in any construct that accepts fundamental arithmetic types.
- On the other hand, it should work for arbitrary precision types as long as they have the same interface as fundamental types.
- It should be possible to derive from the template to add additional functions.
Declarations:
```
/// \file
/// \brief Header file that describes the CheckedNumber class template.
/// \author Lyberta
/// \copyright GNU GPLv3 or any later version.
#pragma once
#include
#include
namespace ftz
{
namespace General
{
/// \brief A concept for a predicate.
template
concept bool Predicate()
{
return requires()
{
typename T::ValueType;
} &&
requires (typename T::ValueType value)
{
{T::Check(value)} -> void;
};
}
/// \brief A checked number.
/// \details Checked number is a number which can't have some values which are
/// otherwise possible for the underlying type. For example, a floating point
/// number which can't be negative. This class takes a predicate class which
/// defines the underlying type of the value and a static function to check the
/// value. Predicate must throw std::domain_error if the value is illegal.
/// \tparam T Type of the predicate.
template
class CheckedNumber
{
public:
using ValueType = typename T::ValueType; ///
CheckedNumber& operator+=(const U& val);
/// \brief
Solution
Headers
For forward declarations of streams, use `
#include
#include
#include
namespace ftz
{
namespace General
{
template
concept bool Predicate()
{
return requires()
{
typename T::ValueType;
} &&
requires (typename T::ValueType value)
{
{T::Check(value)} -> void;
};
}
/// \brief A checked number.
/// \details Checked number is a number which can't have some values which are
/// otherwise possible for the underlying type. For example, a floating point
/// number which can't be negative. This class takes a predicate class which
/// defines the underlying type of the value and a static function to check the
/// value. Predicate must throw std::domain_error if the value is illegal.
/// \tparam T Type of the predicate.
template
class CheckedNumber
{
public:
using ValueType = typename T::ValueType; ///
constexpr CheckedNumber& operator+=(const U& val);
/// \brief Subtracts a value from the underlying value.
/// \tparam U Type of the value.
/// \param[in] val Value to subtract.
/// \return Reference to this number.
/// \throw std::domain_error If the difference is illegal.
template
constexpr CheckedNumber& operator-=(const U& val);
/// \brief Mulptiplies the underlying value by another value.
/// \tparam U Type of the value.
/// \param[in] val Value to multiply by.
/// \return Reference to this number.
/// \throw std::domain_error If the product is illegal.
template
constexpr CheckedNumber& operator*=(const U& val);
/// \brief Divides the underlying value by another value.
/// \tparam U Type of the value.
/// \param[in] val Value to divide by.
/// \return Reference to this number.
/// \throw std::domain_error If the quotient is illegal.
template
constexpr CheckedNumber& operator/=(const U& val);
/// \brief Computes the remainder of the division by another value.
/// \tparam U Type of the value.
/// \param[in] val Value to divide by.
/// \return Reference to this number.
/// \throw std::domain_error If the remainder is illegal.
template
constexpr CheckedNumber& operator%=(const U& val);
/// \brief Computes bitwise AND of underlying value and another value.
/// \tparam U Type of the value.
/// \param[in] val Value to compute with.
For forward declarations of streams, use `
in the header - it's more lightweight than requiring and . You'll need the latter two for the implementation, of course.
We're missing , which is required for std::enable_if_t and std::is_base_of. (Aside: why not use std::is_base_of_v instead?). We also need , for std::move.
Terminology
I expect a "predicate" to return a boolean value. A function that throws should be called an "assertion".
It might also be better if we used the standard operator() for the test, and allow passing a lambda (or indeed, any std::function) as the predicate. Unfortunately, C++14 doesn't allow using a callable as a template parameter, so that would require us to carry a member around with each value.
Don't overload the short-circuiting operators
Overloading operator&& and operator|| can cause surprises for users, as the second argument is always evaluated, unlike with the built-in versions. Instead, allow the arguments to convert to bool in the usual way (remember that an explicit operator bool will be used in this context if necessary).
GetValue and SetValue seem pointless
These two functions add no value (!) to the class, since they just duplicate operator ValueType() and operator=.
Assignment operators should return *this
template
constexpr CheckedNumber& CheckedNumber::operator=(ValueType newvalue)
{
T::Check(newvalue);
value = std::move(newvalue);
// oops!
}
Implement copying
In case the predicate is expensive, allow copy construction and assignment. These methods can be noexcept.
Don't implement non-assigning operators
There's no need for operators such as (unary or binary) + and -, or ~, |, etc - we're happy with the standard fallback using the conversion to ValueType. The same goes for the functions - they will work just fine with the implicit conversion to the underlying type.
Assigning operators can be noexcept...
... if the underlying type's operator is noexcept. Let's assume that it is, since this is intended for arithmetic types.
Use the assignment operator instead of repeating test code
Consider the implementation of operator+:
{
ValueType result = value + val;
T::Check(result);
value = std::move(result);
return *this;
}
Most of that merely duplicates assigning from value + val, so re-use that:
{
return *this = value + val;
}
Write some tests
Without tests, it's not obvious that you've used | instead of ^ in the implementation of operator^=(), for example.
Modified code
I was able to reduce much of the code, without losing any of the functionality:
``#include
#include
#include
namespace ftz
{
namespace General
{
template
concept bool Predicate()
{
return requires()
{
typename T::ValueType;
} &&
requires (typename T::ValueType value)
{
{T::Check(value)} -> void;
};
}
/// \brief A checked number.
/// \details Checked number is a number which can't have some values which are
/// otherwise possible for the underlying type. For example, a floating point
/// number which can't be negative. This class takes a predicate class which
/// defines the underlying type of the value and a static function to check the
/// value. Predicate must throw std::domain_error if the value is illegal.
/// \tparam T Type of the predicate.
template
class CheckedNumber
{
public:
using ValueType = typename T::ValueType; ///
constexpr CheckedNumber& operator+=(const U& val);
/// \brief Subtracts a value from the underlying value.
/// \tparam U Type of the value.
/// \param[in] val Value to subtract.
/// \return Reference to this number.
/// \throw std::domain_error If the difference is illegal.
template
constexpr CheckedNumber& operator-=(const U& val);
/// \brief Mulptiplies the underlying value by another value.
/// \tparam U Type of the value.
/// \param[in] val Value to multiply by.
/// \return Reference to this number.
/// \throw std::domain_error If the product is illegal.
template
constexpr CheckedNumber& operator*=(const U& val);
/// \brief Divides the underlying value by another value.
/// \tparam U Type of the value.
/// \param[in] val Value to divide by.
/// \return Reference to this number.
/// \throw std::domain_error If the quotient is illegal.
template
constexpr CheckedNumber& operator/=(const U& val);
/// \brief Computes the remainder of the division by another value.
/// \tparam U Type of the value.
/// \param[in] val Value to divide by.
/// \return Reference to this number.
/// \throw std::domain_error If the remainder is illegal.
template
constexpr CheckedNumber& operator%=(const U& val);
/// \brief Computes bitwise AND of underlying value and another value.
/// \tparam U Type of the value.
/// \param[in] val Value to compute with.
Code Snippets
template <typename T>
constexpr CheckedNumber<T>& CheckedNumber<T>::operator=(ValueType newvalue)
{
T::Check(newvalue);
value = std::move(newvalue);
// oops!
}{
ValueType result = value + val;
T::Check(result);
value = std::move(result);
return *this;
}{
return *this = value + val;
}#include <type_traits>
#include <utility>
#include <istream>
namespace ftz
{
namespace General
{
template <typename T>
concept bool Predicate()
{
return requires()
{
typename T::ValueType;
} &&
requires (typename T::ValueType value)
{
{T::Check(value)} -> void;
};
}
/// \brief A checked number.
/// \details Checked number is a number which can't have some values which are
/// otherwise possible for the underlying type. For example, a floating point
/// number which can't be negative. This class takes a predicate class which
/// defines the underlying type of the value and a static function to check the
/// value. Predicate must throw std::domain_error if the value is illegal.
/// \tparam T Type of the predicate.
template<Predicate T>
class CheckedNumber
{
public:
using ValueType = typename T::ValueType; ///< Underlying type of the value.
/// \brief Constructor.
/// \param[in] val Value to set.
/// \throw std::domain_error If value is illegal.
constexpr CheckedNumber(ValueType val = ValueType{});
/// \brief Returns the underlying value.
/// \return Underlying value.
constexpr operator ValueType() const noexcept { return value; }
/// \brief Preincrements the underlying value.
/// \return Reference to this number.
/// \throw std::domain_error if result is illegal.
constexpr CheckedNumber& operator++();
/// \brief Postincrements the underlying value.
/// \return Copy of this number before increment.
/// \throw std::domain_error if the incremented value is illegal.
constexpr CheckedNumber operator++(int);
/// \brief Predecrements the underlying value.
/// \return Reference to this number.
/// \throw std::domain_error if result is illegal.
constexpr CheckedNumber& operator--();
/// \brief Postdecrements the underlying value.
/// \return Copy of this number before decrement.
/// \throw std::domain_error if the decremented value is illegal.
constexpr CheckedNumber operator--(int);
/// \brief Adds a value to the underlying value.
/// \tparam U Type of the value.
/// \param[in] val Value to add.
/// \return Reference to this number.
/// \throw std::domain_error If the sum is illegal.
template <typename U>
constexpr CheckedNumber& operator+=(const U& val);
/// \brief Subtracts a value from the underlying value.
/// \tparam U Type of the value.
/// \param[in] val Value to subtract.
/// \return Reference to this number.
/// \throw std::domain_error If the difference is illegal.
template <typename U>
constexpr CheckedNumber& operator-=(const U& val);
/// \brief Mulptiplies the underlying value by another value.
/// \tparam U Type of the value.
/// \param[in] val Value to multiply by.
/// \return Reference to this number.
/// \throw std::domain_error If the product is illegal.
template <typename U>
constexpr CheckedNumber& operator*=#include <gtest/gtest.h>
#include <cmath>
#include <sstream>
#include <stdexcept>
struct assert_even
{
using ValueType = int;
static constexpr void Check(ValueType n) {
if (n % 2)
throw std::domain_error("Violated EvenNumber constraint");
}
};
struct assert_small
{
using ValueType = char;
static constexpr void Check(ValueType n) {
if (n < -9 or 9 < n)
throw std::domain_error("Violated SmallNumber constraint");
}
};
struct assert_small_double
{
using ValueType = double;
static constexpr void Check(ValueType n) {
if (n < -9 or 9 < n)
throw std::domain_error("Violated SmallNumber constraint");
}
};
using ftz::General::CheckedNumber;
using EvenNumber = CheckedNumber<assert_even>;
using SmallNumber = CheckedNumber<assert_small>;
using SmallDouble = CheckedNumber<assert_small_double>;
TEST(EvenNumber, construct)
{
EXPECT_NO_THROW({EvenNumber a{0};});
EXPECT_THROW({EvenNumber a{1};}, std::domain_error);
}
TEST(EvenNumber, istream)
{
EvenNumber a{6};
std::istringstream in{"3 2"};
EXPECT_THROW(in >> a, std::domain_error);
EXPECT_EQ(a, 6); // unchanged
EXPECT_TRUE(in >> a);
EXPECT_EQ(a, 2);
EXPECT_FALSE(in >> a); // end of stream
EXPECT_EQ(a, 2); // unchanged
}
TEST(EvenNumber, increment)
{
EvenNumber a;
EXPECT_THROW({++a;}, std::domain_error);
EXPECT_THROW({a++;}, std::domain_error);
EXPECT_EQ(a, 0); // strong exception guarantee
}
TEST(EvenNumber, add)
{
EvenNumber a;
EXPECT_THROW({a+=1;}, std::domain_error);
EXPECT_EQ(a+=2, 2);
}
TEST(EvenNumber, false)
{
EvenNumber a;
EXPECT_FALSE(a);
}
TEST(EvenNumber, true)
{
EvenNumber a = 6;
EXPECT_TRUE(a);
}
TEST(EvenNumber, unary_negate)
{
EvenNumber a = 6;
EXPECT_EQ(-a, -6);
}
TEST(SmallNumber, construct)
{
EXPECT_THROW({SmallNumber a{-10};}, std::domain_error);
EXPECT_NO_THROW({SmallNumber a{-9};});
EXPECT_NO_THROW({SmallNumber a{};});
EXPECT_NO_THROW({SmallNumber a{9};});
EXPECT_THROW({SmallNumber a{10};}, std::domain_error);
}
TEST(SmallNumber, increment)
{
SmallNumber a{8};
EXPECT_EQ(++a, 9);
EXPECT_THROW({a++;}, std::domain_error);
EXPECT_EQ(a, 9); // strong exception guarantee
}
TEST(SmallNumber, multiply)
{
SmallNumber a{4};
EXPECT_EQ(a*4, 16);
EXPECT_THROW({a*=4;}, std::domain_error);
EXPECT_EQ(a, 4); // strong exception guarantee
}
TEST(SmallDouble, trigonometric)
{
SmallDouble a{0};
EXPECT_DOUBLE_EQ(std::sin(a), 0);
EXPECT_DOUBLE_EQ(std::cos(a), 1);
}Context
StackExchange Code Review Q#154403, answer score: 3
Revisions (0)
No revisions yet.