I am trying to implement a minimal symbolic algebra library (AKA C.A.S.) in C++ that allows me to utilize delayed evaluation similar to this question from SO.
My class hierarchy is getting a little complex, as it uses CRTP, abstract base classes, and smart pointers (which is where I'm running into issues).
I'm not confident using smart pointers yet, so I'm hoping that I can get feedback on how I'm using them and whether or not there are better ways to handle this. I'm also appreciative of any feedback on how to make this work without running into any unforeseen or classic pitfalls.
I've included background code to demonstrate a simplified version of what I have so far, and my actual question is down below. I appreciate any feedback I can get.
Background:
I have an abstract base class which looks like this:
class BaseSymbolic
: public std::enable_shared_from_this<BaseSymbolic> {
public:
virtual ~BaseSymbolic() {}
inline std::shared_ptr<BaseSymbolic> as_ptr();
virtual std::string as_str() const = 0; // For printing a symbolic.
virtual size_t hash() const = 0; // For comparing two symbolics.
};
inline std::shared_ptr<BaseSymbolic> BaseSymbolic::as_ptr() {
return shared_from_this();
}
With a CRTP base class that looks like this:
template<typename Derived>
class Symbolic
: public BaseSymbolic {
public:
inline Derived &operator~();
inline const Derived &operator~() const;
inline std::shared_ptr<Derived> as_ptr();
std::string as_str() const override;
inline operator std::string() const;
size_t hash() const override;
};
// Using the overloaded binary 'not' operator here is just a style choice. I'm
// sure this could easily be changed to 'derived' or 'const_derived', but I
// like the way it looks as an operator rather than a function call.
template<typename Derived>
inline Derived &Symbolic<Derived>::operator~() {
return static_cast<Derived &>(*this);
}
template<typename Derived>
inline const Derived &Symbolic<Derived>::operator~() const {
return static_cast<const Derived &>(*this);
}
// Uses static_pointer_cast to down cast the shared pointer to the derived type.
template<typename Derived>
inline std::shared_ptr<Derived> Symbolic<Derived>::as_ptr() {
return std::static_pointer_cast<Derived>(shared_from_this());
}
template<typename Derived>
std::string Symbolic<Derived>::as_str() const {
// I realize underscores before names can be bad style,
// I just don't know what to call these vvvvvvv yet.
return static_cast<const Derived &>(*this)._as_str();
}
template<typename Derived>
inline Symbolic<Derived>::operator std::string() const {
return static_cast<const Derived &>(*this)._as_str();
}
template<typename Derived>
size_t Symbolic<Derived>::hash() const {
return static_cast<const Derived &>(*this)._hash();
}
I chose not to use the Derived
/Base
CRTP pattern here for simplicity. Right now, nothing inherits from the derived classes.
I'm separating the virtual call from the CRTP call (as_str()
vs _as_str()
), just to minimize the amount of virtual function calls in my code. I'm pretty sure the vtable gets carried around regardless, but I think it reduces the size of the vtable.
I'm unsure about the as_ptr()
calls, which return a casted std::shared_ptr
. In my tests, I occasionally get away with calling the Symbolic
version, but mostly it defaults to the BaseSymbolic
version.
Now, I have a couple derived classes from Symbolic
, such as symbolic variables, numbers, expressions, etc. that I want to be able to use in a matrix, vector, etc. (which is why the abstract base class is needed). I chose to use CRTP to manage all of these so that I could use templates to handle the types.
An example declaration of a symbolic number:
template<typename T>
class Number
: public Symbolic<Number<T>> {
private:
T value_;
size_t hash_;
public:
explicit inline Number();
// Checks to make sure T is a number internally.
explicit inline Number(const T &m);
inline Number(const Number<T> &m);
inline Number<T> &operator=(const T &rhs);
inline Number<T> &operator=(const Number<T> &rhs);
inline std::string _as_str() const;
inline size_t _hash() const;
inline T value() const;
};
In order to implement delayed evaluation, I create a templated "expression" class for each operation (add, subtract, multiply, etc.) that stores a const reference to the Symbolic
arguments and is also derived from Symbolic
.
An example of a delayed evaluation operation:
template<typename T1, typename T2>
class ExprAdd
: public Symbolic<ExprAdd<T1, T2>> {
private:
const T1 &lhs_;
const T2 &rhs_;
public:
explicit inline ExprAdd(const T1 &lhs, const T2 &rhs);
inline std::string _as_str() const;
inline size_t _hash() const;
};
template<typename T1, typename T2>
inline ExprAdd<T1, T2>::ExprAdd(const T1 &lhs, const T2 &rhs)
: lhs_(lhs),
rhs_(rhs) {}
// The trailing return type is just a style choice. Sometimes as these get
// more complex, the return type can get pretty large, or I want to use
// decltype(...) to keep things simple.
// Also, the `const` modifier for the return type is just so that I can
// store the result in the wrapper class below.
template<typename T1, typename T2>
inline auto operator+(const Symbolic<T1> &lhs, const Symbolic<T2> &rhs)
-> const ExprAdd<T1, T2> {
return ExprAdd<T1, T2>(~lhs, ~rhs);
}
This leads the expression a + b
, where a
and b
are both variables, to return the type const ExprAdd<Variable, Variable>
My goal in the end is to check whether the two are both numbers and to "collapse" the expression if they are, meaning const ExprAdd<Number<...>, Number<...>>
gets replaced with a.value() + b.value()
.
But I also don't want to have the user keep track of the type, meaning I don't want them to use template arguments for creating variables.
I don't want this:
Number<int> a = 2;
Number<int> b = 3;
ExprAdd<Number<int>, Number<int>> c = a + b;
I do want this:
sym a = 2;
sym b = 3;
sym c = a + b;
Actual Question:
I created a derived class called sym
which holds a smart pointer to a BaseSymbolic
. I chose to use the 'rule of zero' for the class, because all it does is hold a smart pointer. Not sure if I explicitly need the copy and move functions for this.
Note that the sample below is simplified just for integers. There will be other constructors to handle other types in real life.
Also, the symbolic manipulations will be handled elsewhere, this is just to demonstrate one specific aspect of the code, namely the smart pointer class.
class sym
: public Symbolic<sym> {
private:
// Not sure if this needs to be a 'shared_ptr'. Might be a 'unique_ptr'.
std::shared_ptr<BaseSymbolic> ptr_;
public:
// Other ctors for other types. For example, there might be:
// explicit inline sym(std::string name) for a named variable.
inline sym(int m);
// These seem wrong. I want to be able to accept 'const' expressions,
// but don't mind copying. Right now, this accepts the rvalues
// from the addition operator.
template<typename Derived>
inline sym(const Symbolic<Derived> &&m);
template<typename Derived>
inline sym &operator=(const Symbolic<Derived> &&rhs);
inline std::string _as_str() const;
inline size_t _hash() const;
};
// Constructor
inline sym::sym(int m)
: ptr_(std::make_shared<Number<int>>(m)) {}
// This is most certainly wrong, but it works somehow.
template<typename Derived>
inline sym::sym(const Symbolic<Derived> &&m)
: ptr_(std::make_shared<Derived>(std::move(~m))) {}
// Assignment Operators
// This is also most certainly wrong.
template<typename Derived>
inline sym &sym::operator=(const Symbolic<Derived> &&m) {
ptr_ = std::make_shared<Derived>(std::move((~m)));
return *this;
}
// Member Functions
inline std::string sym::_as_str() const {
return ptr_->as_str();
}
inline size_t sym::_hash() const {
return ptr_->hash();
}
My question is whether or not I am implementing the smart pointer wrapper class correctly. I'm having trouble understanding how this might be used to chain expressions such as c = c + a
while maintaining the reference to c
. My first thought is that I'll need a temporary reference while I replace the smart pointer with a new value using reset
.
I would appreciate any feedback, especially about how to handle the smart pointer wrapper class. So far, my solution seems tenuous at best, and I would like to learn how to improve on it.
Here is a link to some working code.