2

This is the basic implementation I'm using for expression templates, based on the CRTP which allows me to conveniently combine multiple types of operations without also asking everything to be an expression tree.

template<typename E>
struct OpExpression
{
    auto eval(iter_type n) const
    {
        return static_cast<E const&>(*this).eval(n);
    }
};

template<typename T>
struct OpFrame : OpExpression<OpFrame<T>>
{
    T *ptr;
    OpFrame(T *ptr) : ptr{ ptr } {}

    T eval(iter_type n) const
    {
        return ptr[n];
    }
};


template<typename T>
struct OpLiteral : OpExpression<OpLiteral<T>>
{
    T value;
    OpLiteral<T>(T value) : value{ value } {}

    T eval(iter_type) const
    {
        return value;
    }
    operator T() const
    {
        return value;
    }
};

Here is a class to apply the addition between two values (and an operator overload to make it pretty):

template<typename E1, typename E2>
struct OpFrameAdd : OpExpression<OpFrameAdd<E1, E2>>
{
    OpFrameAdd(E1 const a, E2 const b) : a{ a }, b{ b } {}
    auto eval(iter_type n) const
    {
        return a.eval(n) + b.eval(n);
    }

protected:
    E1 const a;
    E2 const b;
};

template<typename E1, typename E2>
auto operator+(OpExpression<E1> const& a, OpExpression<E2> const& b)
{
    auto v = OpFrameAdd<E1, E2>(*static_cast<const E1*>(&a), *static_cast<const E2*>(&b));
    return v;
}

To provide some more detail/context; I have an array holding a bunch of values (could be of various types), and an arithmetic expression defining how I want to transform that array into another one. However, this transformation is not concrete, so I'm using the expression tree as something I can pass to objects that will then handle it in different ways (i.e. I have to wait to evaluate it). Additionally, I only want to define the expression once.

I'm wondering, if based on this design, I can introduce literals (without casting to an OpLiteral) to my expressions? For example:

double arr[100]{ 0 };
OpFrame arr_t(arr);

// I can do this
auto ev1 = arr_t + arr_t + arr_t + OpLiteral(2.0);

// But I would prefer to do this
auto ev2 = arr_t + arr_t + arr_t + 2.0;

Based on my question here, I know 2.0 won't automatically cast to the correct type, but the solution is also not compatible with this design (it causes either an ambiguous call, or in bigger expressions, mixes up the tree by applying the generic template rather than one based on OpExpression<T>).

How I had tried to implement that solution:

template<typename E1, typename T>
auto operator+(OpExpression<E1> const& a, T b)
{
    auto v = OpFrameAdd<E1, OpLiteral<T>>(*static_cast<const E1*>(&a), OpLiteral<T>(b));
    return v;
}
template<typename E2, typename T>
auto operator+(T a, OpExpression<E2> const& b)
{
    auto v = OpFrameAdd<OpLiteral<T>, E2>(OpLiteral<T>(a), *static_cast<const E2*>(&b));
    return v;
}

So my questions are:

Is it possible to augment the design to use the literal in the preferred way? If not, is this is just a limitation of the templates/design I chose? (Casting to OpLiteral is still a lot easier than making a new operator overload for each type and for both sides). More broadly, is there a known (different) design to deal with this problem? Have I applied this design correctly to the problem?

EDIT

With the given design, it doesn't appear possible to accept and convert another type implicitly. Ultimately, it seems the problem can be phrased as: I want to have an operation between 1) A parent class and another parent class; 2) A parent class an any other object; 3) Any class and that parent class. Obviously this is inherently problematic.

For instance, when I attempted to remedy the ambiguous call in the above attempt, it instead results in operations between children of OpExpression becoming a template argument of OpLiteral. This is because instead of resolving the 'correct' operator (i.e. applying operator with both argument types of OpExpression), it will choose the one with the more generic argument type.

The conclusion is that as it is, this is simply a limitation of the design (and for good reason).

However, suppose I have a complete list of all the literal types I would want to use in an expression. If I don't want to create individual template specializations for each one, how would I modify the overloaded operator to use that? Would I instead have to modify the classes of OpExpression?

Riddick
  • 319
  • 3
  • 15
  • What is a literal to you? If it is one of the built-in types (finite typelist), then yes, you can do this via `std::enable_if_t<>` and possibly `boost::mpl::vector<>` to store the typelist. If not, then you want a partial function template specialization, which is a no-go; but you can usually delegate from a generic fn template to a static fn of a class template and the latter can be partially specialized. The conceptual issue here is, however, that in some expressions you _might_want_ `OpExpression<>` to be a literal, e.g. when you work on the meta-tree. – lorro May 22 '19 at 23:33
  • A literal can potentially be of type `std::complex<>`. Would delegating to a partially specialized class template work to generalize the overloaded operator functions? Ideally, I would want the expressions in a readable format. I also do have a list of the literal types. Re. the conceptual issue; perhaps I'm misunderstanding you - the only way I use any of the `OpExpression` types is by calling the `eval` function, so would it matter if its literal or not? Ultimately, it seems I have to cast to a `OpLiteral` here. – Riddick May 22 '19 at 23:52
  • The specialization with the most bound parameters wind the race. Thus, if you added a typename Dummy=void parameter, you could have the generic for void and then have enabe_if for the other ones I think. – lorro May 23 '19 at 00:04
  • As for the other part: do you say you never want to have metafunctions (functions implemented as OpExpressions that operate over OpExpressions)? If that’s banned, this part is not an issue. – lorro May 23 '19 at 00:07
  • Also, SFINAE. You might typedef something in your OpExpression that you can detect (e.g. use as return type) in your overloads. – lorro May 23 '19 at 00:15
  • Actually yes, I will probably employ metafunctions (though I'm not sure in what capacity yet). Currently, I change how the expression is treated by changing `eval` (I know the algorithm at compile time), though it might be better to apply metafunctions. Regarding your last point, that was one of my initial thoughts also, since everything inherits from `OpExpression`, I can add a typedef to check on. – Riddick May 23 '19 at 10:04

0 Answers0