I've been working on this concept for the past few days where a variable_t
is of any type T
; assuming that it is at least arithmetic
, and that all of the available C++
language operators
that perform arithmetic
, comparison
, pre
&post
-increment
&decrement
, and bitwise
operations makes sense.
I have expressed my intentions within the source code throughout the comments, please refer to them to have an understanding of why I chose to do things a certain way. My class is currently header only to keep things as simple as possible. While having support of all of these operators; most of them have two versions, one that takes a variable_t<T>
as its rhs
value that may or may not be of the same type as the lhs
's type and the other will accept a plain value of any type T
for its rhs
and again it may or may not be the same
type as the lhs
type.
This does create a lot of boilerplate code; so forgive me if this class appears to be long to analyze. I do however believe that it is performing the calculations and conversions in the way that I have intended them to. Again you can refer to the comment sections for my intentions of how this class template is supposed to behave.
I also have support for std::ostream
and std::istream
to insert and extract it's member data to and from stream
objects. There are also two versions of function templates that will generate a variable_t<T>
based on the parameter value or type that is passed into the function to construct that type, this comes in handy with the use of auto
when declaring a type variable_t<T>
to create its instantiation.
#pragma once
#include <iostream>
namespace math {
/**************************************************************************
* NOTES:
*
* The design of variable_t<T> has the following properties:
* the LHS always has precedence over RHS and this is intentional.
*
* Type conversion will always be based on the type of the left hand side.
* Memory width(size) and precision will not always be preserved as truncation
* and or narrowing is to be expected.
*
* If widening or higher precission needs to be preserved, this will automatically
* be done with the use of the key word auto and the variable_t<Ty> creation functions.
*
* ----------------------------------------//
* Example: - Truncation expected //
* ----------------------------------------//
*
* // This will result in f1 as still being a float type.
* variable_t<float> f1(5.2f);
* variable_t<double> d1(7.9)
* variable_t<std::uint64_t> ui64(100);
* f1 += d1 / ui64;
*
* f1 == 5.279
* ----------------------------------------//
*
* If the keyword auto is used as such:
* auto x = d1 + f1;
* then in this case x should end up being a variable_t<double>
* and widening and precision will be reserved
*
* This type of design is intentional as I believe that it gives
* the user more flexibility to have the choice to either truncate
* data when the extra bits are not needed or to preserve data when
* it is necessary.
*
* This allows the library to be versatile and prevents any user
* from having to be forced to some specific conformance or policy
* giving them the flexibility they need.
*
* -------------------------------------------------------------------
*
* Any division by 0 will result in the LHS being 0.
* no excpetions are thrown. This is something the user
* must be aware of, and this is intentional!
*
* Example:
* // Generates
* auto t1 = variable( 3.5 ); variable_t<double> t1( 3.5 );
* auto t2 = variable( 4 ); variable_t<int> t2( 4 );
*
*
* auto t3 /= (t1 + 0.5 - t2);
*
* t3 /= (3.5 + 0.5 - 4)
* t3 /= (0) variable_t<double> t3(0.0);
*
* -------------------------------------------------------------------
*
* For all of the Bitwise operators, typically doing bitwise calculations
* requires that both arguments are of an integral type. In order for this
* class to support all of the bitwise operators it is legal to do bitwise
* operations on floating point types, however the user must be aware of the
* fact that both the LHS and RHS are implicilty converted or casted to int type
* then the bitwise calculation is performed and then the resulting value is
* used to assign the lhs's value being casted back to lhs's type.
*
* Example:
*
* ----------------------------------------//
*
* variable_t<float> f1{ 5.2 };
* variable_t<double> d1{ 7.9 };
* variable_t<std::uint64_t> ui64{ 0 };
*
* ui64 = f1 & d1;
*
* // ui64 will have a value of 5.
*
* // Let's check to see the conversions
* // f1 converted to int = 5;
* // d1 converted to int = 7;
* // 5 & 7 = 5;
* // then 5 is casted back to uint64_t and is assigned
* // to ui64
*/
template<typename Ty>
class variable_t {
Ty t;
public:
// -----------------------------------------------
// Constructors & Access Operators
// Default Constructor
inline variable_t() : t{ 0 } {}
// Explicit Constructor
explicit inline variable_t(Ty val) : t{ val } {}
// Copy Constructor with both types being the same
inline variable_t(const variable_t<Ty>& rhs) { this->t = rhs(); }
// Copy Constructor with both types being different
template<typename T>
inline variable_t(const variable_t<T>& rhs) { this->t = static_cast<Ty>(rhs()); }
// Basic operator() returns internal private member
// instead of having or using an accessor function
Ty operator()() const { return t; }
// Same as above: any value passed has no effect as it just returns member
// this isn't necessary but just another way to access the internal member
Ty operator[](unsigned n) const { return t; }
// Overloaded operator() that takes a value of type Ty and will changes it's state or value
variable_t<Ty>& operator()(const Ty t) {
variable_t<Ty> v{ t };
*this = v;
return *this;
}
// ---------------------
// Assignment Operators
// Assignment: both types are the same: variable_t<Ty> = variable_t<Ty>
inline variable_t<Ty>& operator=(const variable_t<Ty>& rhs) {
this->t = rhs();
return *this;
}
// Assignment: both types are different: variable_t<Ty> = variable_t<T> - rhs is casted to lhs
template<typename T>
inline variable_t<Ty>& operator=(const variable_t<T>& rhs) {
this->t = static_cast<Ty>(rhs());
return *this;
}
// Assignment: both types are the same: variable_t<Ty> = Ty
inline variable_t<Ty>& operator=(const Ty& rhs) {
this->t = rhs;
return *this;
}
// Assignment: both types are different: variable_t<Ty> = T - rhs is casted to lhs
template<typename T>
inline variable_t<Ty>& operator=(const T& rhs) {
this->t = static_cast<Ty>(rhs);
return *this;
}
// Compound Assignment
// Addition
template<typename T>
inline variable_t<Ty>& operator+=(const variable_t<T>& rhs) {
this->t += static_cast<Ty>(rhs());
return *this;
}
template<typename T>
inline variable_t<Ty>& operator+=(const T& rhs) {
this->t += static_cast<Ty>(rhs);
return *this;
}
// Subtraction
template<typename T>
inline variable_t<Ty>& operator-=(const variable_t<T>& rhs) {
this->t -= static_cast<Ty>(rhs());
return *this;
}
template<typename T>
inline variable_t<Ty>& operator-=(const T& rhs) {
this->t -= static_cast<Ty>(rhs);
return *this;
}
// Multiplication
template<typename T>
inline variable_t<Ty>& operator*=(const variable_t<T>& rhs) {
this->t *= static_cast<Ty>(rhs());
return *this;
}
template<typename T>
inline variable_t<Ty>& operator*=(const T& rhs) {
this->t *= static_cast<Ty>(rhs);
return *this;
}
// Division
template<typename T>
inline variable_t<Ty>& operator/=(const variable_t<T>& rhs) {
if (rhs() == 0) {
this->t = 0;
}
else {
Ty inv = 1 / static_cast<Ty>(rhs());
this->t *= inv;
}
return *this;
}
template<typename T>
inline variable_t<Ty>& operator/=(const T& rhs) {
if (rhs == 0) {
this->t = 0;
}
else {
Ty inv = 1 / static_cast<Ty>(rhs);
this->t *= inv;
}
return *this;
}
// Modulus
template<typename T>
inline variable_t<Ty>& operator%=(const variable_t<T>& rhs) {
this->t %= static_cast<Ty>(rhs());
return *this;
}
template<typename T>
inline variable_t<Ty>& operator%=(const T& rhs) {
this->t %= static_cast<Ty>(rhs);
return *this;
}
// Bitwise &
template<typename T>
inline variable_t<Ty>& operator&=(const variable_t<T>& rhs) {
int val = static_cast<int>( this->t );
val &= static_cast<int>(rhs());
variable_t<Ty> v{ static_cast<Ty>( val ) };
*this = v;
return *this;
}
template<typename T>
inline variable_t<Ty>& operator&=(const T& rhs) {
int val = static_cast<int>(this->t);
val &= static_cast<int>(rhs);
variable_t<Ty> v{ static_cast<Ty>(val) };
*this = v;
return *this;
}
// Bitwise |
template<typename T>
inline variable_t<Ty>& operator|=(const variable_t<T>& rhs) {
int val = static_cast<int>(this->t);
val |= static_cast<int>(rhs());
variable_t<Ty> v{ static_cast<Ty>(val) };
*this = v;
return *this;
}
template<typename T>
inline variable_t<Ty>& operator|=(const T& rhs) {
int val = static_cast<int>(this->t);
val |= static_cast<int>(rhs);
variable_t<Ty> v{ static_cast<Ty>(val) };
*this = v;
return *this;
}
// Bitwise ^
template<typename T>
inline variable_t<Ty>& operator^=(const variable_t<T>& rhs) {
int val = static_cast<int>(this->t);
val ^= static_cast<int>(rhs());
variable_t<Ty> v{ static_cast<Ty>(val) };
*this = v;
return *this;
}
template<typename T>
inline variable_t<Ty>& operator^=(const T& rhs) {
int val = static_cast<int>(this->t);
val ^= static_cast<int>(rhs);
variable_t<Ty> v{ static_cast<Ty>(val) };
*this = v;
return *this;
}
// Bitwise <<
template<typename T>
inline variable_t<Ty>& operator<<=(const variable_t<T>& rhs) {
int val = static_cast<int>(this->t);
val <<= static_cast<int>(rhs());
variable_t<Ty> v{ static_cast<Ty>(val) };
*this = v;
return *this;
}
template<typename T>
inline variable_t<Ty>& operator<<=(const T& rhs) {
int val = static_cast<int>(this->t);
val <<= static_cast<int>(rhs);
variable_t<Ty> v{ static_cast<Ty>(val) };
*this = v;
return *this;
}
// Bitwise >>
template<typename T>
inline variable_t<Ty>& operator>>=(const variable_t<T>& rhs) {
int val = static_cast<int>(this->t);
val >>= static_cast<int>(rhs());
variable_t<Ty> v{ static_cast<Ty>(val) };
*this = v;
return *this;
}
template<typename T>
inline variable_t<Ty>& operator>>=(const T& rhs) {
int val = static_cast<int>(this->t);
val >>= static_cast<int>(rhs);
variable_t<Ty> v{ static_cast<Ty>(val) };
*this = v;
return *this;
}
// ------------------------------------
// Arithmetic Operators
// Unary - Addition
inline variable_t<Ty> operator+() const {
return variable_t<Ty>(+this->t);
}
// Unary - Negation
inline variable_t<Ty> operator-() const {
return variable_t<Ty>(-this->t);
}
// Compliment
inline variable_t<Ty>& operator~() {
this->t = ~this->t;
return *this;
}
// Binary
// Addition
template<typename T>
inline variable_t<Ty> operator+(const variable_t<T>& rhs) const {
return variable_t<Ty>(this->t + static_cast<Ty>(rhs()));
}
template<typename T>
inline variable_t<Ty> operator+(const T& rhs) const {
return variable_t<Ty>(this->t + static_cast<Ty>(rhs));
}
// Subtraction
template<typename T>
inline variable_t<Ty> operator-(const variable_t<T>& rhs) const {
return variable_t<Ty>(this->t - static_cast<Ty>(rhs()));
}
template<typename T>
inline variable_t<Ty> operator-(const T& rhs) const {
return variable_t<Ty>(this->t + static_cast<Ty>(rhs));
}
// Multiplication
template<typename T>
inline variable_t<Ty> operator*(const variable_t<T>& rhs) const {
return variable_t<Ty>(this->t * static_cast<Ty>(rhs()));
}
template<typename T>
inline variable_t<Ty> operator*(const T& rhs) const {
return variable_t<Ty>(this->t * static_cast<Ty>(rhs));
}
// Division
template<typename T>
inline variable_t<Ty> operator/(const variable_t<T>& rhs) const {
variable_t<Ty> var(*this);
if (rhs() != 0) {
Ty inv = 1 / static_cast<Ty>(rhs());
var.t = var.t * inv;
}
else {
var = variable_t<Ty>(0);
}
return var;
}
template<typename T>
inline variable_t<Ty> operator/(const T& rhs) const {
variable_t<Ty> var( *this );
if (rhs != 0) {
Ty inv = 1 / static_cast<Ty>(rhs);
var.t = var.t * inv;
}
else {
var = variable_t<Ty>(0);
}
return var;
}
// Modulus
template<typename T>
inline variable_t<Ty> operator%(const variable_t<T>& rhs) const {
return variable_t<Ty>(this->t % static_cast<Ty>(rhs()));
}
template<typename T>
inline variable_t<Ty> operator%(const T& rhs) const {
return variable_t<Ty>(this->t % static_cast<Ty>(rhs));
}
// Bitwise &
template<typename T>
inline variable_t<Ty> operator&(const variable_t<T>& rhs) const {
int lhs = static_cast<int>(this->t);
int val = lhs & static_cast<int>( rhs() );
return variable_t<Ty>(static_cast<Ty>(val));
}
template<typename T>
inline variable_t<Ty> operator&(const T& rhs) const {
int lhs = static_cast<int>(this->t);
int val = lhs & static_cast<int>(rhs);
return variable_t<Ty>(static_cast<Ty>(val));
}
// Bitwise |
template<typename T>
inline variable_t<Ty> operator|(const variable_t<T>& rhs) const {
int lhs = static_cast<int>(this->t);
int val = lhs | static_cast<int>(rhs());
return variable_t<Ty>(static_cast<Ty>(val));
}
template<typename T>
inline variable_t<Ty> operator|(const T& rhs) const {
int lhs = static_cast<int>(this->t);
int val = lhs | static_cast<int>(rhs);
return variable_t<Ty>(static_cast<Ty>(val));
}
// Bitwise ^
template<typename T>
inline variable_t<Ty> operator^(const variable_t<T>& rhs) const {
int lhs = static_cast<int>(this->t);
int val = lhs ^ static_cast<int>(rhs());
return variable_t<Ty>(static_cast<Ty>(val));
}
template<typename T>
inline variable_t<Ty> operator^(const T& rhs) const {
int lhs = static_cast<int>(this->t);
int val = lhs ^ static_cast<int>(rhs);
return variable_t<Ty>(static_cast<Ty>(val));
}
// Bitwise <<
template<typename T>
inline variable_t<Ty> operator<<(const variable_t<T>& rhs) const {
int lhs = static_cast<int>(this->t);
int val = lhs << static_cast<int>(rhs());
return variable_t<Ty>(static_cast<Ty>(val));
}
template<typename T>
inline variable_t<Ty> operator<<(const T& rhs) const {
int lhs = static_cast<int>(this->t);
int val = lhs << static_cast<int>(rhs);
return variable_t<Ty>(static_cast<Ty>(val));
}
// Bitwise >>
template<typename T>
inline variable_t<Ty> operator>>(const variable_t<T>& rhs) const {
int lhs = static_cast<int>(this->t);
int val = lhs >> static_cast<int>(rhs());
return variable_t<Ty>(static_cast<Ty>(val));
}
template<typename T>
inline variable_t<Ty> operator>>(const T& rhs) const {
int lhs = static_cast<int>(this->t);
int val = lhs >> static_cast<int>(rhs);
return variable_t<Ty>(static_cast<Ty>(val));
}
// -------------------------------------------------
// Increment - Decrement Operators
variable_t<Ty>& operator++() {
this->t++;
return *this;
}
variable_t<Ty> operator++(int) {
auto v(*this); // copy
operator++(); // pre-increment
return v; // return old value
}
variable_t<Ty>& operator--() {
this->t--;
return *this;
}
variable_t<Ty>& operator--(int) {
auto v(*this); // copy
operator--(); // pre-decrement
return v; // return old value
}
// ------------------------------------------------
// Comparison operators
template<typename T>
inline bool operator==(const variable_t<T>& rhs) const{
return (this->t == static_cast<Ty>(rhs()));
}
template<typename T>
inline bool operator==(const T& rhs) const {
return (this->t == static_cast<Ty>(rhs));
}
template<typename T>
inline bool operator!=(const variable_t<T>& rhs) const {
return !(*this == rhs);
}
template<typename T>
inline bool operator!=(const T& rhs) const {
return !(this == rhs);
}
template<typename T>
inline bool operator<(const variable_t<T>& rhs) const {
return (this->t < static_cast<Ty>(rhs()));
}
template<typename T>
inline bool operator<(const T& rhs) const {
return (this->t < static_cast<Ty>(rhs));
}
template<typename T>
inline bool operator<=(const variable_t<T>& rhs) const {
return (this->t <= static_cast<Ty>(rhs()));
}
template<typename T>
inline bool operator<=(const T& rhs) const {
return (this->t <= static_cast<Ty>(rhs));
}
template<typename T>
inline bool operator>(const variable_t<T>& rhs) const {
return (this->t > static_cast<Ty>(rhs()));
}
template<typename T>
inline bool operator>(const T& rhs) const {
return (this->t > static_cast<Ty>(rhs));
}
template<typename T>
inline bool operator>=(const variable_t<T>& rhs) const {
return (this->t >= static_cast<Ty>(rhs()));
}
template<typename T>
inline bool operator>=(const T& rhs) const {
return (this->t >= static_cast<Ty>(rhs));
}
};
template<typename T>
auto variable(const T& t) {
return variable_t<T>(t);
}
template<typename T>
auto variable(T& t) {
return variable_t<T>(t);
}
template<typename T>
inline std::ostream& operator<<(std::ostream& os, const variable_t<T>& v) {
return os << v();
}
template<typename T>
inline std::istream& operator>>(std::istream& is, variable_t<T>& v) {
T val;
is >> val;
v = variable_t<T>(val);
return is;
}
}
In order to keep this short I did not include any use case application code; only the header file itself. I have tested most of the operators and so far they appear to be behaving as expected. Feel free to experiment with this class.
The idea here is that you can easily mix different types and perform any kind of calculation that you normally couldn't easily do as this class handles all of the type conversions. However there are some underlying aspects about this class that the user has to be aware of before using it and they are described in the comment sections.
How I plan on using this class:
- It will be used within other classes that I'm working on not shown here
term_t<T>
- a single term of an algebraic expression such as2x^2 + 3x
where2x^2
is a term and3x
is another term. Here thevariable_t<T>
class would be used in place of thex
in those terms.template<typename... Args> class expression_t;
- The fullrhs
orlhs
algebraic expression.
What I would like to know:
- What are your general thoughts about this overall design?
- Are there any possible improvements that can be made?
- Are there any corner cases or gotcha's that I may have missed or overlooked?
- Would this be considered following modern c++ practices?
- I do not have support for
c++20
so the proposed<=>
operator is not available to me. I am currently bound toc++17
- Are there any other operators that should be included, or excluded?
- What could I do to keep or make this as generic, portable, readable and reusable as possible?
-Edit- -- Other features I might add
I'm thinking about adding in a simple feature to this class that would handle information about it's internal value. This would be determined at compile time if possible, and would be calculated during run time when and where needed, for example the internal value was changed, then some of these fields or properties would then need to be updated as well. I could possibly use a simple struct that would contain the following information about its value such as:
is_zero
,is_one
is_even
,is_odd
is_prime
,is_composite
is_real
,is_complex
is_negative
,is_positive
is_power_of_two
, etc.and in addition to these property traits having available support for functions that would return the correct result. I could either have a single function for each trait or I could have a single function that takes a single parameter and depending on that parameter, it would then query for that specific trait. If I choose the second option I could even have it check for more than one trait in a single function call. example:
struct var_props { /* fields */ }; std::vector<bool> getProperties( var_props& props, SomeEnum val ) { /*...*/ }; // and let's say we have a `variable_t<unsigned long> x{ 42 }; var_props props; std::vector<bool> res = x.getProperties( props, IS_COMPLEX | IS_ODD | IS_ONE ); // then the query above would produce and fill out the entire property struct, // and would return a `vector<bool>` containing `{false, false, false}` // Or res = x.getProperties( props, IS_REAL | IS_EVEN | IS_PRIME | IS_COMPLEX ); // and this would yield { true, true, false, false }
The idea above is which ever place the enum above is where the resulting bool to that query would be placed into the vector. Or I could have this function be of a variadic type that would take
1
toN
arguments. Either way is fine by me as long as they would produce the correct results, are fairly computationally fast and don't provide too much of an over head. I could even potentially have this as an outside function that takes avariable_t<T>
with any value and it would then generate the necessary properties instead of the class doing itself. I'm not sure which way I want to go with this as of right now, I'm still in the thought - design process.
operator()
that takes a value of typeTy
that will update or change the internal member's value or state. \$\endgroup\$