Konstruktoren in c ++ ohne neue aufrufen

142

Ich habe oft gesehen, dass Leute Objekte in C ++ mit erstellen

Thing myThing("asdf");

An Stelle von:

Thing myThing = Thing("asdf");

Dies scheint zu funktionieren (mit gcc), zumindest solange keine Vorlagen beteiligt sind. Meine Frage ist jetzt, ist die erste Zeile richtig und wenn ja, sollte ich sie verwenden?

Nils
quelle
25
Jede Form ist ohne neue.
Daniel Daranas
13
Das zweite Formular verwendet den Kopierkonstruktor, also nein, sie sind nicht gleichwertig.
Edward Strange
Ich habe ein bisschen damit gespielt, der erste Weg scheint manchmal zu scheitern, wenn Vorlagen mit parameterlosen Konstruktoren verwendet werden.
Nils
1
Ouh und ich haben das "Nice Question" Abzeichen dafür bekommen, was für eine Schande!
Nils

Antworten:

153

Beide Zeilen sind zwar korrekt, machen aber subtil unterschiedliche Dinge.

In der ersten Zeile wird ein neues Objekt auf dem Stapel erstellt, indem ein Konstruktor des Formats aufgerufen wird Thing(const char*).

Der zweite ist etwas komplexer. Es macht im Wesentlichen das Folgende

  1. Erstellen Sie Thingmit dem Konstruktor ein Objekt vom TypThing(const char*)
  2. Erstellen Sie Thingmit dem Konstruktor ein Objekt vom TypThing(const Thing&)
  3. Rufen Sie ~Thing()das in Schritt 1 erstellte Objekt auf
JaredPar
quelle
7
Ich denke, diese Arten von Aktionen sind optimiert und unterscheiden sich daher in Leistungsaspekten nicht wesentlich.
M. Williams
14
Ich denke nicht, dass deine Schritte ganz richtig sind. Thing myThing = Thing(...)verwendet den Zuweisungsoperator nicht, er ist wie gesagt immer noch Thing myThing(Thing(...))Thing
kopierkonstruiert
1
Sie können also sagen, dass die zweite Zeile falsch ist, da sie ohne ersichtlichen Grund Ressourcen verschwendet. Natürlich ist es möglich, dass das Erstellen der ersten Instanz für einige Nebenwirkungen beabsichtigt ist, aber das ist (stilistisch) noch schlimmer.
MK.
3
Nein, @Jared, es ist nicht garantiert. Aber selbst wenn der Compiler diese Optimierung durchführt, muss auf den Kopierkonstruktor zugegriffen werden können (dh nicht geschützt oder privat), auch wenn er nicht implementiert oder aufgerufen ist.
Rob Kennedy
3
Es scheint, dass die Kopie entfernt werden kann, selbst wenn der Kopierkonstruktor Nebenwirkungen hat - siehe meine Antwort: stackoverflow.com/questions/2722879/…
Douglas Leeder
31

Ich nehme an, mit der zweiten Zeile meinen Sie tatsächlich:

Thing *thing = new Thing("uiae");

Dies wäre die Standardmethode zum Erstellen neuer dynamischer Objekte (die für die dynamische Bindung und den Polymorphismus erforderlich sind) und zum Speichern ihrer Adresse in einem Zeiger. Ihr Code macht das, was JaredPar beschrieben hat, nämlich zwei Objekte zu erstellen (eines hat a übergeben const char*, das andere hat a übergeben const Thing&) und dann den Destruktor ( ~Thing()) für das erste Objekt (das const char*eine) aufzurufen .

Im Gegensatz dazu:

Thing thing("uiae");

Erstellt ein statisches Objekt, das beim Verlassen des aktuellen Bereichs automatisch zerstört wird.

stricken
quelle
1
Leider ist dies in der Tat die häufigste Methode, um neue dynamische Objekte zu erstellen, anstatt auto_ptr, unique_ptr oder verwandte zu verwenden.
Fred Nurk
3
Die Frage des OP war richtig, diese Antwort betrifft ein ganz anderes Problem (siehe @ JaredPars Antwort)
Silmathoron
21

Der Compiler kann das zweite Formular zwar in das erste Formular optimieren, muss es aber nicht.

#include <iostream>

class A
{
    public:
        A() { std::cerr << "Empty constructor" << std::endl; }
        A(const A&) { std::cerr << "Copy constructor" << std::endl; }
        A(const char* str) { std::cerr << "char constructor: " << str << std::endl; }
        ~A() { std::cerr << "destructor" << std::endl; }
};

void direct()
{
    std::cerr << std::endl << "TEST: " << __FUNCTION__ << std::endl;
    A a(__FUNCTION__);
    static_cast<void>(a); // avoid warnings about unused variables
}

void assignment()
{
    std::cerr << std::endl << "TEST: " << __FUNCTION__ << std::endl;
    A a = A(__FUNCTION__);
    static_cast<void>(a); // avoid warnings about unused variables
}

void prove_copy_constructor_is_called()
{
    std::cerr << std::endl << "TEST: " << __FUNCTION__ << std::endl;
    A a(__FUNCTION__);
    A b = a;
    static_cast<void>(b); // avoid warnings about unused variables
}

int main()
{
    direct();
    assignment();
    prove_copy_constructor_is_called();
    return 0;
}

