[C++ | VS2012] Default-Konstruktor nullt Zeiger

Programmiersprachen, APIs, Bibliotheken, Open Source Engines, Debugging, Quellcode Fehler und alles was mit praktischer Programmierung zu tun hat.
Antworten
Benutzeravatar
Schrompf
Moderator
Beiträge: 5163
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas
Wohnort: Dresden
Kontaktdaten:

[C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von Schrompf »

Hallo Leute,

ich habe hier gerade ein moderat interessantes Problem, dass das Starten meines Spiels verhindert. Folgende Klasse:

Code: Alles auswählen

/// Verwaltet alle Konsolenbefehle. Konsolenbefehle können an beliebigen Stellen mittels Helfermakro angelegt werden
/// und registrieren sich im Konstruktor bei der globalen Instanz dieser Verwaltung.
class KonsoleBefehlVerwaltung {
  static bool sIstInitialisiert; // = false
  std::map<std::string, const Befehl*>* mBefehle;

  /// Standardkonstruktor
  KonsoleBefehlVerwaltung() { /* nix */ }

  void MeldeBefehlAn( const Befehl* derda)
  {
    if( !sIstInitialisiert ) { sIstInitialisiert = true; mBefehle = new std::map<std::string, const Befehl*>; }
    mBefehle->insert( std::make_pair( derda->GetName(), derda));
  }
Prinzip ist hoffentlich klar: der Konstruktor macht gar nix, die eigentliche Initialisierung passiert beim ersten Aufruf der Anmelde-Funktion. Quasi ne Art Singleton, die ich hier benutzt habe, um mir jederzeit und überall im Code Konsolenbefehle schreiben zu können - ein praktisches Debug-Instrument.

Der spannende Teil ist, dass je nach globaler Konstruktor-Reihenfolge die ersten x irgendwo angelegten Konsolenbefehle sich sauber anmelden können. Und dann läuft plötzlich der Konstruktor der Verwaltungsklasse UND NULLT DEN ZEIGER. Das schließe ich aus einem Datenbreakpoint, der in "Editor_Debug.exe!KonsoleBefehlVerwaltung::__autoclassinit(unsigned int)" hält. Was zur Hecke? Hab ich da was verpasst? Müsste der Konstruktor den Zeiger nicht einfach in Ruhe lassen? Der nächste Aufruf der Anmelde-Funktion crasht dann natürlich.

Kann jemand mit Einblick in den C++-Standard sagen, ob das hier korrektes Verhalten ist oder ob das ein Compiler-Problem ist? Mich verwundert hier dran vieles... z.B. die Tatsache, dass diese Klasse antik ist und unter GCC3.xy und aufwärts, Visual Studio 6 und aufwärts und selbst unter StormC (Bonuspunkte, wer das kennt) schon kompiliert und funktioniert hat. Der Code ist über 10 Jahre alt! Verwunderlich auch, dass der selbe Code mittels SVN:external in anderen Projekten eingebunden und mitkompiliert sauber funktioniert, nur hier nicht. Und zuletzt: warum meint der Debugger, dass der Konstruktor einen Integer als Parameter nimmt? Die Klasse hat nur den Standard-Konstruktor ohne Parameter.

Ich schreibe das jetzt um, dass es zuverlässig funktioniert. Aber ich mache mir schon Gedanken, ob hier irgendein Vorgang waltet, den ich noch nicht kenne, und die letzten Jahre Lauffähigkeit nur ein Zufall waren.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
Krishty
Establishment
Beiträge: 8350
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von Krishty »

Interessant.

Klassenobjekte vor der Konstruktion anzurühren ist undefiniertes Verhalten. So lange der K’tor nicht durchgelaufen ist, kann weißgottwas passieren, wenn du die Instanz anrührst. Dass der K’tor nichts an ihr verändert, hat nichts zu bedeuten – bevor der K’tor ausgeführt wurde existiert deine KonsoleBefehlVerwaltung-Instanz nicht und alle Befehle, die die anderen K’toren hinschicken, gehen ins Ungewisse.

Es hat die letzten zehn Jahre nur durch Glück geklappt. Es ist durchaus möglich, dass VC2012 das als Feature eingebaut hat, damit man solche Fälle erkennt und beheben muss (ähnlich wie, dass die Auswertung von Funktionsparametern absichtlich durchgewürfelt wird, damit man keinen Text verfasst, der auf einer nicht existenten Reihenfolge aufbaut).

Anders wäre das übrigens, wenn deine Instanz eine struct ohne K’tor – also POD – wäre. Dann begönne ihre Lebenszeit mit Programmbeginn inklusive garantierter Nullinitialisierung zur Kompilierzeit. Kurz: Mit einer globalen Variable hättest du das Problem nicht; es entsteht nur, weil du die überflüssige Kapselung hinzugefügt hast. Aber weil du das jetzt eh änderst erspare ich dir die Vorträge ;)
Zuletzt geändert von Krishty am 25.10.2012, 17:47, insgesamt 1-mal geändert.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Schrompf
Moderator
Beiträge: 5163
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas
Wohnort: Dresden
Kontaktdaten:

Re: [C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von Schrompf »

Danke für den Vortrag :-) Ich könnte Konstruktor (weil leer) und Destruktor (nehm ich halt das Speicherleck in Kauf) entfernen und ne struct draus machen, dann wäre die Klasse POD. Aber ob das besser ist? Dann ist der Zeiger auf die map immer zuverlässig null ab Programmstart und die Leute können mit ihren Anmeldungen kommen. Dafür hab ich dann halt ein Speicherleck, weil die Map nie wieder gelöscht wird. Und sobald ich die Map direkt in die Klasse einbaue, ist jede Art von POD und Reihenfolgen-Garantien wieder dahin.

Wie würdest Du sowas lösen? Also einen zentralen Manager, bei dem sich irgendwo definierte globale Instanzen anmelden können? Ich habe nämlich ehrlich gesagt keine Lust, die Anmeldung explizit irgendwo hinzuschreiben.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: [C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von CodingCat »

Ich verstehe gerade überhaupt nicht, mit welcher Instanz du eigentlich was aufrufst. Wieso ist sIstInitialisiert static, der Rest nicht?

Unabhängig davon erhälst du die richtige Reihenfolge ohne Leck ganz einfach folgendermaßen:

Code: Alles auswählen

/// Verwaltet alle Konsolenbefehle. Konsolenbefehle können an beliebigen Stellen mittels Helfermakro angelegt werden
/// und registrieren sich im Konstruktor bei der globalen Instanz dieser Verwaltung.
class KonsoleBefehlVerwaltung {
  std::map<std::string, const Befehl*> mBefehle;

  void MeldeBefehlAn( const Befehl* derda)
  {
    mBefehle.insert( std::make_pair( derda->GetName(), derda));
  }
};

// Natürlich auch als static Methode in der Klasse möglich
class KonsoleBefehlVerwaltung& KonsoleBefehlVerwaltung()
{
   static class KonsoleBefehlVerwaltung instanz;
   return instanz;
}

// Anmeldung:
KonsoleBefehlVerwaltung().MeldeBefehlAn(...);
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
Benutzeravatar
Krishty
Establishment
Beiträge: 8350
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von Krishty »

Dass es mit D’tor klappt, verwundert mich noch mehr. Du kannst nämlich weder sicherstellen, dass dies der letzte D’tor ist, den dein Programm beim Beenden ausführt; noch, dass kein anderer D’tor diese Instanz aufruft. Falls du in einen der anderen D’toren, die beim Programmende ausgeführt werden, einen Aufruf dieser Klasse inbaust, wette ich, dass das bereits zerstörte Objekt dadurch re-initialisiert wird und auch jetzt schon Speicher leckt.

Das Ganze ist ein C++ inhärentes Problem: Dein Logger hat Zustand; dein Programm auch. C++ erlaubt dir aber wegen der undefinierten statischen Initialisierungsreihenfolge keine Vorhersage, in welchem Zustand sich der Logger befindet, während das Programm in einem Zustand (z.B. initialisiere Speicher-Manager) ist. Es gibt nur zwei Lösungen: Entweder, du hackst die verfrühte Initialisierung direkt in die CRT; oder, du schreibst dir einen Logger, dem sein Zustand egal ist. Beides ist scheiße, wie C++.

Anders ausgedrückt: Du verlangst von deiner Klasse, dass sie jederzeit verfügbar ist. C++ kann für statische Klassen aber nur garantieren, dass sie in dem Zeitfenster der Laufzeit von main() verfügbar sind. Das geht nicht.
Zuletzt geändert von Krishty am 25.10.2012, 17:58, insgesamt 1-mal geändert.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: [C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von CodingCat »

Krishty hat geschrieben:Das Ganze ist ein C++ inhärentes Problem: Dein Logger hat Zustand; dein Programm auch. C++ erlaubt dir aber wegen der undefinierten statischen Initialisierungsreihenfolge keine Vorhersage, in welchem Zustand sich der Logger befindet, während das Programm in einem Zustand (z.B. initialisiere Speicher-Manager) ist. Es gibt nur zwei Lösungen: Entweder, du hackst die verfrühte Initialisierung direkt in die CRT; oder, du schreibst dir einen Logger, dem sein Zustand egal ist. Beides ist scheiße, wie C++.
Das stimmt so nicht. Siehe Codebeispiel in meinem Post obendrüber. Du kannst in C++ tatsächlich modulübergreifend globale Variablen mit vollkommen wohldefinierter Initialisierungs- und Konstruktionsreihenfolge definieren, indem du diese als statische lokale Variablen definierst. Damit hast du dasselbe Verhalten wie es Schrompf in seinem Eingangspost skizziert hat, nur eben korrekt und automatisch.
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
Benutzeravatar
Krishty
Establishment
Beiträge: 8350
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von Krishty »

Und wann wird die in definierter Reihenfolge angelegte Variable wieder zerstört?
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: [C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von CodingCat »

In der umgekehrten Reihenfolge natürlich.
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
Benutzeravatar
Schrompf
Moderator
Beiträge: 5163
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas
Wohnort: Dresden
Kontaktdaten:

Re: [C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von Schrompf »

Verstehe. Speicherlecks hatte ich bisher nicht. Die Konsolenbefehle, die sich da anmelden wollten, waren ebenso globale Objekte sonstwo im Code, die sich im Konstruktor bei der Zentrale melden wollen. Daher leaken die nicht, deren Destruktor muss sich auch nicht abmelden.

Aber ich glaube, ich bau es auf Standard-Singleton um, wie CodingCat vorgeschlagen hat. Initialisierung beim ersten Zugriff, der Rest ist wurscht.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
Krishty
Establishment
Beiträge: 8350
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von Krishty »

CodingCat hat geschrieben:In der umgekehrten Reihenfolge natürlich.
Die Variable wird der atexit()-Liste der CRT beigefügt. Die dort eingetragenen Variablen werden bei Programmende in umgekehrter Reihenfolge und vor den globalen Variablen zerstört. So weit, so richtig.

Wenn vor der Konsole aber eine andere Klasse initialisiert wurde, und in ihrem K’tor nicht die Konsole aufgerufen hat, in ihrem D’tor hingegen schon, wird sie auf die bereits zerstörte Konsole zugreifen.
Zuletzt geändert von Krishty am 25.10.2012, 18:03, insgesamt 1-mal geändert.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: [C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von CodingCat »

Beispiel:

Code: Alles auswählen

/// Verwaltet alle Konsolenbefehle. Konsolenbefehle können an beliebigen Stellen mittels Helfermakro angelegt werden
/// und registrieren sich im Konstruktor bei der globalen Instanz dieser Verwaltung.
class KonsoleBefehlVerwaltung {
  std::map<std::string, const Befehl*> mBefehle;

  void MeldeBefehlAn( const Befehl* derda)
  {
    mBefehle.insert( std::make_pair( derda->GetName(), derda));
  }
  void MeldeBefehlAb( const Befehl* derda)
  {
    mBefehle.erase(derda->GetName());
  }
};

// Natürlich auch als static Methode in der Klasse möglich
class KonsoleBefehlVerwaltung& KonsoleBefehlVerwaltung()
{
   static class KonsoleBefehlVerwaltung instanz;
   return instanz;
}

struct BefehlA : Befehl
{
   BefehlA()
   {
      KonsoleBefehlVerwaltung().MeldeAn(this);
   }
   ~BefehlA()
   {
      KonsoleBefehlVerwaltung().MeldeAb(this);
   }
} const BefehlAInstanz; // Meldet BefehlA bei Programmstart an und bei Programmende ab. KonsoleBefehlVerwaltungs-Instanz wird erst nach Abmeldung zerstört (umgekehrte Reihenfolge).

// Andere Module können genauso globale Befehlsinstanzen anlegen. Die Reihenfolge der Befehlsanmeldungen ist über mehrere Module nicht definiert, die Reihenfolge gegenüber der KonsoleBefehlVerwaltungs-Instanz ist jedoch immer wohldefiniert.
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
Benutzeravatar
Krishty
Establishment
Beiträge: 8350
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von Krishty »

Gegenbeispiel:

Code: Alles auswählen

class Idealist {
	int volatile * ptr;
public:

	Idealist()
		: ptr(new int)
	{ }

	void dream() {
		(*ptr) = 42;
	}

	~Idealist() {
		delete ptr;
		ptr = nullptr;
	}

};

Idealist & getIdealist() {
	static Idealist idealist;
	return idealist;
}

class Realist {
public:

	Realist() { }

	~Realist() {
		getIdealist().dream(); // the world dies with him
	}

};

Realist & getRealist() {
	static Realist realist;
	return realist;
}

int main() {

	getRealist();
	getIdealist();

	return 0;
}
Ich würde einen globalen Zeiger machen, mich auf seine Nullinitialisierung verlassen, und auf das Speicherleck scheißen.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: [C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von CodingCat »

Was machst du da für einen Quatsch? ;)

In deinem Beispiel wird zuerst der Realist erzeugt, anschließend der Idealist. Folglich wird bei Programmende zunächst der Idealist zerstört, dann der Realist. Dass das nicht funktioniert, ist vollkommen korrektes Verhalten. Eine Destruktion, die in ihren Anforderungen nicht der umgekehrten Initialisierung entspricht, ist an jeder Stelle des Programms tödlich.
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
Benutzeravatar
Krishty
Establishment
Beiträge: 8350
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: [C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von Krishty »

Das Problem ist, dass es im D’tor des Realist keine Möglichkeit gibt, festzustellen, ob der Idealist noch existiert. Das gleiche bei Schrompf: Niemand hält die anderen D’toren davon ab, beim Schließen noch schnell irgendwas mit dem Ding zu machen. Wenn man also nicht von vornherein sicherstellt, dass jeder, der das benutzt, bei seine Konstruktion einen Ticker zieht, ist so ein Ding eine tickende Zeitbombe.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
CodingCat
Establishment
Beiträge: 1857
Registriert: 02.03.2009, 21:25
Wohnort: Student @ KIT
Kontaktdaten:

Re: [C++ | VS2012] Default-Konstruktor nullt Zeiger

Beitrag von CodingCat »

Naja, die Regeln sind in diesem Fall wirklich einfach. Vieles in (und außerhalb) C++ ist eine tickende Zeitbombe; ob Objekte bei der Destruktion noch existieren, lässt sich auch sonst nirgends feststellen - das gilt insbesondere für globale Objekte. Im Übrigen ist die Abmeldung in diesem Fall nicht einmal notwendig, insofern kann bei der Destruktion eigentlich überhaupt nichts schief gehen.
alphanew.net (last updated 2011-07-02) | auf Twitter | Source Code: breeze 2 | lean C++ library | D3D Effects Lite
Antworten