Как уменьшить избыточный код при добавлении новых перегруженных ссылочных операторов 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);
Другие вопросы по тегам