Wie kann man beim Verketten die Kopie entfernen?

10

Ich erstelle eine Klasse von Verkettungstypen, wie das kleine Beispiel unten. Es scheint, dass beim Verketten von Elementfunktionen der Kopierkonstruktor aufgerufen wird. Gibt es eine Möglichkeit, den Aufruf des Kopierkonstruktors loszuwerden? In meinem Spielzeugbeispiel unten ist es offensichtlich, dass ich mich nur mit Provisorien beschäftige und daher "sollte" (vielleicht nicht nach den Maßstäben, aber logisch) eine Elision sein. Die zweitbeste Wahl, um elision zu kopieren, wäre, den Verschiebungskonstruktor aufzurufen, aber dies ist nicht der Fall.

class test_class {
    private:
    int i = 5;
    public:
    test_class(int i) : i(i) {}
    test_class(const test_class& t) {
        i = t.i;
        std::cout << "Copy constructor"<< std::endl;
    }
    test_class(test_class&& t) {
        i = t.i;
        std::cout << "Move constructor"<< std::endl;
    }
    auto& increment(){
        i++;
        return *this;
    }
};
int main()
{
    //test_class a{7};
    //does not call copy constructor
    auto b = test_class{7};
    //calls copy constructor
    auto b2 = test_class{7}.increment();
    return 0;
}

Bearbeiten: Einige Klarstellungen. 1. Dies hängt nicht von der Optimierungsstufe ab. 2. In meinem realen Code habe ich komplexere (z. B. Heap zugewiesene) Objekte als Ints

DDaniel
quelle
Welche Optimierungsstufe verwenden Sie zum Kompilieren?
JVApen
2
auto b = test_class{7};ruft den Kopierkonstruktor nicht auf, weil er wirklich gleichwertig ist test_class b{7};und Compiler klug genug sind, um diesen Fall zu erkennen, und daher das Kopieren leicht vermeiden können. Das gleiche kann man nicht machen b2.
Einige Programmierer Typ
In dem gezeigten Beispiel gibt es möglicherweise keinen oder nur einen großen Unterschied zwischen Verschieben und Kopieren, und dies ist nicht jedem bewusst. Wenn Sie so etwas wie einen großen Vektor hineingesteckt haben, könnte dies eine andere Sache sein. Normalerweise ist das Verschieben nur für Ressourcen sinnvoll, die Typen verwenden (z. B. viel Heap-Mem usw.). Ist dies hier der Fall?
Darune
Das Beispiel sieht erfunden aus. Haben Sie tatsächlich I / O ( std::cout) in Ihrem Kopierer? Ohne sie sollte die Kopie weg optimiert werden.
Rustyx
@rustyx, entfernen Sie das std :: cout und machen Sie den Kopierkonstruktor explizit. Dies zeigt, dass die Kopierelision nicht von std :: cout abhängig ist.
DDaniel

Antworten:

7
  1. Teilantwort (es wird nicht b2an Ort und Stelle konstruiert , sondern verwandelt die Kopierkonstruktion in eine Verschiebungskonstruktion): Sie können die incrementElementfunktion für die Wertekategorie der zugeordneten Instanz überladen :

    auto& increment() & {
        i++;
        return *this;
    }
    
    auto&& increment() && {
        i++;
       return std::move(*this);
    }
    

    Dies bewirkt

    auto b2 = test_class{7}.increment();

    zu verschieben-konstruieren, b2weil test_class{7}es ein temporäres ist und die &&Überladung von test_class::incrementaufgerufen wird.

  2. Für eine echte In-Place-Konstruktion (dh nicht einmal eine Verschiebungskonstruktion) können Sie alle speziellen und nicht speziellen Elementfunktionen in constexprVersionen umwandeln. Dann können Sie tun

    constexpr auto b2 = test_class{7}.increment();

    und Sie müssen weder einen Umzug noch eine Kopierkonstruktion bezahlen. Dies ist natürlich für das einfache test_class, aber nicht für ein allgemeineres Szenario möglich, das keine constexprMitgliedsfunktionen zulässt .

lubgr
quelle
Die zweite Alternative macht auch b2nicht modifizierbar.
Einige Programmierer Typ
1
@Someprogrammerdude Guter Punkt. Ich denke, es constinitwäre der Weg dorthin.
lubgr
Was ist der Zweck der kaufmännischen Und-Zeichen nach dem Funktionsnamen? Das habe ich noch nie gesehen.
Philip Nelson
1
@PhilipNelson sie sind Ref-Qualifikanten und können diktieren, was thisgenau wie constMitgliedsfunktionen ist
kmdreko
1

Grundsätzlich erfordert das Zuweisen eines auf einen Aufrufen eines Konstruktors, dh einer Kopie oder einer Verschiebung . Dies unterscheidet sich von bei der auf beiden Seiten der Funktion bekannt ist, dass es sich um dasselbe unterschiedliche Objekt handelt. Eine kann sich ähnlich wie ein Zeiger auf ein freigegebenes Objekt beziehen.


Der einfachste Weg ist wahrscheinlich, den Kopierkonstruktor vollständig zu optimieren. Die Werteinstellung wird bereits vom Compiler optimiert, es ist nur die std::cout, die nicht weg optimiert werden kann.

test_class(const test_class& t) = default;

(oder entfernen Sie einfach sowohl den Kopier- als auch den Verschiebungskonstruktor)

Live-Beispiel


Da Ihr Problem im Wesentlichen mit der Referenz zusammenhängt, gibt eine Lösung wahrscheinlich keine Referenz auf das Objekt zurück, wenn Sie das Kopieren auf diese Weise beenden möchten.

  void increment();
};

auto b = test_class{7};//does not call copy constructor
b.increment();//does not call copy constructor

Eine dritte Methode basiert in erster Linie auf der Kopierentfernung. Dies erfordert jedoch ein Umschreiben oder Einkapseln des Vorgangs in eine Funktion, um das Problem insgesamt zu vermeiden (ich bin mir bewusst, dass dies möglicherweise nicht das ist, was Sie möchten, aber es könnte ein Problem sein Lösung für andere Benutzer):

auto b2 = []{test_class tmp{7}; tmp.increment().increment().increment(); return tmp;}(); //<-- b2 becomes 10 - copy constructor not called

Eine vierte Methode verwendet stattdessen eine Verschiebung, die entweder explizit aufgerufen wird

auto b2 = std::move(test_class{7}.increment());

oder wie in dieser Antwort zu sehen .

Darune
quelle
@ O'Neil dachte an einen anderen Fall - korrigiert
Darune