PDA

View Full Version : [C++] dipendenze incrociate & compilazione


PGI-Bis
27-05-2007, 16:13
Se la classe A ha un riferimento alla classe B e la classe B ha un riferimento alla classe A

//A.cpp
class A {
private:
A a;
}

//B.cpp
class B {
private:
B b;
}

di che colore deve essere il capretto da sacrificare a Bacco affinchè il compilatore (g++) gestisca l'incrocio?

PGI-Bis
27-05-2007, 16:33
Penso di aver risolto con due forward declaration... ma penso è già un termine forte.

PGI-Bis
27-05-2007, 17:27
Infatti non va :D. Aita, Aita!

71104
27-05-2007, 19:16
io suppongo che tu abbia sostanzialmente provato a fare una cosa del genere:

class A;
class B;

class A {
private:
B b;
}

class B {
private:
A a;
}


ma è chiaro che questa roba non potrebbe mai funzionare perché stai creando una struttura di dati che ne contiene un'altra, che però ne contiene un'altra ancora identica alla prima, e così via all'infinito. o più semplicemente, come ti dice il compilatore, dichiarando b all'interno di A stai usando un tipo non completamente definito, e la cosa non si può fare.

memore della tua ammirevole esperienza in Java :ave:, e a dispetto della tua modestia :D, sono prone a pensare che tu dichiarando a e b intendessi mettere dentro ciascuna di quelle classi un qualche tipo di riferimento all'oggetto dell'altra classe, e non l'oggetto stesso. puoi scegliere se usare puntatori o references, ma ti consiglio puntatori perché non sono sicuro che si possa fare coi references (i references sono delle specie di puntatori con possibilità di dangling molto ridotta, ovvero il C++ ti costringe sintatticamente ad inizializzarli e dargli valori validi, e in questo caso potresti avere difficoltà sintattiche ad inizializzarli).

quindi in sostanza io direi:

class A;
class B;

class A {
private:
B *b;
}

class B {
private:
A *a;
}


e ricordati di inizializzarli nel costruttore, ma soprattutto di liberarli quando non ti servono più visto che il C++ non "garbage-collecta" :D

PGI-Bis
27-05-2007, 20:50
:grrr: Dopo piglio i due volumi del Deitel e lo Stroustrup, ci faccio un bel falò e gli ballo intorno gridando come un selvaggio. :grrr:

71104
27-05-2007, 21:45
:|

PGI-Bis
28-05-2007, 12:26
Toglimi una curiosità perchè qui navigo veramente a vista.

Le classi A e B si conoscono a vicenda. Non posso includere in A il sorgente di B e includere il B il sorgente di A perchè il compilatore si arrotola, cosa peraltro evidente se si considera come funziona l'include.

Orbene, posso usare la dichiarazione posticipata sia in A che in B rispettivamente di B ed A. Risolve l'intoppo. De problem is: poichè a dichiarazione posticipata sembra generare un tipo temporaneo incompleto non riesco ad invocare delle funzioni membro di A o B da B o A.

Cioè (pseudo):

class A
private: A a;
public: void pippo() {}

class A;
class B
private A b;

public: void qui() {
a->pippo(); << ciccia! pippo non esiste.
}

La "soluzione" che ho adottato è stata l'inclusione del sorgente di A in B (in B #include "A.cpp"). In A permane la dichiarazione posticipata di B (in a class B; ). Funziona. Che non significa che sia giusto. Ma se io volessi usare in A una funzione membro di B? Sono punto e a capo: dovrei includere in A B.cpp ma B.cpp include A.cpp...

E' possibile aggiungere delle funzioni membro alla dichiarazione posticipata?

71104
28-05-2007, 13:18
ORRORE, non bisogna mai includere files sorgenti :D
la direttiva #include serve ad includere solamente headers; può essere usata teoricamente per includere sorgenti, ma non si deve fare. inoltre la dichiarazione della classe deve essere separata dall'implementazione, ed in tal modo hai una sorta di dichiarazione forward che include tutti i metodi, i quali però sono implementati più avanti. la dichiarazione della classe deve stare nell'header, e l'implementazione nei sorgenti .cpp. in pratica ti devono uscire fuori quattro files:

