2010-04-23 8 views
19

Aggiungo nuovi overload di operatore per sfruttare i riferimenti di runtime C++ 0x e mi sembra di produrre un sacco di codice ridondante.Come ridurre il codice ridondante quando si aggiungono nuovi sovraccarichi dell'operatore di riferimento di runtime C++ 0x

Ho una classe, tree, che contiene un albero di operazioni algebriche su valori doppi. Ecco un caso esempio 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" 

Per ogni operazione binaria (come plus), ogni lato può essere sia un lvalue tree, rvalue tree o double. Ciò si traduce in 8 sovraccarichi per ciascuna operazione 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 

che ha anche essere ripetuta in modo per ciascuna operazione binaria (meno, moltiplicazione, divisione, ecc).

Come potete vedere, ci sono davvero solo 4 funzioni che ho effettivamente bisogno di scrivere; gli altri 4 possono lanciare e inoltrare ai casi principali.

Avete suggerimenti per ridurre le dimensioni di questo codice?

PS: La classe è in realtà più complessa di un semplice albero di doppio. Ridurre le copie migliora notevolmente le prestazioni del mio progetto. Quindi, i sovraccarichi di valore valgono per me, anche con il codice extra. Ho il sospetto che potrebbe esserci un modo di sostituire il modello in "avanti e indietro", ma non riesco a pensare a nulla.

risposta

7

Basta una risposta tardiva rapida: Se la classe in questione è mobile, la mossa è molto a buon mercato, e si dovrebbe sempre passare da tutti gli argomenti, se è possibile, poi passando gli argomenti per valore potrebbe essere un'opzione:

Se l'albero è mobile e un valore di riferimento viene passato come argomento effettivo, gli argomenti della funzione verranno inizializzati con il costruttore di spostamento dell'albero, ove possibile, altrimenti il ​​costruttore di copie. Quindi, la funzione può fare ciò che vuole con i suoi argomenti nel modo appropriato (come, ad esempio, spostando i loro interni in giro).

È necessario uno spostamento extra quando si passa un argomento di riferimento rvalue rispetto alla versione lots-of-overload, ma penso che sia generalmente migliore.

Inoltre, gli argomenti IMO, tree && devono accettare gli lvalues ​​tramite una copia temporanea, ma questo non è ciò che fanno attualmente i compilatori, quindi non è molto utile.

+2

+1, con i riferimenti Rval passano di valore è tornato in stile –

+0

Ciò funziona perfettamente. – Inverse

+0

Ma ho dovuto aggiungere le versioni '+ (double a, tree b)' e '(tree a, double a)'. – Inverse

4

Innanzitutto, non vedo perché l'operatore + debba modificare gli argomenti (non si tratta di un'implementazione di albero binario immutabile tipica), quindi non ci sarebbe alcuna differenza tra il valore r e il riferimento al valore di l. Ma supponiamo che i sottoalberi abbiano un puntatore fino al genitore o qualcosa del genere.

Dall'esempio di utilizzo che hai mostrato, sembra che ci sia una conversione implicita da doppio ad albero. In tal caso, i casi "inoltrati e inoltrati" non sono necessari, il compilatore troverà la conversione definita dall'utente.

I sovraccarichi di non movimento non si trasformano in una nuova istanza per passare al nuovo albero? Se è così, penso che tu possa scrivere tre dei tuoi quattro casi rimanenti come spedizionieri.

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); } 

Naturalmente, è possibile utilizzare una macro per contribuire a generare i tre (o sette) versioni di inoltro di ciascun operatore.

EDIT: se quelle chiamate sono ambigue o decidono di ricorsione, come su:

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 fallimento all'operatore di utilizzare conversioni implicite:

#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; 
} 

e la versione senza modelli in cui la conversione implicita funziona:

#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; 
} 
+0

rendere il costruttore del valore doppio non esplicito potrebbe essere un'opzione praticabile ... gli overload che suggerisci producono molte copie non necessarie dei valori di l? – Inverse

+0

Dipende da come avevate pianificato di implementare le versioni non mobili per iniziare. Immagino che avresti avuto bisogno di fare una copia di ogni argomento di non movimento, e gli spedizionieri che ti suggerisco di fare proprio questo. Ora, ovviamente, copiare la costruzione (durante l'argomento pass-by-value) seguito da move probabilmente non è altrettanto efficiente della costruzione di copia direttamente nella posizione richiesta, ma non dovrebbe essere molto peggiore. E una volta che il compilatore inizia l'inline è difficile sapere se c'è qualche differenza. –

+0

Sfortunatamente, questa soluzione elegante non ha funzionato con VS 2010 RC. A seconda del caso d'uso, il compilatore lamenta una ricorsione infinita e sovraccarichi ambigui. Ma non ho ancora provato gcc. – Inverse

1

Penso che il problema sia che tu hai definito l'operazione con parametri non const.Se si definisce

tree operator +(const tree& a, const tree& b); 

Non v'è alcuna differenza tra r-value e riferimento l-value, quindi non è necessario definire anche

tree operator +(tree&&  a, const tree& b); 

Se oltre doppia è convertibile in albero come tree x = 1.23; Pensa, non hai bisogno di definire né

tree operator +(double  a, const tree& b){ return tree(a) + b; } 

il compilatore farà il lavoro per voi.

Sarà necessario fare la differenza tra rvalues ​​e lvalue se l'operatore + accetta il parametro albero per valore

tree operator +(tree a, tree b); 
+1

"... Non c'è alcuna differenza tra il valore r e il riferimento al valore l, quindi non è necessario definire anche ..." - Non seguo, potresti elaborare? La mia comprensione è che il compilatore sceglierà il sovraccarico più vicino, incluso il caso (valore, valore). – Inverse

+0

@inverse: un valore r può associare a un riferimento const solo bene. E dal momento che si utilizzano solo riferimenti const, non vi è alcun reale bisogno di sovraccarichi di riferimento del valore r. 'Albero operator + (albero const & a, albero const & b);' è in grado di fare il lavoro di 'operatore albero + (albero && un albero const & b);' bene, a meno che non si sta facendo qualcosa di inaspettato. –

+3

"un valore di R può legare "Vero, ma poi si perde l'ottimizzazione di essere in grado di assumere la proprietà delle strutture interne invece di copiarle in profondità. –

3

Si suppone di definirli come funzioni membro, in modo che non si deve sovraccarico lvalue o rvalue come unità principale (che è inutile comunque) Cioè,

class Tree { 
    Tree operator+ const (const Tree&); 
    Tree operator+ const (Tree&&); 
}; 

perché la lor valueness della prima è irrilevante. Inoltre, il compilatore costruirà automaticamente per te se tale costruttore è disponibile. Se i costrutti ad albero sono doppi, puoi usare automaticamente i doppi qui, e il doppio sarà appropriatamente un rvalore. Questo è solo due metodi.

+0

In realtà funziona bene, tranne che per il caso con un doppio sul lato sinistro , ad es. '1.3 + tree (2.4)', o mi manca qualcosa ... – Inverse

+0

È normale dichiarare gli operatori come funzioni amico, in questo modo è possibile sovraccaricare su entrambi gli operandi liberamente, incluso su valore r rispetto a riferimento a valore l. –

+0

scrittura double tree + è facile. amico operator + (doppia num, albero && ref) {return std :: forwa rd (rif) + num; } Questo va bene per entrambi i valori L e R, grazie a std :: forward. Tuttavia, se Tree costruisce da double, allora questo potrebbe non essere affatto necessario. Puoi dichiarare come funzioni amico .. ma non mi piace e lo uso solo quando è impossibile usare una funzione membro. – Puppy

Problemi correlati