Подтвердить что ты не робот

Как уменьшить избыточный код при добавлении новых перегрузок ссылок С++ 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 мне подходят, даже с дополнительным кодом. У меня есть подозрение, что может быть способ отформатировать вышеприведенные случаи "бросить" и "вперед", но я ничего не могу придумать.

4b9b3361

Ответ 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);