c++ - programacion - operaciones aritmeticas suma resta multiplicacion y division
Cómo reducir el código redundante al agregar nuevas sobrecargas de operadores de referencia de valores en r++ 0x (4)
Estoy agregando nuevas sobrecargas de operadores para aprovechar las referencias de valores de c ++ 0x r, y siento que estoy produciendo una gran cantidad de código redundante.
Tengo una clase, tree
, que contiene un árbol de operaciones algebraicas en valores dobles. Aquí hay un ejemplo de caso de uso:
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"
Para cada operación binaria (como más), cada lado puede ser un tree
valores, un tree
valores o un double
. Esto resulta en 8 sobrecargas para cada operación binaria:
// 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
que también debe repetirse de una manera para cada operación binaria (menos, multiplicar, dividir, etc.).
Como puedes ver, en realidad solo hay 4 funciones que necesito escribir; Los otros 4 pueden lanzar y reenviar a los casos principales.
¿Tiene alguna sugerencia para reducir el tamaño de este código?
PD: La clase es en realidad más compleja que un simple árbol de dobles. La reducción de copias mejora dramáticamente el rendimiento de mi proyecto. Por lo tanto, las sobrecargas de valores valen la pena para mí, incluso con el código adicional. Tengo la sospecha de que podría haber una manera de eliminar los casos de "emitir y enviar", pero parece que no puedo pensar en nada.
Creo que el problema es que ha definido la operación con parámetros no constantes. Si usted define
tree operator +(const tree& a, const tree& b);
No hay diferencia entre el valor r y la referencia de valor l, por lo que no es necesario definir también
tree operator +(tree&& a, const tree& b);
Si además el doble es convertible a árbol como tree x = 1.23;
Pensemos, no necesitas ni definir.
tree operator +(double a, const tree& b){ return tree(a) + b; }
El compilador hará el trabajo por ti.
Deberá marcar la diferencia entre rvalues y lvalues si el operador + toma el parámetro del árbol por valor
tree operator +(tree a, tree b);
En primer lugar, no veo por qué el operador + modificaría los argumentos (no es esta una implementación inmutable típica de árbol binario), por lo que no habría diferencia entre el valor r y la referencia de valor l. Pero supongamos que los subárboles tienen un puntero hacia el padre o algo así.
Del ejemplo de uso que mostró, parece que hay una conversión implícita de doble a árbol. En ese caso, sus casos de "conversión y envío" no son necesarios, el compilador encontrará la conversión definida por el usuario.
¿Las sobrecargas que no se mueven no crean una nueva instancia para ingresar al nuevo árbol? Si es así, creo que puedes escribir tres de tus cuatro casos restantes como reenviadores.
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); }
Por supuesto, puede usar una macro para ayudar a generar las tres (o siete) versiones de reenvío de cada operador.
EDITAR: si esas llamadas son ambiguas o resuelven la recursión, ¿qué tal si?
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 del operador falla al usar conversiones implícitas:
#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;
}
Y versión sin plantillas donde funciona la conversión implícita:
#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;
}
Se supone que debes definirlas como funciones miembro, de modo que no tengas que sobrecargarte en lvalue o rvalue como la unidad primaria (que de todos modos no es necesario) Eso es,
class Tree {
Tree operator+ const (const Tree&);
Tree operator+ const (Tree&&);
};
Porque el valor de I o R del primero es irrelevante. Además, el compilador construirá automáticamente si ese constructor está disponible. Si el árbol se construye a partir del doble, entonces usted puede usar automáticamente los dobles aquí, y el doble será apropiadamente un valor. Esto es sólo dos métodos.
Solo una rápida respuesta tardía: si la clase en cuestión es movible, el movimiento es muy barato, y siempre se movería de todos los argumentos si es posible, luego pasar los argumentos por valor podría ser una opción:
tree operator +(tree a, tree b);
Si el árbol es móvil y se pasa un valor de referencia como el argumento real, entonces los argumentos de la función se inicializarán con el constructor de movimiento del árbol cuando sea posible, de lo contrario, el constructor de copia. Luego, la función puede hacer lo que quiera con sus argumentos de la manera adecuada (como, por ejemplo, mover sus partes internas).
Incurre en un movimiento adicional cuando se pasa un argumento de referencia de valor en comparación con la versión de lotes de sobrecargas, pero creo que generalmente es mejor.
Además, los argumentos de IMO, tree &&
deberían aceptar valores de valores a través de una copia temporal, pero esto no es lo que hacen actualmente los compiladores, por lo que no es muy útil.