Frage zu Funktionsobjekten



  • Hallo,
    ich habe eine Frage zu Funktionsobjekten. Der Code und die Ausgabe sind unten. Meine Frage ist: Wieso wird der Destruktor 3 mal aufgerufen? Der Konstruktor wird anscheinend nur einmal aufgerufen.

    class ausgabe
    {
    public:
    	void operator()(const int& x)
    	{
    		cout << x << " ";
    	}
    	
    	ausgabe()
    	{
    		cout << "Konstruktor!!" << endl;
    	}
    
    	~ausgabe()
    	{
    		cout << "Destruktor!!" << endl;
    	}
    };
    
    int main()
    {
    	list<int> l;
    	for (int i=0; i<20; i++)
    		l.push_back(i);
    	for_each(l.begin(), l.end(), ausgabe());
            ...
    

    Ausgabe:
    Konstruktor!!
    0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Destruktor!!
    Destruktor!!
    Destruktor!!
    Drücken Sie eine beliebige Taste . . .



  • [class.copy.ctor]/8:

    If the definition of a class X does not explicitly declare a move constructor, a non-explicit one will be implicitly declared as defaulted if and only if

    (8.1) X does not have a user-declared copy constructor,
    (8.2) X does not have a user-declared copy assignment operator,
    (8.3) X does not have a user-declared move assignment operator, and
    (8.4) X does not have a user-declared destructor.

    [ Note: When the move constructor is not implicitly declared or explicitly supplied, expressions that otherwise would have invoked the move constructor may instead invoke a copy constructor. — end note ]

    Deine Klasse hat also keinen move-ctor. Zuerst beginnt bei der Parameterübergabe an for_each() ein temporary ausgabe zu leben (ctor), wird in den Parameter kopiert (dein ctor läuft nicht, sondern der implicitly defined copy-ctor). Das temporary wird zerstört (1. dtor), for_each() tut sein ding. Da deine Klasse keinen move-ctor hat wird am Ende von for_each() der Parameter als Kopie zurückgegeben. Der Parameter wird am Ende von for_each() zerstört (2. dtor). Die von for_each() zurückgegebene Kopie wird zerstört (3. dtor).

    Dasselbe in Grün:

    #include <iostream>
    
    struct foo
    {
        foo() { std::cout << "ctor\n"; }
        // foo(foo const &)  ... implicitly defined copy-ctor
        // foo(foo &&)  ... move-ctor gibt es nicht
        ~foo() { std::cout << "dtor\n"; }
    };
    
    foo bar(foo f)
    {
        return f;  // zur Rückgabe wird kopiert, moven geht ja nicht ^^
    }  // f wird zerstört (2. dtor) 
    
    int main()
    {
        bar(
            foo{}  // ctor fuer das temporary, dann copy-ctor in den parameter, moven geht ja nicht
        )  // temporary wird zerstört (1. dtor)
        ;  // von bar() zurückgegebenes foo wird zerstoert (3. dtor)
    }
    


  • Ergänzung: In einem optimierten Build wird der Dtor nur 2x aufgerufen. Dabei wird das Temporary beim Funktionsaufruf weggelassen, der Parameter wird direkt konstruiert.



  • Hier wurde ja schon erklärt. Zum besseren Nachvollziehen ergänzt du am besten deine Ausgabe-Klasse um (wie schon von @Swordfish angedeutet):

    class ausgabe
    {
    public:
        // deine bisherigen Funktionen hier
    
        ausgabe(const ausgabe&)
        {
            cout << "Kopierkonstruktor!!" << endl;
        }
    };
    

    Dann damit ausprobieren.

    Und dann implementierst du noch den Move-Konstruktor:

        ausgabe(ausgabe&&)
        {
            cout << "Move-Konstruktor!!" << endl;
        }
    

    Und schaust erneut, was passiert.



  • Weil es neben dem Standardkonstruktor auch noch den Kopierkonstruktor (copy constructor) gibt:

    ausgabe(const ausgabe &other)
    {
    	cout << "Kopierkonstruktor!!" << endl;
    }
    

    Edit: Ups, hatte die anderen Antworten nicht gelesen (manchmal springt er nicht zum letzten Beitrag...).



  • @hustbaer sagte in Frage zu Funktionsobjekten:

    Ergänzung: In einem optimierten Build wird der Dtor nur 2x aufgerufen. Dabei wird das Temporary beim Funktionsaufruf weggelassen, der Parameter wird direkt konstruiert.

    Ja, und dann noch zusätzlich zu Copy-Elison die RVO. Kopfschmerzen.



  • @Swordfish Naja ge-returnte Parameter können nicht ge-NRVOt werden 🙂

    Weiss nicht ob der Standard es theoretisch erlauben würde, aber auf Grund der Calling-Conventions kann das im non-inline Fall schonmal gar nicht hinhauen.

    Wobei es im Inline Fall interessant wäre ob's theoretisch erlaubt ist.



  • @hustbaer Ich hab' ja gesagt

    @Swordfish sagte in Frage zu Funktionsobjekten:

    Kopfschmerzen.

    @hustbaer sagte in Frage zu Funktionsobjekten:

    ge-NRVOt

    Den Ausdruck merk' ich mir weil so furchtbar elogant ;p

    @hustbaer sagte in Frage zu Funktionsobjekten:

    Wobei es im Inline Fall interessant wäre ob's theoretisch erlaubt ist.

    Natürlich? Solange as-if ... ?



  • @Swordfish sagte in Frage zu Funktionsobjekten:

    @hustbaer sagte in Frage zu Funktionsobjekten:

    Wobei es im Inline Fall interessant wäre ob's theoretisch erlaubt ist.

    Natürlich? Solange as-if ... ?

    Nö ich mein schon den beobachtbaren Teil. Bei (N)RVO ist ja erlaubt dass wirklich die beobachtbaren Effekte der Kopie wegfallen. Sonst wäre es ja uninteressant, z.B. schonmal da AFAIK die meisten Compiler Speicheranforderungen als beobachtbar einstufen (bin mir auch nichtmal sicher ob es vom Standard her überhaupt schon erlaubt wäre Speicheranforderungen wegzuoptimieren).

    Also egal was der Standard dazu sagt, es scheint kein aktueller Compiler mit nur einem Dtor-Aufruf zu machen: https://godbolt.org/z/JgWJ__



  • @hustbaer Schönes Beispiel. Ich habe grad keine Lust den Standart danach abzugrasen.



  • Huiiiii, Clang optimiert new+delete weg: https://godbolt.org/z/HyhyHH 🙂
    Aber leider (noch?) nicht std::allocator Aufrufe 😞



  • @hustbaer sagte in Frage zu Funktionsobjekten:

    Aber leider (noch?) nicht std::allocator Aufrufe

    Naja, die könnten doch auch side-effects haben?



  • @Swordfish Konstruktoren können auch Nebeneffekte haben. So wie auch selbst definierte globale operator new/delete Nebeneffekte haben könnten. Trotzdem dürfen die wohl wegoptimiert werden. (Konstruktoren sicher siehe (N)RVO & Co., und bei den globalen operator new/delete ist zumindest Clang der Meinung dass es schon OK wäre die Aufrufe wegzuoptimieren.)
    Und warum dann nicht auch std::allocator?



  • @hustbaer sagte in Frage zu Funktionsobjekten:

    @Swordfish Konstruktoren können auch Nebeneffekte haben. So wie auch selbst definierte globale operator new/delete Nebeneffekte haben könnten. Trotzdem dürfen die wohl wegoptimiert werden. (Konstruktoren sicher siehe (N)RVO & Co., und bei den globalen operator new/delete ist zumindest Clang der Meinung dass es schon OK wäre die Aufrufe wegzuoptimieren.)
    Und warum dann nicht auch std::allocator?

    Habe ich nicht gesagt Kopfschmerzen?? Ich denke darüber heute sicher nicht mehr nach.

    @hustbaer sagte in Frage zu Funktionsobjekten:

    bei den globalen operator new/delete ist zumindest Clang der Meinung dass es schon OK wäre die Aufrufe wegzuoptimieren.

    Definier' sie mal selbst, ist clang dann immer noch derselben Meinung?


Anmelden zum Antworten