a.h:

#ifndef __A_H__
#define __A_H__

#include "b.h"

class A {
private:
B *b;

public:
void pippo();

};

#endif


b.h:

#ifndef __B_H__
#define __B_H__

#include "a.h"

class B {
private:
A *a;

public:
void pluto();

};

#endif


a.cpp:

#include "a.h"

void A::pippo() {
.
.
.
b->pluto();
}


b.cpp

#include "b.h"

void B::pluto() {
.
.
.
a->pippo();
}


alcune note:
1) gli #ifndef iniziali in ciascun header servono a proteggere dall'inclusione multipla, la quale provocherebbe la ridichiarazione dei simboli dichiarati dall'header.
2) questo esempio di codice è sintatticamente corretto, però si "arrotola" lo stesso a runtime (un metodo chiama l'altro).
3) cosa importante: quando passi la lista dei files da compilare al compilatore non passargli gli headers, ma solamente i sorgenti .cpp (ed eventualmente, se ne hai, i sorgenti .c). in questo caso dovresti richiamanre semplicemente:

gcc [opzioni varie] a.cpp b.cpp

71104
28-05-2007, 13:25
ah, un'altra cosa: copiando dal tuo sorgente ho finito per dichiarare come private i due metodi pippo e pluto, ma in realtà è ovvio che ciascuno di essi deve stare in una sezione public affinché l'altra classe possa chiamarlo :)

ora correggo il post precedente.

edit - si buonanotte, mi ero anche scordato di dichiarare a e b come puntatori :D

71104
28-05-2007, 13:34
in alternativa un'altra soluzione valida avrebbe potuto essere questa:

ab.h:

#ifndef __AB_H__
#define __AB_H__

class B; // ad A serve la dichiarazione forward, a B invece no

class A {
private:
B *b;

public:
void pippo();

};

class B {
private:
A *a;

public:
void pluto();

};

#endif


ab.cpp:

#include "ab.h"

void A::pippo() {
.
.
.
b->pluto();
}

void B::pluto() {
.
.
.
a->pippo();
}


anche questa ovviamente provoca uno stack overflow a runtime visto che i due metodi si chiamano incondizionatamente. l'esempio sintattico e semantico però spero si capisca.

PGI-Bis
28-05-2007, 14:14
Che tu sappia non c'è un compilatore che sia in grado di risolvere da solo queste beghe? Ora uso g++.

71104
28-05-2007, 19:48
Che tu sappia non c'è un compilatore che sia in grado di risolvere da solo queste beghe? Ora uso g++. non puoi essere coadiuvato a livello di compilatore per questi problemi: il fatto che tu non possa far uso di tipi non completamente definiti è parte dello standard del C++ (e anche del C se è per questo, basta traslare tutto il discorso sulle struct), così come anche il sistema degli #include assieme al suo fastidioso problema delle inclusioni multiple. se esistesse ipoteticamente un compilatore pseudo-C++ che evitasse di "arrotolarsi" (quanto mi piace sta parola :D) quando tu fai l'inclusione reciproca di due sorgenti .cpp (orrore :nonsifa: ) verrebbero meno altre regole del C++; magari anche il fatto stesso che non si possa far uso diretto di tipi incompleti. avresti realizzato un sistema di import anziché di #include, e ti ritroveresti a fronteggiare il problema di dover avvisare il programmatore quando questi cercasse di definire dei tipi che contengono se stessi all'infinito (Java risolve nel migliore dei modi questo e miliardi di altri problemi: gli oggetti vengono espressi sempre by reference e quindi non può esistere una struttura dati che ne contiene un'altra, ma solo una struttura dati che contiene il riferimento a un'altra).

71104
28-05-2007, 19:53
tutto questo parlare di C++ traviato mi ha fatto venire in mente quello che di fatto è un'implementazione di C++ traviato, ovvero il C# :D
il C#, similmente a Java, ha un sistema di packaging che si gestisce tramite import anziché #include, e gli oggetti viaggiano da una parte all'altra del programma sempre e solo per copia di riferimento (come in Java). hai anche a disposizione un garbage collector nonché l'enorme potenza della piattaforma .NET, la quale (se ricordo bene) ti mette a disposizione (se vuoi) le interfacce API Win32. in altre parole, sei proprio sicuro di dover programmare in C++? :)

