C++ Klassen-/Headerproblem

DaysShadow

Admiral
Registriert
Jan. 2009
Beiträge
9.235
Hi,

ich habe zwei Klassen die sich gegenseitig kennen und jeweils Member von der anderen Klasse haben.
Irgendwas mache ich aber offensichtlich falsch, denn ich bekomme folgende Fehlermeldungen:

Code:
In file included from include/B.hpp:14:0,
                 from src/B.cpp:1:
include/A.hpp:23:17: error: 'B' in namespace 'abc::xyz' does not name a type
include/A.hpp:28:30: error: 'abc::xyz::B' has not been declared
include/A.hpp:28:37: error: expected ')' before 'ptrToB'

Zu folgendem Code:

Klasse A:

Code:
#ifndef A_HPP
#define	A_HPP

#include <tr1/memory>

#include "B.hpp"

namespace abc
{
    namespace xyz
    {
        class A
        {
            private:
                
                abc::xyz::B::Ptr ptrToB_;
            
            public:
                
                A();
                A( abc::xyz::B::Ptr ptrToB );
                ~A();
                
                typedef std::tr1::shared_ptr< A > Ptr;
        };
    }
}

#endif	/* A_HPP */

Klasse B:

Code:
#ifndef B_HPP
#define	B_HPP

#include <vector>
#include <tr1/memory>

#include "A.hpp"

namespace abc
{
    namespace xyz
    {
        class B
        {
            private:
                
                std::vector< abc::xyz::A::Ptr > aPtrs;
                
            public:
                
                B( );
                ~B( );
                
                typedef std::tr1::shared_ptr< B > Ptr;
        };
    }
}

#endif	/* B_HPP */

Das ist Problem ist scheinbar, dass B im Header von A nicht bekannt ist, obwohl ich es ja inkludiere.
Wo liegt da der Fehler?
Und ist der typedef in den jeweiligen Klassen legitim?
Hatte die beiden typedefs erst im namespace liegen, aber da gab es die selben Fehler, verständlich, da er ja von mir mit der Umschreibung nicht behoben wurde...

Es bringt mich immer wieder zum verzweifeln...^^

Danke an alle Helfer!
 
Beide Dateien verwenden Include-Guards, z.B. dieser:
Code:
#ifndef	A_HPP
#define	A_HPP

Wenn du jetzt die Datei "a.hpp" inkludierst, so wird A_HPP definiert. In "a.hpp" inkludierst du aber anschließend sofort "b.hpp", wodurch auch B_HPP definiert wird. Die inkludierte Datei "b.hpp" scheitert jetzt jedoch beim Inkludieren von "a.hpp" am Include-Guard und wird daher nicht mehr inkludiert. Dies führt dazu, dass die Klasse A zwar Klasse B kennt, umgekehrt aber nicht. Lösen kann man dieses Problem nur mit Forward-Declarations: sowohl in "a.hpp" als auch in "b.hpp" musst du die jeweils andere Datei als Forward-Declaration anlegen und die Includes entfernen.
 
Hm ok, wenn ich A so umschreibe, ist zwar B bekannt, aber B::Ptr trotzdem nicht.
Wie kann ich das regeln?
Überhaupt sinnvoll?

Ich kann natürlich die normale nicht "getypedeffte" Variante für den shared_ptr nutzen, aber wenn es doch geht, wüsste ich gerne wie.

Danke jedenfalls schonmal.

Code:
#ifndef A_HPP
#define	A_HPP

#include <tr1/memory>

// No inclusion of B
//#include "B.hpp"

namespace abc
{
    namespace xyz
    {
        class B; // forward declaration

        class A
        {
            private:
                
                abc::xyz::B::Ptr ptrToB_; // Error: B::Ptr nicht bekannt
            
            public:
                
                A();
                A( abc::xyz::B::Ptr ptrToB );
                ~A();
                
                typedef std::tr1::shared_ptr< A > Ptr;
        };
    }
}

#endif	/* A_HPP */
 
