Templates und deren Grenzen



  • pumuckl schrieb:

    1. Ist die Variante mit dem new char... nicht nur unschön, sie müsste sogar wenn mans genau nimmt noch unschöner sein - aus C-Cast mach reinterpret_cast

    Jopp - das wärs schon noch geworden, aber wollte ja erst mal wissen, ob es von der theorie so richtig wäre

    pumuckl schrieb:

    1. ist die Variante dann nicht nur noch unschöner, sondern es müsste eigentlich, getriggert durch den reinterpret_cast auf ein Array, folgendes passieren: sämtliche Alamrglocken schrillen und es blinkt ein großes, grelles Schild mit der Aufschrift "alignment/padding?!"

    Oh - daran hatte ich wirklich nicht gedacht - aber habs ja jz auch wieder mit nem "sauberen" new und placement new - aber das speicherleck bleibt bestehen - und ich weiß nicht, warum -.-

    pumuckl schrieb:

    1. Die neue Variante ist zwar schöner anzusehn, aber nicht exception-safe (falls das für dich von Bedeutung ist): Wenn irgendwo im Rest der init-Liste eine Exception hochkommt dann hat das Objekt selbst nie existiert, weil nie alle Member initialisiert wurden, und alles was bleibt nachdem die bereits initialisierten Objekte abgeräumt wurden ist ein Speicherleck. Also kein new in Init-Listen, wenn man Exception safe bleiben möchte (es sei denn man initialisiert damit einen Smartpointer, dessen entsprechender Konstruktor unter Garantie keine Exceptions wirft.)

    Also pack ich nen try / catch um die schleife und fange bad_alloc auf - und falls es geworfen wurde, führe ich den dtor aus und werfe die exception einfach ganz normal weiter?!

    bb

    edit: Also vll weiß ichs auch einfach nicht, aber ich glaube das normale new darf gar kein new auf Line sein sondern _muss_ auf nen intergralen Typen sein?!
    Meine Lösung sieht jetzt folgendermaßen aus
    (ich weiß nicht, ob man den exception-safe ctor normalerweise ähnlich implementiert aber ich hab anscheind noch irgendwo nen fehler drin - jedenfalls gibt er bei nem bad_alloc anscheind nicht wieder alles frei ><

    Minimal-Code (fertig für copy&paste ^^):

    template <size_t line_count, size_t row_count, typename ValueType>
    class TMatrix
    {
    public:
    	typedef size_t					SizeType;
    
    	class Line
    	{
    	private:
    		ValueType *values;
    
    		void destruct(SizeType i = row_count) throw()
    		{
    			if(!values)
    				return;
    
    			for(; i != 0; --i)
    			{
    				values[i-1].~ValueType();
    			}
    			delete [] values;
    			values = nullptr;
    		}
    	public:
    		Line(ValueType fill_with = ValueType())
    			:	values(new ValueType[row_count])
    		{
    			SizeType i = SizeType();
    			try
    			{
    				for(; i != row_count; ++i)
    				{
    					new (values + i) ValueType(fill_with);
    				}
    			}
    			catch (...)
    			{
    				destruct(i);
    				throw;
    			}
    		}
    
    		~Line()
    		{
    			destruct();
    		}
    	};
    
    private:
    	Line *lines;
    
    	void destruct(SizeType i = line_count) throw()
    	{
    		if(!lines)
    			return;
    
    		for(; i != 0; --i)
    		{
    			lines[i-1].~Line();
    		}
    		delete [] reinterpret_cast <char*> (lines);
    		lines = nullptr;
    	}
    
    public:
    	TMatrix(ValueType fill_with = ValueType())
    		:	lines ( reinterpret_cast <Line*> (new char[sizeof(Line) * line_count]) )
    	{
    		SizeType i = SizeType();
    		try
    		{
    			for(; i != line_count; ++i)
    			{
    				new (lines + i) Line(fill_with);
    			}
    		}
    		catch(...)
    		{
    			destruct(i);
    			throw;
    		}
    	}
    
    	~TMatrix()
    	{
    		destruct();
    	}
    };
    

    Nun könnte man aus destruct noch nen template machen und wahrscheinlich auch noch nen template für init oder so - aber im Grunde genommen sollte es doch so (oder ähnlich) gehen, oder?

    Zu Alignment/Padding: Sicher, dass das hier eine Rolle spielt?

    bb und danke schon mal fürs angucken?! : >



  • Dravere schrieb:

    // Und jetzt Spezialisierungen:
    template <typename Treturnvaluetype, typename Tvaluetype>
    Treturnvaluetype Determinant_LaPlace<Treturnvaluetype, 2, Tvaluetype> (const TMatrix <2, 2, Tvaluetype> &thismatrix)
    {
        return Treturnvaluetype(thismatrix[0][0] * thismatrix[1][1] - thismatrix[0][1] * thismatrix[1][0]);
    }
    // ...
    

    Leider sind partielle Spezialisierungen von Funktionstemplates nicht erlaubt, oder kompiliert das dein Compiler ?



  • *mist*


  • Administrator

    KasF schrieb:

    Leider sind partielle Spezialisierungen von Funktionstemplates nicht erlaubt, oder kompiliert das dein Compiler ?

    Jap, tut er. Zwar habe ich jetzt nicht genau das gemacht, aber habe ein foo-Test gemacht:

    #include "Test.hpp"
    
    #include <iostream>
    
    template<typename T>
    void foo()
    {
    	std::cout << "T!" << std::endl;
    }
    
    template<>
    void foo<void>()
    {
    	std::cout << "void" << std::endl;
    }
    
    int main()
    {
    	foo<int>();
    	foo<void>();
    
    	test::wait("Press ENTER to exit...");
    
    	return 0;
    }
    

    Mir persönlich ist der genaue Standard dazu nicht bekannt. Verwende den MSVC 2005.

    Aber was soll, dann nimmt man statt Templatespezialisierung Funktionsüberladung. Sollte genauso gehen.

    Grüssli



  • Das entscheidende war, dass du dort eben partielle Spezialisierung benutzt hast. Komplette Spezialisierung ist erlaubt, partielle eben nicht..


  • Administrator

    drakon schrieb:

    Das entscheidende war, dass du dort eben partielle Spezialisierung benutzt hast. Komplette Spezialisierung ist erlaubt, partielle eben nicht..

    Achso ... *Kopf -> Tisch*
    Das kommt ja auch erst mit dem nächsten Standard 🙂

    Naja, aber in diesem Fall sollten Funktionsüberladungen Abhilfe schaffen.

    Grüssli



  • Dravere schrieb:

    Das kommt ja auch erst mit dem nächsten Standard 🙂

    Sind in C++0x partielle Spezialisierungen von Funktionen möglich? Was war der Grund, sie bisher nicht einzuführen?

    Dravere schrieb:

    *Kopf -> Tisch*

    Hehe, als ich das zuerst gesehen habe, dachte ich, es handle sich um eine Dereferenzierung. Dann hat mich verwirrt, dass bei "Tisch" das * nach dem Bezeichner steht... Vielleicht sollte ich langsam ins Bett... 😃



  • Dravere schrieb:

    drakon schrieb:

    Das entscheidende war, dass du dort eben partielle Spezialisierung benutzt hast. Komplette Spezialisierung ist erlaubt, partielle eben nicht..

    Achso ... *Kopf -> Tisch*
    Das kommt ja auch erst mit dem nächsten Standard 🙂

    Naja, aber in diesem Fall sollten Funktionsüberladungen Abhilfe schaffen.

    Grüssli

    Hehe..

    Wenn wir schon bei dabei sind..

    Das dürfte für dich interessant sein (warst doch du,der Probleme hatte, oder?:))
    http://developer.amd.com/documentation/videos/pages/IntroductiontoAMDCodeAnalystPerformanceAnalyzer.aspx

    (Achtung lautes Video.. -.-)


  • Administrator

    Nexus schrieb:

    Dravere schrieb:

    Das kommt ja auch erst mit dem nächsten Standard 🙂

    Sind in C++0x partielle Spezialisierungen von Funktionen möglich? Was war der Grund, sie bisher nicht einzuführen?

    Ich dachte ich hätte mal sowas gehört. Aber kann es grad nicht finden im letzten Draft. Aber das Zeug finde ich teilweise sowieso etwas komplex beschrieben 🙂
    Oder ich bin einfach zu müde und zu viel gelernt heute, vielleicht bring ich ein paar Dinge grad völlig durcheinander 😉

    Grüssli



  • bisher hat der msvc eigtl immer warnungen gebracht, wenn er was gemacht hat, was noch nicht im standard zu finden ist, deshalb war ich davon ausgegangen, dass partielle Spezialisierung durchaus im Standard ist - aber kA, das war ja jz au net mehr das Problem ^^

    Ich habe jz folgenden Code:
    Aber anscheind wird im nicht-bad_alloc-Fall nicht der komplette Speicher freigegeben (ein vergleichsmäßig kleiner Teil wird "vergessen", glaube ich) und ohne den cast auf char* hab ichs gar nicht hinbekommen - womit die Probleme mit Padding bleiben - die ich aber zumindest mit den aktuell verwendeten Datentypen umgehe (Pointer bzw double sollten ja von der größe her beide gut geeignet sein)

    Helfer für CTor (placement new) und DTor (expliziter DTor-Aufruf + delete)

    template <typename T>
    void destruct(T * &array_values, size_t count) throw()
    {
    	if(!array_values)
    		return;
    
    	for(SizeType i = SizeType(); i != count; ++i)
    	{
    		T *tmp = array_values + i;
    		if (tmp)
    			tmp->~T();
    	}
    
    	delete [] reinterpret_cast <char*> (array_values);
    	array_values = nullptr;
    }
    
    template <typename T, SizeType, typename TCtorArg>
    void init(T * &array_values, size_t count, const TCtorArg &ctor_argument = TCtorArg())
    {
    	try
    	{
    		array_values = reinterpret_cast <T*> ( new char[sizeof(T) * count] );
    	}
    	catch (...)
    	{
    		array_values = nullptr;
    		throw;
    	}
    
    	for(SizeType i = SizeType(); i != count; ++i)
    	{
    		try
    		{
    			new (array_values + i) T(ctor_argument);
    		}
    		catch (...)
    		{
    			destruct(array_values, i);
    			throw;
    		}
    	}
    }
    

    die ich wie folg verwendet habe:

    Line(ValueType fill_with = ValueType())
    {
    	try
    	{
    		alloc::placement_new::init(values, row_count, fill_with);
    	}
    	catch (...)
    	{
    		alloc::placement_new::destruct(values, row_count);
    		throw;
    	}
    }
    
    ~Line() throw()
    {
    	alloc::placement_new::destruct(values, row_count);
    }
    
    TMatrix(ValueType fill_with = ValueType())
    {
    	try
    	{
    		alloc::placement_new::init(lines, line_count, fill_with);
    	}
    	catch (...)
    	{
    		alloc::placement_new::destruct(lines, line_count);
    		throw;
    	}
    }
    
    ~TMatrix() throw()
    {
    	alloc::placement_new::destruct(lines, line_count);
    }
    

    Ich wäre über eine Reaktion sehr erfreut ^^

    bb



  • die try catch sind meines Wissens nicht nötig. Was ich dir oben versucht hab zu sagen ist folgendes:

    class B
    {
      A* aptr;
      C  c;
    
    public:
      B() : aptr(new A()), c() 
      {}
    };
    

    Wenn jetzt z.B. der Konstruktor von c eine Exception auslöst, dann geschieht folgendes:
    - alle bis dahin initialisierten Member von B werden zerstört. Da aptr ein POD ist und keinen Dtor hat wirds einfach gelöscht. Speicherleck geschaffen
    - Da der Ctor von B nicht vollständig ausgeführt worden ist, hat das B-Objekt nie exisitiert, also wird auch kein Dtor für B aufgerufen. Speicherleck lebt weiter.
    - da das B-Objekt nie existiert hat, kann auch von außen niemand den für aptr allokierten Speicher freigeben. Das Speicherleck bleibt also und ist nicht mehr zu retten.

    Meine Aussage war also lediglich, dass die Aufrufe von new() in der Init-Liste eines Ctors für einfache Pointer nicht exceptionsicher sind.
    Folgende Möglichkeiten gibts:

    class B1
    {
      A* aptr;
      C  c;
    
    public:
      B1() : aptr(0), c() 
      {
        try {
         aptr = new a(); 
        }
        catch(...)
        { /* delete alle pointer */ }
      }
    };
    
    class B2
    {
      shared_ptr<A> aptr;
      C  c;
    
    public:
      B2() : aptr(new A()), c() 
      {}
    };
    

    Bei Variante 1 kann man das try/catch weglassen, wenns nur einen Pointer in der Klasse gibt der so initialisiert wird. Bei einem fehlschlagenden new wid der allokierte Speicher immer freigegeben. Ab zwei Pointern muss aber try/catch eingebaut werden: wenn das zweite new eine exception wirft muss der erste seinen Speicher freigeben.
    Variante 2 geht deshalb, weil der smart-pointer den allokierten speicher wieder freigibt, wenn im Laufe der Konstruktion von B2 eine exception fliegt.



  • Wenn es nur einen Pointer und c gibt kann man auch einfach die Initialisierungsreihenfolge umdrehen. Dann wird aptr erst nach dem Konstruieren von c alloziert und du hast kein Speicherleck mehr.



  • Braunstein schrieb:

    Wenn es nur einen Pointer und c gibt kann man auch einfach die Initialisierungsreihenfolge umdrehen. Dann wird aptr erst nach dem Konstruieren von c alloziert und du hast kein Speicherleck mehr.

    Stimmt. Allerdings musst du dafür die Reihenfolge der Memberdeklarationen entsprechend ändern. Und wenn du ein paar Wochen später zu dem Schluss kommst dass da noch ein Member in die Klasse muss, hängst du den hinten ran, dessen Ctor schmeißt und - juhu. Wieder Speicherleck. Sowas macht man entweder richtig und robust oder man lässts gleich bleiben.



  • pumuckl schrieb:

    Braunstein schrieb:

    Wenn es nur einen Pointer und c gibt kann man auch einfach die Initialisierungsreihenfolge umdrehen. Dann wird aptr erst nach dem Konstruieren von c alloziert und du hast kein Speicherleck mehr.

    Stimmt. Allerdings musst du dafür die Reihenfolge der Memberdeklarationen entsprechend ändern. Und wenn du ein paar Wochen später zu dem Schluss kommst dass da noch ein Member in die Klasse muss, hängst du den hinten ran, dessen Ctor schmeißt und - juhu. Wieder Speicherleck. Sowas macht man entweder richtig und robust oder man lässts gleich bleiben.

    Naja. Das hast du bei deiner Variante 1 auch. Das "einzig" robuste, was man machen sollte, ist eine RAII Klasse zu benutzen. Dann hat man Ruhe.



  • Hihi.
    Mal sehen wie lange es dauert bis auch in diesem Thread die alte RAII Debatte ausbricht 🙂
    *pfeiff, daumen dreh und wart*



  • pumuckl schrieb:

    B1() : aptr(0), c() 
      { 
        try { 
         aptr = new a(); 
        } 
        catch(...) 
        { /* delete alle pointer */ } 
      }
    

    Anders hab ichs doch nun auch nicht gemacht!?
    Jopp - das try / catch im CTor zusätzlich noch mal zu haben war vermutlich wirklich unnötig - also hab ich jz nur noch einen Funktions-Aufruf im CTor...

    bb



  • Wieso wird eigentlich immer shared_ptr als Smart Pointer vorgeschlagen? Je nachdem bietet er nur unnötige Funktionalität. Meines Erachtens hat er etwa den gleichen Status wie std::vector - einfach mal verwenden, solange man nichts Besseres weiss oder sich nicht darum kümmern will.

    Wenn die Klasse nicht kopiert oder zugewiesen werden muss, kann man auch scoped_ptr verwenden. Wenn bei Kopien oder Zuweisungen der Besitz übertragen werden soll, stellt auto_ptr eine Möglichkeit dar. Ich glaube zwar, die meisten hier wissen das, aber ich wollte es trotzdem mal erwähnen. 😉



  • Was hast du denn gegen boost::shared_ptr bzw. std::vector?



  • hustbaer schrieb:

    Was hast du denn gegen boost::shared_ptr bzw. std::vector?

    Ich hab nichts gegen sie. Ich finde es nur ein wenig schade, dass sie immer als erstes und meistens auch als einziges erwähnt werden. Häufig habe ich das Gefühl, gerade bei std::vector , dass man ihn als Anfänger viel zu oft empfohlen bekommt. Selbst denkt man da nicht daran, dass es noch std::list für verkettete Listen oder std::tr1::array für statische Arrays gibt.

    Gleiches gilt für shared_ptr : Es ist nicht immer so, dass sich mehrere Instanzen einen Zeiger teilen müssen. Wenn man beispielsweise jede Instanz für sich verwaltet und von den anderen abgrenzt, ist scoped_ptr besser geeignet. Nicht nur wegen des Overheads; man sieht auch gerade, dass das Objekt nicht kopiert werden kann. Bei shared_ptr hingegen ist die Gefahr grösser, dass man normal kopiert, sich aber später fragt, warum hinter den beiden Zeigern das gleiche Objekt steht.

    Naja, das Ganze ist vielleicht ein bisschen weit hergeholt, aber bei mir war es so, dass ich am Anfang kaum Ahnung von STL hatte und einfach immer std::vector statt C-Arrays benutzte (ohne Iteratoren). Mir standen deswegen auch weniger Möglichkeiten offen. Klar, wenn man sich wirklich interessiert, liest man sich auch mehr zur Thematik ein. Trotzdem halte ich es nicht für falsch, die Alternativen auch noch aufzuzählen. 😉



  • Hm. OK.
    Ich empfehle shared_ptr und vector weil es die beiden Kandidaten sind die ich zu 90% (mehr wahrscheinlich) verwende.
    Und wozu die Kids verwirren wenn sie's vermutlich eh kaum jemals brauchen werden?

    Und natürlich sollte jeder der sich Programmierer nennen will selbst nachgucken was es so alles in der Standard-Library gibt, bzw. in wirklich bekannten Libraries wie Boost.


Anmelden zum Antworten