PGI-Bis
28-05-2007, 20:28
Non so in C# ma in Java la dipendenza circolare tra unità di compilazione è oggetto di un esplicito disposto delle specifiche (JLS 3, 7.3). Non credo che abbia a che fare con i puntatori. Comunque sia, la faccio superchiara:

//A.cpp
class A {

private: B* b;

public: void call() {
b->call();
}
}

//B.cpp
class B {

private: A* a;

public: void call() {
a->call();
}
}

:fagiano: E' possibile compilare questa roba in C++? :fagiano:

71104
28-05-2007, 21:09
//A.cpp
class A {

private: B* b;

public: void call() {
b->call();
}
}

//B.cpp
class B {

private: A* a;

public: void call() {
a->call();
}
}

:fagiano: E' possibile compilare questa roba in C++? :fagiano: no, e un compilatore che te la fa compilare non è un compilatore C++ :Prrr:
per compilarla con le minori modifiche possibili dovresti #includere in ciascuno l'altro, e la cosa provocherebbe un'inclusione circolare dalla quale l'#ifndef ti salverebbe, ma non riuscirebbe a farti compilare con successo. ipotizza di scrivere questo:

a.cpp

#ifndef __A_CPP__
#define __A_CPP__

#include "b.cpp"

class A {
private: B* b;

public: void call() {
b->call();
}
}

#endif


b.cpp

#ifndef __B_CPP__
#define __B_CPP__

#include "a.cpp"

class B {
private: A* a;

public: void call() {
a->call();
}
}

#endif


proviamo ora ad "espandere" gli #include (è precisamente ciò che fa il compilatore). ciò che otteniamo col primo sorgente è:

a.cpp

#ifndef __A_CPP__
#define __A_CPP__

#ifndef __B_CPP__
#define __B_CPP__

#include "a.cpp" // questo non lo espando perché a questo punto __A_CPP__ risulta definita: a.cpp NON viene incluso un'altra volta

class B {
private: A* a;

public: void call() {
a->call();
}
}

#endif

class A {
private: B* b;

public: void call() {
b->call();
}
}

#endif

per b.cpp otteniamo una cosa del tutto simmetrica. ora togliamo le direttive di preprocessing:

class B {
private: A* a; // A...? che cos'e' A?

public: void call() {
a->call();
}
}

class A {
private: B* b;

public: void call() {
b->call();
}
}

questo è precisamente ciò a cui giunge il compilatore compilando a.cpp, e vale simmetricamente per quando processa b.cpp. l'errore è evidente e non ti salva nessuno: è un problema del sistema stesso degli #include, per risolvere il quale sono stati inventati gli import di Java e C#. se un compilatore ti permette di compilare questa roba dev'essere un compilatore piuttosto fantasioso, e sicuramente non C++. sorry :p

come fai allora a risolvere??
la spiegazione è al post #8: devi necessariamente separare la dichiarazione di una classe dalla sua implementazione. la dichiarazione di una classe ti permette di fatto di avere quella che concettualmente è una dichiarazione forward che definisce già tutti i prototipi dei metodi. è praticamente quello che cercavi al post #7 ;)

PGI-Bis
28-05-2007, 21:48
Benissimo. Appurato che coi cpp non è possibile compilare quella roba, passiamo agli h. Che fa rima con cacca e non è un caso.

Abusando della tua pazienza ti chiederei un ultima cosa. Mi confermi che questo:

#ifndef A_H
#define A_H
#include "B.h"

class A {
private: B *b;
public: A();
};
#endif
#ifndef B_H
#define B_H
#include "A.h"

class B {
private: A* a;
public: B();
};
#endif
#include "A.h"

A::A() { b = 0; }
#include "B.h"

B::B() { a = 0; }

non è compilabile, come autorevolmente sostenuto da quel gran fijo de 'na mign:eek:tt@ di g++

g++ *.cpp

In file included from A.h:3,
from A.cpp:1:
B.h:6: error: ISO C++ forbids declaration of `A' with no type
B.h:6: error: expected `;' before '*' token
In file included from B.h:3,
from B.cpp:1:
A.h:6: error: ISO C++ forbids declaration of `B' with no type
A.h:6: error: expected `;' before '*' token