Ausgabe von gcc 4.4:

TEST: direct
char constructor: direct
destructor

TEST: assignment
char constructor: assignment
destructor

TEST: prove_copy_constructor_is_called
char constructor: prove_copy_constructor_is_called
Copy constructor
destructor
destructor
Douglas Leeder
quelle
Was ist der Zweck der statischen Abgüsse zum Aufheben?
Stephen Cross
1
@Stephen Vermeiden Sie Warnungen vor nicht verwendeten Variablen.
Douglas Leeder
10

Ganz einfach, beide Zeilen erstellen das Objekt auf dem Stapel und nicht wie 'neu' auf dem Heap. Die zweite Zeile beinhaltet tatsächlich einen zweiten Aufruf eines Kopierkonstruktors, daher sollte dies vermieden werden (sie muss auch wie in den Kommentaren angegeben korrigiert werden). Sie sollten den Stapel so oft wie möglich für kleine Objekte verwenden, da er schneller ist. Wenn Ihre Objekte jedoch länger als der Stapelrahmen überleben, ist dies eindeutig die falsche Wahl.

Stephen Cross
quelle
Für diejenigen, die mit dem Unterschied zwischen dem Instanziieren von Objekten auf dem Stapel und dem auf dem Heap nicht vertraut sind (dh neu verwenden und nicht neu verwenden ), ist hier ein guter Thread.
Edmqkk
2

Im Idealfall würde ein Compiler die zweite optimieren, dies ist jedoch nicht erforderlich. Der erste ist der beste Weg. Es ist jedoch ziemlich wichtig, die Unterscheidung zwischen Stack und Heap in C ++ zu verstehen, da Sie Ihren eigenen Heap-Speicher verwalten müssen.

Hündchen
quelle
Kann der Compiler garantieren, dass der Kopierkonstruktor keine Nebenwirkungen hat (z. B. E / A)?
Stephen Cross
@ Stephen - es spielt keine Rolle, ob der Kopierkonstruktor E / A ausführt
Douglas Leeder
Ok, ich verstehe, der Compiler darf das zweite Formular in das erste umwandeln und vermeidet dadurch den Aufruf des Kopierkonstruktors.
Stephen Cross
2

Ich habe ein bisschen damit gespielt und die Syntax scheint ziemlich seltsam zu werden, wenn ein Konstruktor keine Argumente akzeptiert. Lassen Sie mich ein Beispiel geben:

#include <iostream> 

using namespace std;

class Thing
{
public:
    Thing();
};

Thing::Thing()
{
    cout << "Hi" << endl;
}

int main()
{
    //Thing myThing(); // Does not work
    Thing myThing; // Works

}

Wenn Sie also nur Thing myThing ohne Klammern schreiben, wird der Konstruktor aufgerufen, während Thing myThing () den Compiler dazu bringt, einen Funktionszeiger oder etwas anderes zu erstellen? !!

Nils
quelle
6
Dies ist eine bekannte syntaktische Mehrdeutigkeit in C ++. Wenn Sie "int rand ()" schreiben, kann der Compiler nicht wissen, ob Sie "int erstellen und standardmäßig initialisieren" oder "Funktion rand deklarieren" meinen. Die Regel ist, dass es wann immer möglich Letzteres wählt.
Jpalecek
1
Und das, Leute, ist die ärgerlichste Analyse .
23.
2

Im Anhang zur JaredPar- Antwort

1-üblicher ctor, 2.-funktion-ähnlicher-ctor mit temporärem Objekt.

Kompilieren Sie diese Quelle irgendwo hier http://melpon.org/wandbox/ mit verschiedenen Compilern

// turn off rvo for clang, gcc with '-fno-elide-constructors'

#include <stdio.h>
class Thing {
public:
    Thing(const char*){puts(__FUNCTION__ );}
    Thing(const Thing&){puts(__FUNCTION__ );}   
    ~Thing(){puts(__FUNCTION__);}
};
int main(int /*argc*/, const char** /*argv*/) {
    Thing myThing = Thing("asdf");
}

Und Sie werden das Ergebnis sehen.

Aus ISO / IEC 14882 2003-10-15

8.5, Teil 12

Ihre 1. und 2. Konstruktion werden als Direktinitialisierung bezeichnet

12.1, Teil 13

Mit einer Konvertierung des funktionalen Notationstyps (5.2.3) können neue Objekte dieses Typs erstellt werden. [Hinweis: Die Syntax sieht aus wie ein expliziter Aufruf des Konstruktors. ] ... Ein auf diese Weise erstelltes Objekt ist unbenannt. [Hinweis: 12.2 beschreibt die Lebensdauer temporärer Objekte. ] [Hinweis: Explizite Konstruktoraufrufe liefern keine l-Werte, siehe 3.10. ]]


Wo kann man über RVO lesen:

12 Spezielle Elementfunktionen / 12.8 Kopieren von Klassenobjekten / Teil 15

Wenn bestimmte Kriterien erfüllt sind, kann eine Implementierung die Kopierkonstruktion eines Klassenobjekts weglassen, selbst wenn der Kopierkonstruktor und / oder der Destruktor für das Objekt Nebenwirkungen haben .

Schalten Sie es mit dem Compiler-Flag aus dem Kommentar aus, um ein solches Kopierverhalten anzuzeigen.

Bruziuz
quelle