Den Typedef wirst du so wohl nicht benutzen können - da kenne ich jetzt auch keine wirkliche Lösung und wahrscheinlich gibt es auch keine.
 
Für solche Geschichten machst du normalerweise Forward declarations. Ich finde das Konstrukt von dir aber auch etwas seltsam. Warum verwendest du abc::xyz::A::Ptr als Typ anstatt std::tr1::shared_ptr< A > ? Eigentlich sollte es auch nicht nötig sein überall abc::xyz::XXX zu schreiben. Wenn du dich bereits im Namespace abc::xyz befindest ist sowieso klar welches A gemeint ist.
 
Das ist ganz "einfach" :D :

Header A (Includeguards, Includes und Namespaces dazudenken):
Code:
class B;
class A {
tr1::shared_ptr<B> b_;
};

Und andersrum natürlich dann. Header A und B dürfen sich natürlich nicht gegenseitig inkludieren, diesen Zirkelbezug willst du ja gerade loswerden. Da tr1::shared_ptr mit incomplete types umgehen kann, ist das in Butter (bei std::auto_ptr ist das nicht der Fall).

Du hast aber ein anderes Problem (wenn ich shared_ptr richtig in Erinnerung habe):
Wenn ein A und ein B gegenseitig aufeinander verweisen (bzw allgemeiner ein zirkulärer Bezug besteht), können beide Objekte nicht zerstört werden. Hierfür benutzt man dann eigentlich boost::weak_ptr (weiß garnicht, ob das im tr1 enthalten ist).

Edit sagt:

Mal fix ein Update für zirkuläre Referenzen hingeklimpert, dass zumindest mit boost prinpiziell gehen müsste (Aufwand das kompilierfährig hinzubekommen überbleibt dann Dir :)):
Code:
class B;

class A {
  weak_ptr<B> b_;
public A:
  A();
  A( shared_ptr<B> b); // : b_( b) {}
  void setB( shared_ptr<B> b); // b_ = b;
};

und das ganze gespiegelt für B.

Später dann irgendwo anders (außerhalb der beiden A und B Header):
Code:
shared_ptr<A> a( new A);
shared_ptr<B> b( new B(a));
a->setB( b);

Bedenken musst du dann aber, dass jede Benutzung von b_ innerhalb von A mit b_.lock passieren muss (und natürlich wieder gespiegelt das Ganze), ansonsten kann Dir die Referenz unter dem Hintern weggezogen werden:
Code:
void A::doSomethingWithB()
{
  /* wir benutzen das B tatsächlich,
     also brauchen wir auch eine starke Referenz! */
  shared_ptr<B> b = b_.lock();
  b->doSomethingElse();
}

Achso, es sollte dann hoffentlich selbstverständlich sein, dass Objekte von A und B ausschließlich über shared_ptr angefasst werden. Freie Pointer darf es auf keinen Fall mehr geben - genauso wenig Objekte von A und B mit automatic storage (stinknormale Stack-Objekte), sonst funktioniert das ganze nicht. Sicherstellen kann man das, indem man die Konstruktoren private macht (obiges Beispiel wieder vergessen):
Code:
class B;
class A: boost::noncopyable { // noncopyable benutzen oder cctor und operator= deaktivieren
  weak_ptr<B> b_;
  A(); // {}
  A( shared_ptr<B> b); // : b_( b) {}
public:
  static shared_ptr<A> create(); // { return new A() }
  static shared_ptr<A> create( shared_ptr<B>b); // { return new A( b) };

  void setB( shared_ptr<B> b); // { b_ = b; }
};

und dann entsprechend später:
Code:
shared_ptr<A> a = A::create();
shared_ptr<B> b = B::create( a);
a->setB( b);

Der Versuch ein Objekt mit automatic storage anzulegen, wird dann mit einem Compilefehler bestraft:
Code:
void f()
{
  A a; // error: A::A() is private
}

Und nochwas: Benutzung von b_ im Destruktor von A und andersrum ist natürlich pietätslos - man stochert nicht in Leichen rum. ;) Der weak_ptr schützt dich aber auch davor.

