I was looking at the example of expression templates at https://en.wikipedia.org/wiki/Expression_templates. Then I tried to make a simple symbolic expression tree, i.e. to add constants and variables like a + b + 10. So I started with
#include <iostream>
template<typename E>
class Expression {
public:
std::ostream& print(std::ostream& os) const
{
return expression().print(os);
}
E const& expression() const { return static_cast<E const&>(*this); }
};
class Var : public Expression<Var> {
public:
Var(const char name)
: name_(name)
{}
std::ostream& print(std::ostream& os) const
{
return os << name_;
}
private:
const char name_;
};
class Constant : public Expression<Constant> {
public:
Constant(const double value)
: value_(value)
{}
std::ostream& print(std::ostream& os) const
{
return os << value_;
}
private:
const double value_;
};
template<typename E1, typename E2>
class ExpressionSum : public Expression<ExpressionSum<E1,E2>> {
E1 const& u_;
E2 const& v_;
public:
ExpressionSum(E1 const& u, E2 const& v) : u_(u), v_(v)
{
}
std::ostream& print(std::ostream& os) const
{
os << "(";
u_.print(os);
os << " + ";
v_.print(os);
os << ")";
return os;
}
};
template <typename E1, typename E2>
ExpressionSum<E1,E2> operator+(Expression<E1> const& u, Expression<E2> const& v) {
return ExpressionSum<E1, E2>(u.expression(), v.expression());
}
int main() {
Var a('a');
Var b('b');
Constant c(1.0);
auto expr = a + b + c;
expr.print(std::cout);
std::cout << std::endl;
auto expr2 = expr + Constant{2.0};
expr2.print(std::cout);
std::cout << std::endl;
}
The expression expr
is fine, but I cannot reuse expr
to build another expression like expr2
since the temporary ExpressionSum
of a+b
is already destroyed. Is there a way to store these temporaries in the expression?
We want to keep things as simple as possible. The Curiously Recurring Template Pattern (and it's relatives) are powerful tools, but they increase compile-times and are cumbersome when you want to expand what you're doing.
By taking advantage of argument dependent lookup, we can implement operator overloading without having a base class. This greatly simplifies the design of the library. I'll explain more about this in the examples given below
std::ref
We want to keep this library simple. An expression is either a constant, a unary operation and an input, or a binary operation and an input. There aren't any class invariants - the inputs can take on any value, and the operation itself is stored based on it's type, so it can only have 1 value.
This means that we can represent expressions as aggregate types, making them trivially constructible, trivially copyable, trivially destructible, and reducing both compiletimes and the size of the resulting binary.
namespace expr // We need to put them in a namespace so we can use ADL
{
template<class Value>
class Constant
{
public:
Value value;
};
template<class Op, class Input>
class UnaryOp
{
public:
Op op;
Input input;
};
template<class Op, class Left, class Right>
class BinaryOp
{
public:
Op op;
Left lhs;
Right rhs;
};
}
If we write the operator overloads in a namespace, they'll only be considered when working with types from that namespace. This means we can avoid having a base class, and we can use unconstrained templates.
namespace expr
{
template<class A>
auto operator-(A const& a)
{
return UnaryOp<Negate, A>{{}, a};
}
template<class A, class B>
auto operator+(A const& a, B const& b)
{
return BinaryOp<Plus, A, B>{{}, a, b};
}
template<class A, class B>
auto operator-(A const& a, B const& b)
{
return BinaryOp<Minus, A, B>{{}, a, b};
}
template<class A, class B>
auto operator*(A const& a, B const& b) {
return BinaryOp<Times, A, B>{{}, a, b};
}
}
This is pretty simple to achieve - basically, any operation is a functor type that knows how to evaluate the inputs. In C++20, this can be achieved with lambdas, but for our purposes we'll just overload the operator()
.
namespace expr {
class Negate {
template<class A>
constexpr auto operator()(A&& a) const
noexcept(noexcept(-a))
-> decltype(-a)
{
return -a;
}
};
class Plus {
public:
template<class A, class B>
constexpr auto operator()(A&& a, B&& b) const
noexcept(noexcept(a + b))
-> decltype(a + b)
{
return a + b;
}
};
class Minus {
public:
template<class A, class B>
constexpr auto operator()(A&& a, B&& b) const
noexcept(noexcept(a - b))
-> decltype(a - b)
{
return a - b;
}
};
class Times {
public:
template<class A, class B>
constexpr auto operator()(A&& a, B&& b) const
noexcept(noexcept(a * b))
-> decltype(a * b)
{
return a * b;
}
};
}
evaluate
Rather than having it as a member function, we can take advantage of pattern matching and recursion when writing an evaluate
function at namespace scope.
namespace expr
{
// This one is applied to things that aren't constants or expressions
template<class Thing>
auto evaluate(Thing const& t) -> Thing const& {
return t;
}
template<class Value>
auto evaluate(Constant<Value> const& value) {
return evaluate(value.value);
}
template<class Op, class Input>
auto evaluate(UnaryOp<Op, Input> const& expr) {
return expr.op(evaluate(expr.value));
}
template<class Op, class LHS, class RHS>
auto evaluate(BinaryOp<Op, LHS, RHS> const& expr) {
return expr.op(evaluate(expr.lhs), evaluate(expr.rhs));
}
}
Instead of storing reference here:
template<typename E1, typename E2>
class ExpressionSum : public Expression<ExpressionSum<E1,E2>> {
E1 const& u_; // <------| These are references
E2 const& v_; // <------|
public:
ExpressionSum(E1 const& u, E2 const& v) : u_(u), v_(v)
{ }
// ...
};
These does not cause lifetime extension. The wikipedia article assume the expression template is never stored and only live in the same statement as the expression.
Store them as value:
template<typename E1, typename E2>
class ExpressionSum : public Expression<ExpressionSum<E1,E2>> {
E1 u_; // <------| Fixed!
E2 v_; // <------|
public:
ExpressionSum(E1 const& u, E2 const& v) : u_(u), v_(v)
{ }
// ...
};
You can also extend std::tuple
to piggyback on it's EBO:
template<typename E1, typename E2>
class ExpressionSum : public Expression<ExpressionSum<E1,E2>>, private std::tuple<E1, E2> {
auto u_() const -> E1 const& { return std::get<0>(*this); }
auto v_() const -> E2 const& { return std::get<1>(*this); }
public:
ExpressionSum(E1 const& u, E2 const& v) : std::tuple<E1, E2>(u, v)
{ }
// ...
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With