I am adding new operator overloads to take advantage of c++0x rvalue references, and I feel like I'm producing a lot of redundant code.
I have a class, tree
, that holds a tree of algebraic operations on double values. Here is an example use case:
tree x = 1.23;
tree y = 8.19;
tree z = (x + y)/67.31 - 3.15*y;
...
std::cout << z; // prints "(1.23 + 8.19)/67.31 - 3.15*8.19"
For each binary operation (like plus), each side can be either an lvalue tree
, rvalue tree
, or double
. This results in 8 overloads for each binary operation:
// core rvalue overloads for plus:
tree operator +(const tree& a, const tree& b);
tree operator +(const tree& a, tree&& b);
tree operator +(tree&& a, const tree& b);
tree operator +(tree&& a, tree&& b);
// cast and forward cases:
tree operator +(const tree& a, double b) { return a + tree(b); }
tree operator +(double a, const tree& b) { return tree(a) + b; }
tree operator +(tree&& a, double b) { return std::move(a) + tree(b); }
tree operator +(double a, tree&& b) { return tree(a) + std::move(b); }
// 8 more overloads for minus
// 8 more overloads for multiply
// 8 more overloads for divide
// etc
which also has to be repeated in a way for each binary operation (minus, multiply, divide, etc).
As you can see, there are really only 4 functions I actually need to write; the other 4 can cast and forward to the core cases.
Do you have any suggestions for reducing the size of this code?
PS: The class is actually more complex than just a tree of doubles. Reducing copies does dramatically improve performance of my project. So, the rvalue overloads are worthwhile for me, even with the extra code. I have a suspicion that there might be a way to template away the "cast and forward" cases above, but I can't seem to think of anything.
Just a quick late answer: If the class in question is moveable, the move is very cheap, and you would always move from all the arguments if you can, then passing the arguments by value might be an option:
tree operator +(tree a, tree b);
If tree is moveable and an rvalue ref is passed as the actual argument, then the arguments to the function will be initialized with tree's move constructor where possible, else the copy constructor. Then, the function can do whatever it wants with its arguments in the appropriate way (like, say, moving their internals around).
It does incur an extra move when passing an rvalue reference argument compared with the lots-of-overloads version, but I think it's generally better.
Also, IMO, tree &&
arguments should maybe accept lvalues via a temporary copy, but this is not what any compilers currently do, so it's not very useful.
First, I don't see why operator+ would modify the arguments at all (isn't this a typical immutable binary tree implementation), so there'd be no difference between r-value and l-value reference. But let's assume that the subtrees have a pointer up to the parent or something like that.
From the usage example you showed, it looks like there's an implicit conversion from double to tree. In that case, your "cast and forward" cases aren't needed, the compiler will find the user-defined conversion.
Don't the non-move overloads end up making a new instance to go into the new tree? If so, I think you can write three of your remaining four cases as forwarders.
tree operator +(tree&& a, tree&& b); // core case
tree operator +(tree a, tree b) { return std::move(a) + std::move(b); }
tree operator +(tree a, tree&& b) { return std::move(a) + std::move(b); }
tree operator +(tree&& a, tree b) { return std::move(a) + std::move(b); }
Of course, you can use a macro to help generate the three (or seven) forwarding versions of each operator.
EDIT: if those calls are ambiguous or resolve to recursion, how about:
tree add_core(tree&& a, tree&& b);
tree operator +(tree&& a, tree&& b) { return add_core(std::move(a), std::move(b)); }
tree operator +(tree a, tree b) { return add_core(std::move(a), std::move(b)); }
tree operator +(tree a, tree&& b) { return add_core(std::move(a), std::move(b)); }
tree operator +(tree&& a, tree b) { return add_core(std::move(a), std::move(b)); }
EDIT: repro of the operator failure to use implicit conversions:
#include <iostream>
template<typename T>
class tree;
template<typename T> tree<T> add(tree<T> a, tree<T> b)
{
std::cout << "added!" << std::endl << std::endl;
return tree<T>();
}
template<typename T> tree<T> operator +(tree<T> a, tree<T> b) { return add(a, b); }
template<typename T>
class tree
{
public:
tree() { }
tree(const tree& t) { std::cout << "copy!" << std::endl; }
tree(double val) { std::cout << "double" << std::endl; }
friend tree operator +<T>(tree a, tree b);
};
int main()
{
tree<double>(1.0) + 2.0;
return 0;
}
And version without templates where the implicit conversion works:
#include <iostream>
class tree
{
public:
tree() { }
tree(const tree& t) { std::cout << "copy!" << std::endl; }
tree(double val) { std::cout << "double" << std::endl; }
friend tree operator +(tree a, tree b);
};
tree add(tree a, tree b)
{
std::cout << "added!" << std::endl << std::endl;
return tree();
}
tree operator +(tree a, tree b) { return add(a, b); }
int main()
{
tree(1.0) + 2.0;
return 0;
}
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