4
\$\begingroup\$

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 rhsand 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 as 2x^2 + 3x where 2x^2 is a term and 3x is another term. Here the variable_t<T> class would be used in place of the x in those terms.
    • template<typename... Args> class expression_t; - The full rhs or lhs 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 to c++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 to N 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 a variable_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.

\$\endgroup\$
1
  • \$\begingroup\$ I updated the original class above and added support for an overloaded operator() that takes a value of type Ty that will update or change the internal member's value or state. \$\endgroup\$ Jun 22, 2019 at 18:32

1 Answer 1

3
\$\begingroup\$
  1. Your code has trailing whitespaces. Remove them. On Emacs, for example, I use:

    M-x delete-trailing-whitespace
    
  2. There is much debate on #pragma once. (See, for example, #pragma once vs include guards?) Personally, I don't use #pragma once, but I am not opposed to it either. Other people may disagree.

  3. You put your code into the namespace math. This is great, but math seems to be too common. Consider a more creative name.

  4. Your guidelines on truncation are plausible. x += y should not modify the type of x. That said, I think that f1 + d1 should be of type variable_t<double> instead of variable_t<float>. The user should cast manually if information loss is desired:

    f1 + static_cast<variable_t<float>>(d1)
    
  5. Please! Don't suppress errors caused by division by 0. This will cause much more problem than you would think. Throw an exception. This does not cause any degradation in performance because you already check the case of 0 anyway.

  6. Instead of casting floating point types to integer types and then doing bitwise operations, why not ban the operations on types for which the corresponding operations are not available, just like you do for other operators?

  7. Member functions defined in class are automatically inline. There is no point in marking them inline again. Instead of

    inline variable_t() : t{ 0 } {}
    

    Use

    variable_t() : t{ 0 } {}
    

    Similarly for other functions.

  8. Instead of direct-initializing the underlying object with the value 0, consider value-initializing to keep consistent with standard practice.

    variable_t() : t{} {}
    

    You can also use an in-class member initializer.

  9. You define the copy constructor:

    inline variable_t(const variable_t<Ty>& rhs) { this->t = rhs(); }
    

    along with the copy assignment operator. They are redundant. What's more, they force a copy when Ty can actually be moved. Leave them out.

  10. You define a constructor to convert between different variable_t types. (FWIW, technically they are not copy constructors.)

    template<typename T>
    inline variable_t(const variable_t<T>& rhs) { this->t = static_cast<Ty>(rhs()); }
    

    Use an initializer clause instead of assignment. Also, you should constrain this constructor and mark it as explicit when T cannot be implicitly converted to Ty, possibly with the help of SFINAE.

    template <typename T,
              std::enable_if_t<std::is_convertible_v<T, Ty>, int> = 0>
    variable_t(const variable_t<T>& rhs)
        :t(rhs)
    {
    }
    
    template <typename T,
              std::enable_if_t<std::is_constructible_v<Ty, T> &&
                               !std::is_convertible_v<T, Ty>, int> = 0>
    explicit variable_t(const variable_t<T>& rhs)
        :t(rhs)
    {
    }      
    

    With C++20, this becomes easier:

    template <typename T>
        requires std::Constructible<Ty, T>
    explicit(std::ConvertibleTo<T, Ty>) variable_t(const variable_t<T>& rhs)
        :t(rhs)
    {
    }
    

    Similarly for operator= with a different variable_t type and with a different T type.

  11. The use of operator() to access the underlying value is a bit confusing. The standard practice is to use operator*. A conversion operator to Ty is also OK.

  12. Don't support an operator if is does not naturally make sense. In your case, just drop operator[]. It only causes confusion.

  13. Using operator() to set the underlying value is counterintuitive. Drop it as you already support operator=, which is made for this purpose.

  14. Don't use this->t when t is sufficient.

  15. Your assignment operators (including +=, -=, etc.) are duplicating the work of the constructors. Why not support only variable_t<Ty> and let the constructors handle the different types? This way, you only need one operator=, one operator+=, one operator-=, and so on.

  16. Why do use first calculate the reciprocal and then multiply it in your implementation of operator/= instead of just using division?

  17. Comparison operators are symmetrical operators. Such operators are generally implemented as non-member functions to enable conversion on both sides. This way you need one instead of three. (And in fact you only provided two of them!)

  18. What is the point in having an overload of variable for non-const lvalue references? I can't see.

    template<typename T>
    auto variable(T& t) {
        return variable_t<T>(t);
    }
    
  19. You use

    v = variable_t<T>(val);
    

    in your implementation of operator>>. This way, you are duplicating the code of the constructors. Just use

    v = val;
    
  20. You don't need to #include <iostream> just to provide the I/O operations. #include <iosfwd> is sufficient. The user is responsible for #include <iostream> when instantiating them. (See What is the <iosfwd> header?)

  21. Consider supporting all std::basic_istreams and std::basic_ostreams, not just std::istream and std::ostream. Instead of

    template<typename T>
    inline std::ostream& operator<<(std::ostream& os, const variable_t<T>& v)
    

    Use

    template <typename C, typename Tr, typename T>
    std::basic_ostream<C, Tr>&
        operator<<(std::basic_ostream<C, Tr>& os, const variable_t<T>& v)
    

    The implementation doesn't change. Similar for operator>>.

\$\endgroup\$
10
  • 1
    \$\begingroup\$ I appreciate all of your feed back. It gives a lot of insight into where improvements can be made. The above is just a draft as I already knew that it wouldn't be for practical use. I'll have to take some time to incorporate what you have suggested. Once I have that complete, I'll make a new posting with a reference link to this Q/A with my updates. \$\endgroup\$ Jun 24, 2019 at 16:09
  • 1
    \$\begingroup\$ ... 5. Division by 0 - I have my reasons for division of 0 resulting in 0, not now as it isn't currently obvious but later when I start to use this class in other classes, division by 0 will result in different values depending on different situations. I plan on having division by 0 possibly returning 0,1, +/- infinity when I start to introduce limits and the ability to factor polynomials. I may reconsider this also... 6. - Bitwise manipulation - I can agree with your assessment to restricting it only to types that can naturally perform them. .... \$\endgroup\$ Jun 24, 2019 at 16:22
  • 1
    \$\begingroup\$ 11 - operator* instead of operator() I didn't want to use operator* since intuitively for me it appears to work on pointers. In my on rationale the class is a wrapper around some value T. The idea here was that this class's operator() instead of returning an object of itself, it would return the its value. and vice versa, passing a value to it would modify it. I didn't want to have a setValue and getValue functions. So I thought using the operator() and operator(T) to retrieve or modify the object made sense. 12 operator[] - I can remove this as it is not necessary. ... \$\endgroup\$ Jun 24, 2019 at 16:37
  • 1
    \$\begingroup\$ ... 19. I fixed this to remove the constructor call and used the operator= instead. 20. That header is new to me, but I can look into it. and finally 21. supporting all basic_streams is a highly viable option that I will consider. \$\endgroup\$ Jun 24, 2019 at 17:06
  • 1
    \$\begingroup\$ @FrancisCugler Wow that’s a large number of comments! Thank you for reading each bullet carefully and replying. I appreciate! To save time, in future you can omit the points you agree on and just discuss the bullets you need explanation. \$\endgroup\$
    – L. F.
    Jun 25, 2019 at 3:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.