[gelöst]Unterschied Typumwandlungskonstrutor zu Allgemeinen Konstruktor



  • Guten Abend,

    ich lese mich gerade in den Typumwandlungskontruktor ein und verstehe nicht ganz wie man diesen von einem Allgemeinen Konstruktor unterscheiden kann. Bei einem Kopierkonstruktor z.B. wird ja eine Referenz auf dieselbe Klasse als Argument mit übergeben und daran erkennt man ihn dann, aber wie erkennt der Compiler einen Typumwandlungskonstruktor? Wenn es wirklich zwei unterschiedliche Dinge sind dann müssten ja auch die Signaturen unterschiedlich sein oder?

    G hibbes


  • Mod

    Das ist an sich nichts besonderes: Bei Zuweisungen wird automatisch geguckt, ob ein passender Konstruktor mit genau einem Argument vorhanden ist. Eigentlich genau wie beim Kopierkonstruktor, nur mit einem anderen Argumententyp.

    Falls du die automatische Umwandlung nicht wünscht, musst du den entsprechenden Konstruktor explicit machen.



  • SeppJ schrieb:

    Das ist an sich nichts besonderes: Bei Zuweisungen wird automatisch geguckt, ob ein passender Konstruktor mit genau einem Argument vorhanden ist. Eigentlich genau wie beim Kopierkonstruktor, nur mit einem anderen Argumententyp.

    Genauer: mit min. einem Argument und max. einem Argument welches keinen Default-Wert hat (wobei auch alle Argumente einen Default-Wert haben dürfen).
    Dasselbe gilt im Übrigen auch für den Copy-Ctor.
    Und der Default-Ctor darf auch ruhig Argumente haben, so lange alle einen Default-Wert haben.

    Beispiel:

    class foo
    {
    public:
    	static foo f;
    	foo(int a, int b = 123) {} // A
    	foo(void* p, void* p2 = 0) {} // B (auch "p" dürfte einen default-wert haben, nur dann hätten wir mit C zwei default-konstruktoren, was nicht OK ist)
    	foo(foo const& foo = f, int* p = 0) {} // C - copy ctor + default ctor
    };
    
    foo foo::f; // calls C   EDIT: das ist natürlich unfug, da hier C mit dem default-wert aufgerufen würde,
                //           und der default-wert ist f. also genau das objekt welches initialisiert werden soll. henne/ei und so.
                //           syntaktisch OK, aber totaler unfug und zur laufzeit undefiniertes verhalten.
                //           man müsste f also mit einem anderen konstruktor initialisieren, dann wäre es OK
    
    void bar(foo f)
    {
    }
    
    foo bar()
    {
    	foo f; // calls C as default-ctor
    	bar("seppdepp"); // calls B
    	return 1; // calls A
    }
    
    int main()
    {
    	foo f(bar()); // may call C as copy-ctor (may be elided via RVO)
    	foo f2 = f; // calls C as copy-ctor
    }
    

    EDIT: siehe code



  • Danke für die wirklich ausführlichen Erklärungen. Ein Konstruktoraufruf macht mich erstmal stutzig, aber is ja auch schon spät 🙂

    foo bar()
    {
        foo f; // calls C as default-ctor
        bar("seppdepp"); // calls B
        return 1; // calls A: nicht doch C weil copy und default hier eins sind?
    }
    

    Wird hier nicht bei dem Return der Copy-Ctor aufgerufen, bei meinen Tests wird der aufgerufen wenn das Objekt der Klasse per Kopie übergeben wird und wenn es eine Rückgabe vom Typ der Klasse gibt?

    G hibbes



  • Naja, ist ja nicht schwer auszuprobieren:

    #include "stdafx.h"
    #include <iostream>
    
    class foo
    {
    public:
    	char const* m_a;
    
    	foo(char const* a)
    	{
    		m_a = a;
    		std::cout << "conversion ctor: a = " << m_a << "\n";
    	}
    
    	foo(foo const& other)
    	{
    		m_a = other.m_a;
    		std::cout << "copy ctor: a = " << m_a << "\n";
    	}
    
    	void use()
    	{
    		std::cout << "use: a = " << m_a << "\n";
    	}
    
    	void main_checkpoint()
    	{
    		std::cout << "main checkpoint: a = " << m_a << "\n";
    	}
    };
    
    foo test_implicit()
    {
    	return "implicit conversion";
    }
    
    foo test_explicit()
    {
    	return foo("explicit conversion");
    }
    
    foo test_named_value()
    {
    	foo f("named return value");
    	return f;
    }
    
    foo test_named_used_value()
    {
    	foo f("named & used return value");
    	f.use();
    	return f;
    }
    
    foo nested_direct_helper(char const* a)
    {
    	return foo(a);
    }
    
    foo nested_named_used_helper(char const* a)
    {
    	foo f(a);
    	f.use();
    	return f;
    }
    
    foo test_nested_direct()
    {
    	return nested_direct_helper("nested direct");
    }
    
    foo test_nested_named_used()
    {
    	return nested_named_used_helper("nested named used");
    }
    
    foo test_nested_2x_named_2x_used()
    {
    	foo f(nested_named_used_helper("nested 2x named 2x used"));
    	f.use();
    	return f;
    }
    
    int main()
    {
    	{
    		foo f(test_implicit());
    		f.main_checkpoint();
    		std::cout << "\n\n";
    	}
    	{
    		foo f(test_explicit());
    		f.main_checkpoint();
    		std::cout << "\n\n";
    	}
    	{
    		foo f(test_named_value());
    		f.main_checkpoint();
    		std::cout << "\n\n";
    	}
    	{
    		foo f(test_named_used_value());
    		f.main_checkpoint();
    		std::cout << "\n\n";
    	}
    	{
    		foo f(test_nested_direct());
    		f.main_checkpoint();
    		std::cout << "\n\n";
    	}
    	{
    		foo f(test_nested_named_used());
    		f.main_checkpoint();
    		std::cout << "\n\n";
    	}
    	{
    		foo f(test_nested_2x_named_2x_used());
    		f.main_checkpoint();
    		std::cout << "\n\n";
    	}
    }
    

    Mit VC10 mit Debug Einstellungen compiliert:

    conversion ctor: a = implicit conversion
    main checkpoint: a = implicit conversion
    
    conversion ctor: a = explicit conversion
    main checkpoint: a = explicit conversion
    
    conversion ctor: a = named return value
    copy ctor: a = named return value
    main checkpoint: a = named return value
    
    conversion ctor: a = named & used return value
    use: a = named & used return value
    copy ctor: a = named & used return value
    main checkpoint: a = named & used return value
    
    conversion ctor: a = nested direct
    main checkpoint: a = nested direct
    
    conversion ctor: a = nested named used
    use: a = nested named used
    copy ctor: a = nested named used
    main checkpoint: a = nested named used
    
    conversion ctor: a = nested 2x named 2x used
    use: a = nested 2x named 2x used
    copy ctor: a = nested 2x named 2x used
    use: a = nested 2x named 2x used
    copy ctor: a = nested 2x named 2x used
    main checkpoint: a = nested 2x named 2x used
    

    Hier wird also noch ein paar mal der Copy-Ctor aufgerufen. Bei den ganz einfachen Beispielen "fehlt" er allerdings schon.

    Mit Release Einstellungen sieht es dann so aus:

    conversion ctor: a = implicit conversion
    main checkpoint: a = implicit conversion
    
    conversion ctor: a = explicit conversion
    main checkpoint: a = explicit conversion
    
    conversion ctor: a = named return value
    main checkpoint: a = named return value
    
    conversion ctor: a = named & used return value
    use: a = named & used return value
    main checkpoint: a = named & used return value
    
    conversion ctor: a = nested direct
    main checkpoint: a = nested direct
    
    conversion ctor: a = nested named used
    use: a = nested named used
    main checkpoint: a = nested named used
    
    conversion ctor: a = nested 2x named 2x used
    use: a = nested 2x named 2x used
    use: a = nested 2x named 2x used
    main checkpoint: a = nested 2x named 2x used
    

    Wie man sieht wird hier kein einziges mal der Copy-Ctor aufgerufen. Alle Aufrufe wurden wegoptimiert.

    Die Regeln die das ganze laut Standard erlauben nennen sich RVO ("return value optimization") und NRVO ("named return value optimization").



  • p.S.: vielleicht hab ich auch deine Frage falsch verstanden.

    hibbes schrieb:

    return 1; // calls A: nicht doch C weil copy und default hier eins sind?
    

    Da steht 1 (eins), und das ist ein Integer-Literal (eine Konstante).
    Da ein Integer kein "foo" ist, muss irgendwie konvertiert werden, und das kann nur der Konvertierungskonstruktor "A".

    Es darf danach zusätzlich der Copy-Ctor "C" aufgerufen werden, muss aber nicht. Und wird in so einfachen Fällen meist auch nicht.



  • Vielen vielen lieben Dank,
    da hast du dir ja echt Mühe gegeben einem C++ Anfänger was zu erklären. Das bei dem return 1 ja einen Typumwandlung stattfindet hatte ich bei meiner Frage nicht bedacht und nun macht dein Beispiel auch Sinn.

    Ich denke deine anschaulichen Beispiele werden auch Anderen eine große Hilfe sein, danke nochmal. Du solltest C++ Dozent werden 😉

    G hibbes


Anmelden zum Antworten