Noch eine hübsche Sammlung von Tipps zum Umgang mit boost::shared_ptr und Konsoren:
http://www.boost.org/doc/libs/1_46_1/libs/smart_ptr/sp_techniques.html
 
Zuletzt bearbeitet: (Kleinkrams und deutsche Sprache... ^^)
@ IceMatrix:

Da hast du Recht, im Header brauche ich natürlich nicht den gesamten Namespace angeben wenn ich sowieso schon drin bin.
Der Ptr typedef ist dann eher für die Benutzung, weil abc::xyz::xxx::Ptr immer noch kürzer ist als std::tr1::shared_ptr< abc::xyz::xxx >.
Und da alle Ptr dann jeweils shared_ptr wären, würde, zumindest bei mir, keine Verwirrung aufkommen.
Ob es Sinn ergibt, sei trotzdem dahingestellt, bin halt immer noch am Testen und Lernen.
Ich dachte halt das wäre nützlich, aber wie so oft stellt mir C++ ein Bein und lacht mich aus ;)

@ 7H3 N4C3R:

Das mit den zirkulären Referenzen hatte ich gestern beim Testen zu spüren bekommen, da ich aber die Lösung schon kannte vom vielen Lesen zwecks Fehleraufklärung(-_-), konnte ich gleich auf weak_ptr umschreiben, welcher natürlich auch im TR1 enthalten ist.
Ich habe halt einfach mit std::cout jeweils im Kon- und Destruktor eine jeweilige Meldung ausgeben lassen, sodass ich dann sah, dass die Objekte bei ausschließlicher Benutzung von shared_ptr nicht gelöscht wurden.

Dass ich nicht mit normalen Pointer rumpfusche ist mir auch bewusst, ansonsten könnte man es ja auch gleich sein lassen und ich sowieso.

Das man aber Konstruktoren private macht und statische create Methoden anlegt war mir bisher nicht bekannt, aber wenn es der Sache hilft, warum nicht.
Aber warum genau sind selbst Stack-Objekte die mit dem normalen Konstruktor erstellt werden böse, da steige ich nicht ganz hinter, vielleicht kannst du mir das nochmal kurz erläutern.

Danke auf jeden Fall für die ausführliche Antwort!
 
Zuletzt bearbeitet:
DaysShadow schrieb:
Aber warum genau sind selbst Stack-Objekte die mit dem normalen Konstruktor erstellt werden böse, da steige ich nicht ganz hinter, vielleicht kannst du mir das nochmal kurz erläutern.
Stell Dir mal folgendes vor:
Code:
shared_ptr<B> b;
A a( b);
b.set( shared_ptr<A>( &a));
Am Scope-Ende weiß b nichts von A's Zerstörung. Das A wird in jedem Fall zerstört. Mit privaten Konstruktoren und statischen create-Funktionen kann man sich vor sich selbst schützen. ;) So findet schon der Compiler Fehler in der Benutzung.

Vererbung scheidet so aber leider aus.

Genauso böse ist folgendes:
Code:
shared_ptr<A> a; delete a.get();

Dagegen kann man sich schützen, indem man den Destruktor private macht und dem shared_ptr einen deleter mit gibt. Das ist in dem Link weiter oben u.a. erläutert.
 
7H3 N4C3R, ich sehe, daß du in deinem Beispiel shared_ptr immer by value übergibst. Ich würde empfehlen, stattdessen by const reference zu übergeben, denn im Falle von shared_ptr ist unnötiges Kopieren relativ teuer, da die Implementierung einen Gewissen Synchronisationsaufwand betreiben muß, um den internen Referenzzähler thread-sicher zu vergößern bzw. zu verkleinern.

Laut boost-Dokumentation sind concurrent reads ein und desselben shared_ptrs auch sicher, weshalb du dich nicht auf 'undefiniertes' Territorium begeben würdest. ;)
 
@antred:
Stimmt, da hast du Recht, guter Hinweis. :)
 
Zuletzt bearbeitet: (Blödsinn entfernt)
Zurück
Oben