Как уменьшить избыточный код при добавлении новых перегрузок ссылок С++ 0x rvalue
Я добавляю новые перегрузки операторов, чтобы использовать ссылки С++ 0x rvalue, и мне кажется, что я создаю много избыточного кода.
У меня есть класс tree
, который содержит дерево алгебраических операций над двойными значениями. Вот пример использования:
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"
Для каждой бинарной операции (например, плюс) каждая сторона может быть либо lvalue tree
, rvalue tree
, либо double
. Это приводит к 8 перегрузкам для каждой бинарной операции:
// 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
который также должен повторяться для каждой бинарной операции (минус, умножить, делить и т.д.).
Как вы можете видеть, действительно есть только 4 функции, которые мне действительно нужно писать; другие 4 могут отбрасывать и пересылать основные случаи.
Есть ли у вас предложения по уменьшению размера этого кода?
PS: Класс на самом деле более сложный, чем просто дерево двойников. Сокращение копий значительно повышает производительность моего проекта. Таким образом, перегрузки rvalue мне подходят, даже с дополнительным кодом. У меня есть подозрение, что может быть способ отформатировать вышеприведенные случаи "бросить" и "вперед", но я ничего не могу придумать.
Ответы
Ответ 1
Просто быстрый поздний ответ: если рассматриваемый класс является подвижным, это очень дешево, и вы всегда будете двигаться от всех аргументов, если можете, тогда передача аргументов по значению может быть опцией:
tree operator +(tree a, tree b);
Если дерево является подвижным и в качестве фактического аргумента передается rvalue ref, тогда аргументы функции будут инициализироваться конструктором дерева move, где это возможно, иначе конструктор копирования. Затем функция может делать все, что захочет, с помощью своих аргументов соответствующим образом (например, перемещая внутренние элементы).
Это приводит к дополнительному перемещению при передаче ссылочного аргумента rvalue по сравнению с версией с множеством перегрузок, но я думаю, что это вообще лучше.
Кроме того, аргументы IMO, tree &&
должны, возможно, принимать lvalues через временную копию, но это не то, что сейчас делают компиляторы, поэтому это не очень полезно.
Ответ 2
Во-первых, я не понимаю, почему оператор + вообще изменил аргументы (не является ли это типичной реалистичной реализацией бинарного дерева), поэтому не было бы никакой разницы между значениями r-value и l-value. Но предположим, что поддеревья имеют указатель до родителя или что-то в этом роде.
Из примера использования, который вы показали, выглядит как неявное преобразование из двойного в дерево. В этом случае ваши случаи "броска" и "вперед" не нужны, компилятор найдет пользовательское преобразование.
Разве не перегруженные перегрузки в конечном итоге делают новый экземпляр для входа в новое дерево? Если это так, я думаю, вы можете написать три из оставшихся четырех дел в качестве пересылок.
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); }
Конечно, вы можете использовать макрос, чтобы помочь сгенерировать три (или семь) версий пересылки каждого оператора.
EDIT: если эти вызовы неоднозначны или разрешены для рекурсии, как насчет:
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: воспроизведение отказа оператора при использовании неявных преобразований:
#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;
}
И версия без шаблонов, где работает неявное преобразование:
#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
Вы должны определить их как функции-члены, так что вам не нужно перегружать lvalue или rvalue в качестве основного элемента (что в любом случае необязательно). То есть
class Tree {
Tree operator+ const (const Tree&);
Tree operator+ const (Tree&&);
};
потому что l или r важности первого не имеет значения.
Кроме того, компилятор автоматически построит для вас, если этот конструктор доступен. Если дерево построено из double, тогда вы можете автоматически использовать двойники здесь, а double будет соответственно rvalue. Это всего лишь два метода.
Ответ 4
Я думаю, проблема в том, что вы определили операцию с неконстантными параметрами. Если вы определяете
tree operator +(const tree& a, const tree& b);
Нет никакой разницы между значениями r-value и l-value, поэтому вам не нужно также определять
tree operator +(tree&& a, const tree& b);
Если, кроме того, double преобразуется в дерево как tree x = 1.23;
, подумайте, вам не нужно ни определять
tree operator +(double a, const tree& b){ return tree(a) + b; }
компилятор выполнит вашу работу.
Вам нужно будет сделать разницу между значениями rvalues и lvalues, если оператор + принимает параметр дерева по значению
tree operator +(tree a, tree b);