6

Given an expression template tree, I want to create a new optimized tree before processing it. Consider the following example of a multiplication operation:

a * b * c * d,

which produces, due to the left-to-right associativity of operator*, the expression tree:

(((a * b) * c) * d).

I would like to produce a transformed expression tree where multiplication occurs from right-to-left:

(a * (b * (c * d))).

Consider the binary expression type:

template<typename Left, typename Right>
struct BinaryTimesExpr
{
    BinaryTimesExpr() = default;
    BinaryTimesExpr(const BinaryTimesExpr&) = default;
    BinaryTimesExpr(BinaryTimesExpr&&) = default;
    BinaryTimesExpr(Left&& l, Right&& r) : left(forward<Left>(l)), right(forward<Right>(r)) {}

    BinaryTimesExpr& operator=(const BinaryTimesExpr&) = default;
    BinaryTimesExpr& operator=(BinaryTimesExpr&&) = default;

    Left left;
    Right right;
};

Define the multiplication operator operator*:

template<typename Left, typename Right>
BinaryTimesExpr<Constify<Left>, Constify<Right>> operator*(Left&& l, Right&& r)
{
    return {forward<Left>(l), forward<Right>(r)};
}

where Constify is defined by:

template<typename T> struct HelperConstifyRef     { using type = T;  };
template<typename T> struct HelperConstifyRef<T&> { using type = const T&; };
template<typename T>
using ConstifyRef = typename HelperConstifyRef<T>::type;

and used to ensure sub-expressions with const lvalue-references when constructed from lvalues, and copies (by copy/move) of rvalues when constructed from rvalues.

Define the transformation function that creates a new expression template tree with the previous conditions:

template<typename Expr>
auto Transform(const Expr& expr) -> Expr
{
    return expr;
}

template<typename Left, typename Right>
auto Transform(const BinaryTimesExpr<Left, Right>& expr) -> type(???)
{
    return {(Transform(expr.left), Transform(expr.right))};
}

template<typename Left, typename Right>
auto Transform(const BinaryTimesExpr<BinaryTimesExpr<LeftLeft, LeftRight>, Right>& expr) -> type(???)
{
    return Transform({Transform(expr.left.left), {Transform(expr.left.right), Transform(expr.right)}}); // this sintax is invalid...how can I write this?
}

My questions are:

1) How do I determine the return types of the Transform functions? I've tried using type traits like:

template<typename Expr>
struct HelperTransformedExpr
{
    using type = Expr;
};

template<typename Left, typename Right>
struct HelperTransformedExpr<BinaryTimesExpr<Left, Right>>
{
    using type = BinaryTimesExpr<typename HelperTransformedExpr<Left>::type, typename HelperTransformedExpr<Right>::type>;
};

template<typename LeftLeft, typename LeftRight, typename Right>
struct HelperTransformedExpr<BinaryTimesExpr<BinaryTimesExpr<LeftLeft, LeftRight>, Right>>
{
    using type = BinaryTimesExpr<typename HelperTransformedExpr<LeftLeft>::type,
        BinaryTimesExpr<typename HelperTransformedExpr<LeftRight>::type, typename HelperTransformedExpr<Right>::type>>;
};

template<typename Expr>
using TransformedExpr = typename HelperTransformedExpr<Expr>::type;

but don't know how to apply this to solve my question (2) below.

2) How do I write the recursion line:

return Transform({Transform(expr.left.left), {Transform(expr.left.right), Transform(expr.right)}});

3) Is there a cleaner solution for this problem?


Edit: DyP presents a partial solution to the above problem. Below is my full solution based on his answer:

template<typename Expr>
auto Transform(const Expr& expr) -> Expr
{
    return expr;
}

template<typename Left, typename Right>
auto Transform(BinaryTimesExpr<Left, Right> const& expr)
-> decltype(BinaryTimesExpr<decltype(Transform(expr.left)), decltype(Transform(expr.right))>{Transform(expr.left), Transform(expr.right)})
{
    return BinaryTimesExpr<decltype(Transform(expr.left)), decltype(Transform(expr.right))>{Transform(expr.left), Transform(expr.right)};
}

template<typename LeftLeft, typename LeftRight, typename Right>
auto Transform(BinaryTimesExpr<BinaryTimesExpr<LeftLeft, LeftRight>, Right> const& expr)
-> decltype(Transform(BinaryTimesExpr<decltype(Transform(expr.left.left)), BinaryTimesExpr<decltype(Transform(expr.left.right)), decltype(Transform(expr.right))>>{Transform(expr.left.left), {Transform(expr.left.right), Transform(expr.right)}}))
{
    return Transform(BinaryTimesExpr<decltype(Transform(expr.left.left)), BinaryTimesExpr<decltype(Transform(expr.left.right)), decltype(Transform(expr.right))>>{Transform(expr.left.left), {Transform(expr.left.right), Transform(expr.right)}});
}

