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