PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Zirkel-Initialisierung vermeiden?



7.e.Q
25-10-2006, 14:29
Hi Leute,

ich nutze für mein C++ Programm das Design Pattern Singletons, da ich Objekte habe, die programmglobal nur ein einziges Mal existieren sollen. Singletons bieten sich da an, um echte globale Variablen/Instanzen zu vermeiden.

Nun möchte ich so weit es möglich ist auf die Arbeit mit Zeigern verzichten und nur Referenzen nutzen.

Jede Singleton Instanz soll Zugriff auf die Singleton Instanzen der jeweils anderen Klassen bekommen. Dies wollte ich über eine übergeordnete Klasse realisieren, die die Referenzen auf alle Singleton Instanzen beinhaltet.

Dies führt nun leider dazu, daß jede von der Basisklasse abgeleitete Klasse auch eine Referenz auf sich selber hat. Und das wiederum führt leider zu unschönen Zirkel-Initialisierungen, die das Programm zum Absturz bringen (Stack Overflow).

Ist mein Konzept grundlegend falsch? Oder lässt sich diese Art der Verzirkelung irgendwie vermeiden?

Danke

Grüße,
Hendrik

7.e.Q
25-10-2006, 16:12
Da wahrscheinlich einige nicht wissen, wie Singletons unter C++ funktionieren, hier ein Beispiel:




class CSingleton
{
private:
CSingleton(); // private Constructor
~CSingleton(); // private Destructor
public:
static CSingleton& getInstance()
{
static CSingleton Instance;
return Instance;
}

void DoSomething();
};

int main()
{
CSingleton& rSingleton = CSingleton::getInstance();
rSingleton.DoSomething();
return 0;
}



Der Einfachheit halber hab ich die Definition der Members (bis auf getInstance) hier mal weggelassen, da sie unwichtig sind.

Ich möchte nun etwas wie dieses erreichen:



class CBase
{
protected:

CBase();
~CBase();

CSingletonA& SingletonA;
CSingletonB& SingletonB;
};

class CSingletonA : CBase
{
. // Deklaration siehe vorigen Code-Block
.
.
};

class CSingletonB : CBase
{
. // Deklaration siehe vorigen Code-Block
.
.
};

CBase::CBase()
: SingletonA(CSingletonA::getInstance()),
SingletonB(CSingletonB::getInstance())
{
}

void CSingletonA::DoSomething()
{
SingletonB.DoSomethingElse();
}

void CSingletonB::DoSomething()
{
SingletonA.DoSomethingElse();
}

int main()
{
CSingletonA& MySingletonA = CSingletonA::getInstance();
CSingletonB& MySingletonB = CSingletonB::getInstance();

MySingletonA.DoSomething(); // ruft intern CSingletonB::DoSomethingElse() auf
MySingletonB.DoSomething(); // ruft intern CSingletonA::DoSomethingElse() auf

return 0;
}


Dieser Code hat leider zur Folge, daß sich der Ablauf in einer Schleife um die Initialisierung von CSingletonA::SingletonA verfängt.

Wie kann man diesen Kreis verhindern? Mir scheint es, daß bei jedem static CSingletonA Instanz; der Konstruktor aufgerufen wird, der wiederum CSingletonA::getInstance(); aufruft, was wiederum static CSingletonA Instanz; aufruft und so weiter... Ist das in dieser Form der Konstruktion irgendwie zu verhindern?

Ist es irgendwie möglich, daß alle Singleton-Objekte innerhalb eines Programms zugriff auf die jeweils anderen Singleton-Objekte bekommen?

Deklariere ich die Singleton Objekte in jeder Klasse einzeln unter Ausschluss der jeweils eigenen Klasse, dann hab ich einen weiteren Zirkelbezug da drin:

CSingletonB::getInstance() im Constructor von CSingletonA erstellt eine Instanz von CSingletonB, welche in ihrem Constructor CSingletonA::getInstance() aufruft, welche durch einen erneuten Aufruf des Constructors von CSingletonA wieder CSingletonB::getInstance() aufruft. Frei nach TV Kaiser: Ein Teufelskreis.

Lässt sich das irgendwie verhindern, wenn man allen Klassen alle Singletons bekannt machen will?

anda_skoa
25-10-2006, 20:02
Wenn du die Singleton einzeln nicht brauchst, würde ich nur den Container als Singleton machen und dort die einzelnen Objekte normal als Member erzeugen.