int main()
{
    BinaryTimesExpr<int, int> beg{1,2};
    auto res = beg*3*4*5*beg;
    std::cout << res << std::endl;
    std::cout << Transform(res) << std::endl;
}

Output:

(((((1*2)*3)*4)*5)*(1*2))
(1*(2*(3*(4*(5*(1*2))))))

Note that it was necessary to apply the Transform function on every sub-expression besides the most external Transform call (see the last Transform overload).

The full source code can be found here.

A.L.
  • 1,133
  • 1
  • 12
  • 19
  • Have you considered using Boost.Proto to help out with this? Cpp Next seems to be down at the moment, but he did an article about using Proto to re-order math expressions. His example was (A+B)[2] into A[2]+B[2], but since they're all just operators, it ought to apply. – KitsuneYMG Aug 13 '13 at 15:34
  • This is going to be integrated in a library that should not require third-party dependencies. – A.L. Aug 13 '13 at 15:38
  • 1) Boost.Proto is header-only. 2) Do you want to have metaprogramming transformer that transforms `(((a * b) * c) * d)` into `(a * (b * (c * d)))` or would it be sufficient to build the expression tree directly as `(a * (b * (c * d)))`? – dyp Aug 13 '13 at 15:51
  • You want an expression like `((((a * b) * c) * d) * e)` wich evaluates to `binary_exp,decltype(c)>,decltype(d)>,decltype(e)>` , have to be evaluated as `binary_exp>>>` right? Why you don't simply returns the expression in reverse order, in the operators overloads? – Manu343726 Aug 13 '13 at 15:52
  • that is: `template binary_exp operator*(const Left& lhs , const Right& rhs) { return { std::forward( rhs ) , std::forward( lhs ) }; }` – Manu343726 Aug 13 '13 at 15:55
  • @Manu343726 First iteration: `a*b` -> `exp`. Second iteration: `exp*c` -> `exp>`. This doesn't help if each `exp` shall perform one multiplication. – dyp Aug 13 '13 at 15:59
  • @Manu343726 Yes, this solves the problem, but as an exercise, I was wondering how those transformations could be applied to the expression tree (resulting in a new tree). – A.L. Aug 13 '13 at 15:59
  • @DyP I'm seeing that just now... sorry – Manu343726 Aug 13 '13 at 16:00
  • 1
    @Manu343726 I believe DyP is right. I need recursion to completely solve the problem. Moreover, the order matters when we have matrix multiplication. – A.L. Aug 13 '13 at 16:06
  • 2
    @Allan Order matters with floating point types as well. – Mark B Aug 13 '13 at 16:12
  • The problem with perfect forwarding I tried to solve is: When `operator *` is allowed to use perfect forwarding, it'll create types like `BinaryTimesExpr`, and, consequently, also `BinaryTimesExpr&, int>`. This breaks the partial specialization used; maybe it could be fixed using a more generic approach restricted by SFINAE. N.B. rvalues don't profit from (perfect) forwarding here due to lifetime issues. – dyp Aug 13 '13 at 17:41
  • @DyP This `BinaryTimesExpr&, RightRight>` will not happen if `BinaryTimesExpr` is a rvalue. See my new edit with the `Constify` type trait. With this, the expression `a * b * C()` becomes `BinaryTimesExpr, C>`, where the first two terms are lvalues and the third is a rvalue. In my application, the expression nodes should not be instantiated at runtime (they should exist only at compile time). – A.L. Aug 13 '13 at 18:06
  • The issue I've seen was not non-const refs on the underlying arithmetic types, but on the expression type itself: `BinaryTimesExpr&, int>`, or with your `Constify`, `BinaryTimesExpr const&, int>`. The problem would be that this doesn't match the third overload of `Transform` (which expects a `BTE`). However, this is solved by your second overload (in a way that a don't understand yet ;). – dyp Aug 13 '13 at 18:18

3 Answers3

3

Although the OP wanted a solution that didn't use Boost.Proto, others might be interested to see how this could be (pretty easily) done using it:

#include <iostream>
#include <boost/type_traits/is_same.hpp>
#include <boost/proto/proto.hpp>

namespace proto = boost::proto;
using proto::_;

struct empty {};

struct transform
  : proto::reverse_fold_tree<
        _
      , empty()
      , proto::if_<
            boost::is_same<proto::_state, empty>()
          , _
          , proto::_make_multiplies(_, proto::_state)
        >
    >
{};

int main()
{
    proto::literal<int> a(1), b(2), c(3), d(4);
    proto::display_expr( a * b * c * d );
    proto::display_expr( transform()(a * b * c * d) );
}

The above code displays:

multiplies(
    multiplies(
        multiplies(
            terminal(1)
          , terminal(2)
        )
      , terminal(3)
    )
  , terminal(4)
)
multiplies(
    terminal(1)
  , multiplies(
        terminal(2)
      , multiplies(
            terminal(3)
          , terminal(4)
        )
    )
)
Eric Niebler
  • 5,927
  • 2
  • 29
  • 43
0

Without incorporating perfect forwarding:

#include <iostream>

// simplified by making it an aggregate
template<typename Left, typename Right>
struct BinaryTimesExpr
{
    Left left;
    Right right;
};

// "debug" / demo output
template<typename Left, typename Right>
std::ostream& operator<<(std::ostream& o, BinaryTimesExpr<Left, Right> const& p)
{
    o << "(" << p.left << "*" << p.right << ")";
    return o;
}

// NOTE: removed reference as universal-ref yields a reference type for lvalues!
template<typename Left, typename Right>
BinaryTimesExpr < typename std::remove_reference<Left>::type,
                  typename std::remove_reference<Right>::type >
operator*(Left&& l, Right&& r)
{
    return {std::forward<Left>(l), std::forward<Right>(r)};
}


// overload to end recursion (no-op)
template<typename Expr>
auto Transform(const Expr& expr) -> Expr
{
    return expr;
}

template<typename LeftLeft, typename LeftRight, typename Right>
auto Transform(BinaryTimesExpr < BinaryTimesExpr<LeftLeft, LeftRight>,
                                 Right > const& expr)
-> decltype(Transform(
     BinaryTimesExpr < LeftLeft,
                       BinaryTimesExpr<LeftRight, Right>
                     > {expr.left.left, {expr.left.right, expr.right}}
   ))
{
    return Transform(
        BinaryTimesExpr < LeftLeft,
                          BinaryTimesExpr<LeftRight, Right>
                        > {expr.left.left, {expr.left.right, expr.right}}
    );
}


int main()
{
    BinaryTimesExpr<int, int> beg{1,2};
    auto res = beg*3*4*5*6;
    std::cout << res << std::endl;
    std::cout << Transform(res) << std::endl;
}

Output:

(((((1*2)*3)*4)*5)*6)

(1*(2*(3*(4*(5*6)))))

dyp
  • 38,334
  • 13
  • 112
  • 177
  • I am getting a strange compilation error when compiling this code: Internal compiler error: Error reporting routines re-entered. Anyway, I believe an extra call to `Transform` is necessary in your second overload. Try printing (2*3)*(4*5) to see if (2*(3*(4*5))) is produced. – A.L. Aug 13 '13 at 16:39
  • @Allan o.O I'm using clang++3.4 [live example](http://coliru.stacked-crooked.com/view?id=678c93dbde7aa7b203ed0cc1ac0ab533-7885f3d27d18134d8479d2ab5250c852) – dyp Aug 13 '13 at 16:43
  • @Allan Yes this example doesn't fold, it just "reverses" the tree. – dyp Aug 13 '13 at 16:44
  • The `Transform` call needs to be performed on `expr.left.left`, `expr.left.right` and `expr.right`. – A.L. Aug 13 '13 at 16:55
  • @Allan I'll try to figure out first how to incorporate perfect forwarding. Adding the `Transform` calls to the other two parts is simple AFAIK. – dyp Aug 13 '13 at 16:56
0

The expression is really a binary tree. For example, the expression a * b * c * d * e is evaluated as ((((a * b) * c) * d) * e) so what you have is the folowwing tree (Sorry about the three-years-old-child-like drawing, ipad without stylus...):

enter image description here

As you can see, the default evaluated expression is generated trasversing the tree with left-side-first inorder.

On the other hand, the reverse-evaluated expression (What OP wants) is generated trasversing the tree with right-side-first-inorder:

enter image description here

So one solution is to trasverse the generated expression tree in right-side-first-inorder, and create a new tree in the process.

Manu343726
  • 13,969
  • 4
  • 40
  • 75
  • What is the tree is more complex (involving more branches): (2*3)*(4*(5*6))? – A.L. Aug 13 '13 at 16:45
  • @Allan First note that what I mean with ((a*b)*c) is the form of the expression (How is evaluated) `a * b * c`, that is how the compiler evaluates the expression acording to operators precedence rules. Not what is the tree generated given a expression such as `((a*b)*c)`. – Manu343726 Aug 13 '13 at 16:52