2010-04-23 4 views
19

Ich bin neue Operator Überladungen hinzufügen, um Vorteile von C++ 0x rvalue Referenzen zu nutzen, und ich fühle mich wie ich eine Menge von redundantem Code produzieren.Wie reduziere ich redundanten Code beim Hinzufügen neuer C++ 0x Rvalue Referenz Operator Überladungen

Ich habe eine Klasse, tree, die einen Baum der algebraischen Operationen auf doppelte Werte enthält. Hier ist ein Beispiel Anwendungsfall:

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" 

Für jede binäre Operation (wie plus), kann jede Seite entweder ein L-Wert tree, rvalue tree oder double. Dies führt zu 8 Überlastungen für jede binäre Operation:

// 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 

, die auch in einer Art und Weise für jede binäre Operation (minus, multiplizieren, dividieren usw.) wiederholt werden.

Wie Sie sehen können, gibt es wirklich nur 4 Funktionen, die ich tatsächlich schreiben muss; die anderen 4 können in die Kernfälle werfen und weiterleiten.

Haben Sie Vorschläge zur Reduzierung der Größe dieses Codes?

PS: Die Klasse ist eigentlich komplexer als nur ein Baum der Doppelgänger. Das Reduzieren von Kopien verbessert die Leistung meines Projekts erheblich. Also, die Rvalue-Überladungen sind für mich lohnenswert, auch mit dem zusätzlichen Code. Ich habe den Verdacht, dass es einen Weg zur Vorlage die "cast and forward" -Fälle oben geben könnte, aber ich kann nicht scheinen, an etwas zu denken.

Antwort

7

Nur eine kurze späte Antwort: Wenn die Klasse in Frage beweglich ist, ist der Umzug sehr billig, und Sie würden immer von allen anderen Argumenten bewegen, wenn Sie können, dann die Argumente vorbei Wert könnte eine Option sein:

Wenn tree verfahrbar ist und ein rvalue ref als das eigentliche Argument übergeben wird, werden die Argumente für die Funktion wo möglich mit dem move -Konstruktor von tree initialisiert, sonst der Kopierkonstruktor. Dann kann die Funktion mit ihren Argumenten auf die richtige Art und Weise tun, was sie will (wie etwa ihre Interna verschieben).

Beim Übergeben eines Rvalue-Referenzarguments wird im Vergleich zur Version mit vielen Überladungen eine zusätzliche Verschiebung vorgenommen, aber ich denke, es ist im Allgemeinen besser.

Auch IMO, tree && Argumente sollten vielleicht Lvalues ​​über eine temporäre Kopie akzeptieren, aber das ist nicht, was Compiler derzeit tun, so dass es nicht sehr nützlich ist.

+2

+1, mit Rvalue Referenzen nach Wert übergeben ist wieder in der Art –

+0

Dies funktioniert perfekt. – Inverse

+0

Aber ich musste hinzufügen '+ (double a, tree b)' und '(tree a, double a)' Versionen. – Inverse

4

Erstens, ich sehe nicht, warum Operator + die Argumente überhaupt ändern würde (ist dies nicht eine typische unveränderliche Binärbaumimplementierung), so würde es keinen Unterschied zwischen R-Wert und L-Wert-Referenz geben. Aber nehmen wir an, dass die Teilbäume einen Zeiger auf den Eltern oder etwas ähnliches haben.

Aus dem gezeigten Beispiel ergibt sich eine implizite Konvertierung von Double in Tree. In diesem Fall werden Ihre Fälle "cast and forward" nicht benötigt, der Compiler wird die benutzerdefinierte Konvertierung finden.

Verursacht die nicht bewegliche Überladung keine neue Instanz, um in den neuen Baum zu gelangen? Wenn das so ist, denke ich, dass Sie drei Ihrer verbleibenden vier Fälle als Forwarder schreiben können.

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

Natürlich können Sie ein Makro verwenden, um die drei (oder sieben) Weiterleitungsversionen jedes Operators zu generieren.

EDIT: wenn diese Anrufe sind zweideutig oder Rekursion lösen, wie etwa:

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 des Betreibers Ausfall implizite Konvertierungen zu verwenden:

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

und Version ohne Vorlagen, wo die implizite Umwandlung funktioniert:

#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