Praktisch so


class Container
{
// singelton methoden hier

public:
inline ClassA& objectA() { return m_objectA; }
private:
ClassA m_objectA;
};


Aufruf wäre dann


ClassA& a = Container.getInstance().objectA();


Auf keinen Fall ist wie im Moment bei dir ClassA ein Container, also keine Subklasse von Container ("Is-A" Relation), sondern Container hält ClassA ("has-A" Relation)

Ciao,
_

7.e.Q
26-10-2006, 09:23
Danke!

Kann ich an der Stelle dann auch wieder mit den Initialisierungen im Constructor arbeiten? So:




ClassB::ClassB() : m_objectA( Container::getInstance().objectA )
{
}


Ich frag, bevor ich alles umbau, weil das doch 'ne ganze Menge ist, hier...


EDIT:
Okay, da das nicht geht mit Referenzen als Member der jeweils anderen Klassen, mache ich das jetzt mit Macros, so ungern ich das tu.

anda_skoa
26-10-2006, 14:25
Das sollte schon gehen. Du bekommst aus der Methode des Containers eine Referenz und in der Initialiserungsliste kann man normalerweise eine lokale Referenz initialiseren.

Edit: geht definitiv, siehe Beispiel


#include <iostream>

class Item
{
public:
Item(){}
};

class Container
{
public:
static Container& getInstance()
{
static Container instance;

return instance;
}

inline Item& item() { return m_item; }

private:
Container()
{
std::cout << "Container: m_item created at " << std::hex << &m_item << std::endl;
}

Item m_item;
};

class Foo
{
public:
Foo() : m_item(Container::getInstance().item())
{
std::cout << "Foo: m_item initialized with " << std::hex << &m_item << std::endl;
}

private:
Item& m_item;
};

int main()
{
Foo f;

return 0;
}


Ciao
_

7.e.Q
27-10-2006, 12:49
Schon, ja. Bei einer Klasse. Aber wenn ich mehrere Klassen habe, die jeweils alle anderen Klassen kennen sollen, kommt es scheinbar leider unweigerlich zu solchen Zirkeln.

anda_skoa
27-10-2006, 16:19
"Schräges" Design :D

Ciao,
_

7.e.Q
27-10-2006, 22:58
Aber es ist unheimlich praktisch, wenn man eine eierlegende Wollmilchsau programmiert. Mein Programm ist primär ein Loader für eine Pseudo-Embedded Baugruppe (im Prinzip ein vollwertiger PC, der auf eine VME-Baugruppe gequetscht wurde), auch lauffähig auf Standard-Hardware (normaler PC). Die Loader-Funktion ist seine Hauptaufgabe. Es fungiert als Server für Downloads von anderen Systemen. Über ein proprietäres Protokoll werden zuerst Datei-Informationen übertragen und danach die Datei als Byte-Stream bis zur vorher in den Infos übergebenen Dateigröße. Also alles, was auf dem Kanal zwischen 0 und Dateigröße reinkommt, wird 1:1 in die Datei auf der Platte geschrieben. Danach wird diese Datei, die wiederum ein proprietäres (in Zip verpacktes) Format enthält, entpackt und die daraus entpackte Binary (Sogenannte Ladesoftware, auch Runware genannt) ausgeführt. Sehr unsicher, eigentlich, aber da es nicht im öffentlich zugänglichen Netz eingesetzt wird, ist Sicherheit an dieser Stelle irrelevant.

Das Programm bietet zusätzlich die Möglichkeit, sich per Telnet Client (Putty o.ä.) auf eine Konsole zu verbinden, wo über einen umfangreichen Befehlssatz alle erdenklichen Informationen über die laufenden Runware-Prozesse abgefragt werden, sowie Runware-Prozesse beendet werden können. Desweiteren sind Versionsinformationen zu installierten Tools und Plugins abfragbar, sowie über die Baugruppentypen innerhalb eines VME-Racks.

Und da alle diese Komponenten irgendwie zusammenwirken, möchte ich, daß sie sich auch alle gegenseitig kennen. Und um das ganze weitestgehend absturzsicher zu gestalten, möchte ich soweit möglich auf die Verwendung von Pointern verzichten, weshalb ich deren Anzahl zur Zeit drastisch zu reduzieren versuche. Bis jetzt klappt das ganz gut.

"Schräges" Design nehm ich mal als Kompliment. :) :) :)