How to reduce redundant code when adding new c++0x rvalue reference operator overloads

南楼画角 提交于 2019-12-28 10:05:04

问题


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.


回答1:


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.




回答2:


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;
}



回答3:


You're supposed to define them as member functions, so that you don't have to overload on lvalue or rvalue as the primary unit (which is unnecessary anyway) That is,

class Tree {
    Tree operator+ const (const Tree&);
    Tree operator+ const (Tree&&);
};

because the l or r valueness of the first is irrelevant. In addition, the compiler will automatically construct for you if that constructor is available. If tree constructs from double, then you can automatically use doubles here, and the double will be appropriately an rvalue. This is just two methods.




回答4:


I think the problem is that you have defined the operation with non const parameters. If you define

tree operator +(const tree& a, const tree& b);

There is no difference between r-value and l-value reference, so you don't need to define also

tree operator +(tree&&      a, const tree& b);

If in addition double is convertible to tree as tree x = 1.23; lets think, you don't need neither define

tree operator +(double      a, const tree& b){ return tree(a) + b; }

the compiler will do the work for you.

You will need to make the difference between rvalues and lvalues if the operator+ takes the tree parameter by value

tree operator +(tree a, tree b);


来源:https://stackoverflow.com/questions/2696156/how-to-reduce-redundant-code-when-adding-new-c0x-rvalue-reference-operator-ove

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!