die Nicht-Explizitmachung des Konstruktors für den doppelten Wert könnte eine praktikable Option sein ... erzeugen die Überladungen, die Sie vorschlagen, viele unnötige Kopien von l-Werten? – Inverse

+0

Es hängt davon ab, wie Sie die nicht beweglichen Versionen implementieren wollten. Ich nehme an, Sie würden eine Kopie jedes Nicht-Bewegungs- Arguments machen müssen, und die Forwarder, die ich vorschlage, tun genau das. Nun, natürlich ist das Kopieren von Konstrukten (während des Arguments pass-by-value), gefolgt von move, wahrscheinlich nicht so effizient wie die Konstruktion von Kopien direkt an der gewünschten Stelle, aber es sollte auch nicht viel schlechter sein. Und sobald der Compiler anfängt, zu inlinern, ist es schwer zu wissen, ob es irgendeinen Unterschied gibt. –

+0

Leider hat diese elegante Lösung nicht mit VS 2010 RC funktioniert. Je nach Anwendungsfall beschwert sich der Compiler über unendliche Rekursion und mehrdeutige Überladungen. Aber ich habe gcc noch nicht probiert. – Inverse

1

Ich denke, das Problem ist, dass yo Sie haben die Operation mit nicht konstanten Parametern definiert.Wenn Sie

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

Es definieren keinen Unterschied zwischen r-Wert und l-Referenzwert, so dass Sie nicht auch doppelt

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

Wenn zusätzlich definieren müssen Baum als tree x = 1.23; umwandelbar ist Denken Sie, Sie brauchen weder definieren

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

der Compiler wird die Arbeit für Sie erledigen.

Sie müssen den Unterschied zwischen rvalues ​​und lvalues ​​machen, wenn der Bediener + den Baum Parameter als Wert nimmt

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

"... Es gibt keinen Unterschied zwischen R-Wert und L-Wert-Referenz, also müssen Sie auch nicht definieren ..." - Ich folge nicht, könnten Sie das näher erläutern? Mein Verständnis ist der Compiler wählt die nächste Überladung, einschließlich der (Rvalue, Lvalue) Fall. – Inverse

+0

@inverse: Ein r-Wert kann an eine const-Referenz gebunden werden. Und da Sie nur const-Referenzen verwenden, besteht kein realer Bedarf für die r-Wert-Referenzüberladungen. 'tree operator + (const tree & a, const tree & b);' ist in der Lage, die Arbeit von 'tree operator + (tree && a, const tree & b);' einfach in Ordnung, es sei denn, Sie tun etwas Unerwartetes. –

+3

"Ein r-Wert kann binden zu einer const-Referenz ganz gut. "Richtig, aber dann verlierst du die Optimierung, die internen Strukturen übernehmen zu können anstatt sie tief zu kopieren. –

3

Du solltest du sie als Member-Funktionen definieren, so dass Sie nicht zu Überlast auf L-Wert oder R-Wert als die primäre Einheit (die ohnehin nicht erforderlich ist) das heißt,

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

weil die L oder R valueness der ersten irrelevant ist. Darüber hinaus wird der Compiler automatisch für Sie erstellen, wenn dieser Konstruktor verfügbar ist. Wenn der Baum aus dem Double-Konstrukt konstruiert wird, können Sie hier automatisch Double-Werte verwenden, und der Double-Wert wird entsprechend als rvalue angezeigt. Dies sind nur zwei Methoden.

+0

Das funktioniert eigentlich ok, außer für den Fall mit einem Double auf der linken Seite , zB '1.3 + tree (2.4)', oder fehlt mir etwas ... – Inverse

+0

Es ist üblich, Operatoren als Friend-Funktionen zu deklarieren, so dass Sie beide Operanden frei überladen können, einschließlich r-Wert vs l-Wert-Referenz. –

+0

Schreiben von Double + Tree ist einfach Freund Operator + (doppelte Zahl, Tree && ref) {return std :: forwa rd (ref) + num; } Das ist gut für beide L und R-Werte dank std :: forward. Wenn Tree jedoch aus Double erstellt, ist dies möglicherweise überhaupt nicht erforderlich. Sie können als Freund Funktionen erklären .. aber ich mag es nicht und nur verwenden, wenn es unmöglich ist, eine Member-Funktion zu verwenden. – Puppy

Verwandte Themen