Oppure ho dimenticato qualcosa nei file h o cpp su indicati (che ho ristretto al minimo possibile)? :fagiano:

71104
29-05-2007, 02:24
urka, grazie per avermi fatto notare che non compila: praticamente hai fatto quello che ti avevo detto al post #8, ma ho detto na cazzata perché separare la dichiarazione dall'implementazione non basta a risolvere il problema ^^
le soluzioni servono entrambe: devi sia usare gli headers sia le dichiarazioni forward, ed inoltre a questo punto non è più necessario includere nulla dagli headers, ma puoi includere direttamente tutto solo dai sorgenti. in poche parole questo dovrebbe risolvere tutto:

a.h:

#ifndef A_H
#define A_H

class B;

class A {
private:
B *b;

public:
A();
~A();

void call();

};

#endif


b.h:

#ifndef B_H
#define B_H

class A;

class B {
private:
A *a;

public:
B();
~B();

void call();

};

#endif


a.cpp:

#include "a.h"
#include "b.h"

A::A() {
b = new B();
b->call();
}

A::~A() {
delete b;
}

void A::call() {
}


b.cpp

#include "a.h"
#include "b.h"

B::B() {
a = new A();
a->call();
}

B::~B() {
delete a;
}

void B::call() {
}

71104
29-05-2007, 02:29
ok, ho provato a compilarlo da me e stavolta sintatticamente è corretto: compila ma non linka (ovviamente ci manca il main). una roba del genere comunque non la eseguire perché se istanzi una di quelle due classi inizi a chiamare i due costruttori a ruota e fai stack overflow :p

fammi sapere se hai ancora problemi.

PGI-Bis
29-05-2007, 13:47
:D Ma quanto ti voglio bene? Ma quanto? Dai, dimmelo! Su! Osa! :D

Certo che se il deitel o lo strappentroff si dedicassero un po' meno all'apologia del puntatore e un po' più alle questione concrete gioverebbe e non poco.

71104
29-05-2007, 15:29
:D Ma quanto ti voglio bene? Ma quanto? Dai, dimmelo! Su! Osa! :D il C++ ti provoca sbalzi d'umore notevoli :|
sarà che dicono che è il linguaggio più complesso :read:

ma toglimi la curiosità: perché stai programmando in C++? è per lavoro?

Certo che se il deitel o lo strappentroff si dedicassero un po' meno all'apologia del puntatore e un po' più alle questione concrete gioverebbe e non poco. vuoi dirmi che ne' nel Daytona ne' nello Struzkowzskyj viene descritto come separare la dichiarazione di una classe dall'implementazione? non ho letto quei due libri (non ho quasi mai letto libri di informatica :D), ma mi pare strano...

PGI-Bis
29-05-2007, 15:46
Non è per lavoro. Non faccio più il programmatore. E' per un programma che sto scrivendo per un amico. Devo acquisire delle immagini da uno scanner. Ovviamente, Java. Mancano le API così devo fare un ponte JNI "a manina". Pensavo di usare C++ anzichè C, tanto per cambiare un po'. Mal me ne incolse.

Sia il deitel che lo strakenvac trattano della separazione tra dichiarazione e definizione ma quanto dicono è insufficiente ad un'applicazione reale. Mi pare che lo abbiamo visto. Qui non è sufficiente dire faccio l'h, poi faccio il cpp e funziona. Col cacchio che funziona. Non solo occorre la dichiarazione del file h ma occorre anche una dichiarazione posticipata nella dichiarazione dell'intestazione.

E non è che si tratti di un caso di natura eccezionale. Tutti i sistemi orientati agli oggetti hanno delle dichiarazioni ricorsive come radice della loro gerarchia di tipi.

RaouL_BennetH
31-05-2007, 13:07
piccolo OT che non apporterà nulla alle vostre stupende argomentazioni ...

volevo solo dirvi che il modo che avete usato di stravolgere il cognome di sturmtruppen mi sta facendo ancora capovolgere dalle risate :rotfl: :sbonk:

grazie :)

/OT.