I am trying to create a Fixed Point Arithmetics library : I call fixed point a number which has bits reserved for decimal part.
Here is the code :
#ifndef FIXEDPOINTNUMBER_HPP
#define FIXEDPOINTNUMBER_HPP
#include <type_traits>
#include <cstdint>
///////////////////////////////////////////////////////////
//////////////////// DECLARATION ////////////////////
///////////////////////////////////////////////////////////
/**
* @brief Provides fixed-point number calculations.
* @author Julien Vernay (JDM)
* @date 01-01-2018 (dd-mm-yyyy)
* @arg @c T Underlying type, no overhead
* @arg @c N Number of bits used for decimal part
* @details Fixed-Point Number uses an int value, so we only needs int manipulation with bitshift tricks instead of floating arithmetics.
* @details The underlying value @c val can be represented by : <em>VALUE = val / (2^N)</em>
*/
template<typename T, unsigned char N>
class FixedPointNumber {
public:
FixedPointNumber(); /**< @brief Constructs with 0 */
FixedPointNumber(T value, bool raw = 0); /**< @brief Constructs with a @c T value */
FixedPointNumber(float value); /**< @brief Constructs with a @c float value */
operator T() const; /**< @brief Casts to integer value of type @c T (eventually flooring) */
operator float() const; /**< @brief Casts to a float value */
T raw() const; /**< @brief Returns @c val without any casting */
template<unsigned char N2>
operator FixedPointNumber<T, N2>() const; /**< @brief Casts to another FixedPointNumber with same underlying type */
template<typename T2>
operator FixedPointNumber<T2, N>() const; /**< @brief Casts to another FixedPointNumber with same decimal part bits */
template<typename T2, unsigned char N2>
operator FixedPointNumber<T2, N2>() const; /**< @brief Casts to another FixedPointNumber */
FixedPointNumber<T, N>& operator+=(FixedPointNumber<T, N> const& rhs);
FixedPointNumber<T, N>& operator-=(FixedPointNumber<T, N> const& rhs);
FixedPointNumber<T, N>& operator*=(FixedPointNumber<T, N> const& rhs);
FixedPointNumber<T, N>& operator/=(FixedPointNumber<T, N> const& rhs);
FixedPointNumber<T, N> operator-() const;
bool operator==(FixedPointNumber<T, N> const& rhs) const;
bool operator>(FixedPointNumber<T, N> const& rhs) const;
private:
std::enable_if_t<std::is_integral_v<T>, T> val;
};
template<typename T, unsigned char N>
FixedPointNumber<T, N> operator+(FixedPointNumber<T, N> lhs, FixedPointNumber<T, N> const& rhs);
template<typename T, unsigned char N>
FixedPointNumber<T, N> operator-(FixedPointNumber<T, N> lhs, FixedPointNumber<T, N> const& rhs);
template<typename T, unsigned char N>
FixedPointNumber<T, N> operator*(FixedPointNumber<T, N> lhs, FixedPointNumber<T, N> const& rhs);
template<typename T, unsigned char N>
FixedPointNumber<T, N> operator/(FixedPointNumber<T, N> lhs, FixedPointNumber<T, N> const& rhs);
template<typename T, unsigned char N>
bool operator!=(FixedPointNumber<T, N> const& lhs, FixedPointNumber<T, N> const& rhs);
template<typename T, unsigned char N>
bool operator<(FixedPointNumber<T, N> const& lhs, FixedPointNumber<T, N> const& rhs);
template<typename T, unsigned char N>
bool operator>=(FixedPointNumber<T, N> const& lhs, FixedPointNumber<T, N> const& rhs);
template<typename T, unsigned char N>
bool operator<=(FixedPointNumber<T, N> const& lhs, FixedPointNumber<T, N> const& rhs);
///////////////////////////////////////////////////////////
//////////////////// DEFINITIONS ////////////////////
///////////////////////////////////////////////////////////
template<typename T, unsigned char N>
FixedPointNumber<T, N>::FixedPointNumber() : val(0) {}
template<typename T, unsigned char N>
FixedPointNumber<T, N>::FixedPointNumber(T value, bool raw) : val(raw ? value : value << N) {}
template<typename T, unsigned char N>
FixedPointNumber<T, N>::FixedPointNumber(float value) {
std::uint32_t value_int = *reinterpret_cast<std::uint32_t*>(&value);
std::uint32_t mantissa = (value_int & 0x007FFFFF) | 0x00800000;
std::int8_t exponent = ((value_int >> 23) & 0x000000FF) - 150 + N;
if (exponent >= 0)
mantissa <<= exponent;
else
mantissa >>= -exponent;
val = (value_int & 0x80000000) ? -static_cast<T>(mantissa) : static_cast<T>(mantissa);
}
template<typename T, unsigned char N>
FixedPointNumber<T, N>::operator T() const {
return val >> (val >= 0) ? N : -N;
}
template<typename T, unsigned char N>
FixedPointNumber<T, N>::operator float() const {
if (val == 0) return 0.f; //trivial case, needed to prevent infinite loops for CLZ
std::uint32_t mantissa = (val >= 0) ? val : -val;
std::uint8_t fbs = 31; //first bit set : fbs = floor(log2(mantissa))
#if defined(__GNUC__) //g++ compiler
fbs -= __builtin_clz(mantissa);
#elif defined(_MSC_VER) //MSVC compiler
fbs -= __lzcnt(mantissa);
#else //unknown compiler : using naive algorithm
for (std::uint32_t copy = mantissa; copy & 0x80000000; --fbs) copy <<= 1;
#endif
if (fbs <= 23)
mantissa <<= 23 - fbs;
else
mantissa >>= fbs - 23;
mantissa &= 0x007FFFFF; //keeping mantissa
mantissa |= (val < 0) ? 0x80000000 : 0; //sign
mantissa |= static_cast<std::uint32_t>(127 + fbs - N) << 23; //exponent
return *reinterpret_cast<float*>(&mantissa);
}
template<typename T, unsigned char N>
T FixedPointNumber<T, N>::raw() const {
return val;
}
template<typename T, unsigned char N>
template<unsigned char N2>
FixedPointNumber<T, N>::operator FixedPointNumber<T, N2>() const {
if (N >= N2)
return { static_cast<T>(val >> (N - N2)), true };
else
return { static_cast<T>(val << (N2 - N)), true };
}
template<typename T, unsigned char N>
template<typename T2>
FixedPointNumber<T, N>::operator FixedPointNumber<T2, N>() const {
return { static_cast<T2>(val), true };
}
template<typename T, unsigned char N>
template<typename T2, unsigned char N2>
FixedPointNumber<T, N>::operator FixedPointNumber<T2, N2>() const {
if (N >= N2)
return { static_cast<T2>(static_cast<T2>(val) >> (N - N2)), true };
else
return { static_cast<T2>(static_cast<T2>(val) << (N2 - N)), true };
}
template<typename T, unsigned char N>
FixedPointNumber<T, N>& FixedPointNumber<T, N>::operator+=(FixedPointNumber<T, N> const& rhs) {
val += rhs.val;
return *this;
}
template<typename T, unsigned char N>
FixedPointNumber<T, N>& FixedPointNumber<T, N>::operator-=(FixedPointNumber<T, N> const& rhs) {
val -= rhs.val;
return *this;
}
template<typename T, unsigned char N>
FixedPointNumber<T, N>& FixedPointNumber<T, N>::operator*=(FixedPointNumber<T, N> const& rhs) {
val = ((+val) * (+rhs.val)) >> N;
return *this;
}
template<typename T, unsigned char N>
FixedPointNumber<T, N>& FixedPointNumber<T, N>::operator/=(FixedPointNumber<T, N> const& rhs) {
val = ((+val) << N) / rhs.val;
return *this;
}
template<typename T, unsigned char N>
FixedPointNumber<T, N> FixedPointNumber<T, N>::operator-() const {
return { static_cast<T>(-val), true };
}
template<typename T, unsigned char N>
bool FixedPointNumber<T, N>::operator==(FixedPointNumber<T, N> const& rhs) const {
return val == rhs.val;
}
template<typename T, unsigned char N>
bool FixedPointNumber<T, N>::operator>(FixedPointNumber<T, N> const& rhs) const {
return val > rhs.val;
}
template<typename T, unsigned char N>
FixedPointNumber<T, N> operator+(FixedPointNumber<T, N> lhs, FixedPointNumber<T, N> const& rhs) {
return lhs += rhs;
}
template<typename T, unsigned char N>
FixedPointNumber<T, N> operator-(FixedPointNumber<T, N> lhs, FixedPointNumber<T, N> const& rhs) {
return lhs -= rhs;
}
template<typename T, unsigned char N>
FixedPointNumber<T, N> operator*(FixedPointNumber<T, N> lhs, FixedPointNumber<T, N> const& rhs) {
return lhs *= rhs;
}
template<typename T, unsigned char N>
FixedPointNumber<T, N> operator/(FixedPointNumber<T, N> lhs, FixedPointNumber<T, N> const& rhs) {
return lhs /= rhs;
}
template<typename T, unsigned char N>
bool operator!=(FixedPointNumber<T, N> const& lhs, FixedPointNumber<T, N> const& rhs) {
return !(lhs == rhs);
}
template<typename T, unsigned char N>
bool operator<(FixedPointNumber<T, N> const& lhs, FixedPointNumber<T, N> const& rhs) {
return rhs > lhs;
}
template<typename T, unsigned char N>
bool operator>=(FixedPointNumber<T, N> const& lhs, FixedPointNumber<T, N> const& rhs) {
return !(rhs > lhs);
}
template<typename T, unsigned char N>
bool operator<=(FixedPointNumber<T, N> const& lhs, FixedPointNumber<T, N> const& rhs) {
return !(lhs > rhs);
}
#endif
So to resume it, the class FixedPointNumber
contains only one variable of integral type T
, which the N
least significant bits are the decimal part, and the sizeof(T) - N
most significant bits are the integral part.
What was aimed first is to have non-integer value smaller than a float, and all the syntax and casting to go with it.
Operators +,-,*,/ and !=,<, >=, <= are not methods of the class in order to have better encapsulation.
Here is an example for a main.cpp
file :
#include "FixedPointNumber.hpp"
#include <iostream>
using namespace std;
using Nbr8 = FixedPointNumber<uint8_t, 8>; //domain : [0, 1[ epsilon = 1/256 8 bits
using Nbr16A = FixedPointNumber<int16_t, 11>; //domain : [-16, 16[ epsilon = 1/2048 16 bits
using Nbr16B = FixedPointNumber<uint16_t, 15>; //domain : [-1, 1[ epsilon = 1/32768 16 bits
int main() {
{
Nbr8 a = 0.37f, b = 0.52f;
float af = 0.37f, bf = 0.52f;
cout << af << " -> " << float(a) << "\t\tError (%) : " << 100 * (af - float(a)) / af << endl;
cout << bf << " -> " << float(b) << "\t\tError (%) : " << 100 * (bf - float(b)) / bf << endl;
cout << af + bf << " -> " << float(a + b) << "\t\tError (%) : " << 100 * (af + bf - float(a + b)) / (af + bf) << endl;
cout << "Nbr8 recap : 25% bits for about 0.5% error if interval correctly chosen" << endl << endl;
}
{
Nbr16B a = 0.37f, b = 0.52f;
float af = 0.37f, bf = 0.52f;
cout << af << " -> " << float(a) << "\t\tError (%) : " << 100 * (af - float(a)) / af << endl;
cout << bf << " -> " << float(b) << "\t\tError (%) : " << 100 * (bf - float(b)) / bf << endl;
cout << af + bf << " -> " << float(a + b) << "\t\t\tError (%) : " << 100 * (af + bf - float(a + b)) / (af + bf) << endl;
cout << "Nbr16 recap : 50% bits for about 0.002% error if interval correctly chosen" << endl << endl;
Nbr16A a2 = a, b2 = b;
cout << "switching point position, less precision but wider domain !" << endl;
cout << "a2 = " << float(a2) << " b2 = " << float(b2) << " AGAINST a1 = " << float(a) << " b1 = " <<float(b) << endl;
cout << "Notice that gap between two consecutive values is constant in a domain, contrary to floating point numbers." << endl << endl;
}
return 0;
}
main()
to demonstrate the usage of the class. No need to cover all of the library, just something that is "selling" point of your library. It might be precision of your values against built-in, or anything else. \$\endgroup\$ – Incomputable Jan 1 '18 at 21:25constexpr
andnoexcept
2) Some of your operators aren't found by ADL. The way to do this normally is to make themfriend
s. \$\endgroup\$ – Rakete1111 Jan 2 '18 at 23:52constexpr
, but now I realized that you use compiler builtins, and I don't know if they areconstexpr
too. I mean, the main reason is "why not?" IMO. Might make your variables more optimization friendly, and then you can use them in constant expressions, which is nice.noexcept
can also help the compiler by making optimizations if you are using exceptions (don't know if you are, if you are not, I don't think it makes a big difference). Nice that you ask, because I don't know. On further investigation, it doesn't matter. Ignore that part and cheers :) \$\endgroup\$ – Rakete1111 Jan 3 '18 at 0:14