Programowanie (IDE, UML, Wzorce...), Programowanie (PHP, Java...), Techblog

Co zrobić z wzajemnymi zależnościami w C++

06 maja, 2009 o 15:56:07 Dodaj komentarz Poziom: 0 Permalink

Od zarania dziejów ludzie zastanawiali się jak ominąć problem wzajemnych zależności w C++. No może nie do końca od zarania dziejów i raczej nie wszyscy ludzie - ale to był problem "od zawsze"

Najpopularniejszym rozwiązaniem jest zadeklarowanie jednej klasy przed drugą:

  1. class A;
  2.  
  3. class B
  4. {
  5. public:
  6.   B(A *) {}
  7. };
  8.  
  9. class A
  10. {
  11.   static B b;
  12. };

Niestety rozwiązanie to ma swoje ograniczenia. Na przykład B nie może wywyołać metody z A. Inną metodą jest zdefiniowanie metod w innym miejscu - jednak może to powodować problemy jeśli na przykład zapomnimy o zdefiniowaniu funkcji:

  1. class A;
  2.  
  3. class A
  4. {
  5.   static B<A> b;
  6. public:
  7.   void doIt();
  8. };
  9.  
  10. class B
  11. {
  12. public:
  13.   B(A *);
  14.   void doSomething();
  15. };
  16.  
  17. void A::doIt()
  18. {
  19.   b.doSomething();
  20. }
  21.  
  22. inline B::B(A *a)
  23. {
  24.   if(a != NULL)
  25.     a->doit();
  26. }
  27.  
  28. inline void B::doSomething()
  29. {
  30.  
  31. }
  32.  

Jest jednak rozwiązanie działające przynajmniej na gcc 4.4. Zdefinjujmy B jako szablon:

  1.  
  2. template<typename A>
  3. class B
  4. {
  5. public:
  6.   B(A *a)
  7.   {
  8.     if(a != NULL)
  9.       a->doit();
  10.   }
  11.   void doSomething() {}
  12. };
  13.  
  14. class A
  15. {
  16.   static B<A> b;
  17. public:
  18.   void doit()
  19.   {
  20.     b.doSomething();
  21.   }
  22. };
  23.  

Pisanie za każdym razem ]]> może być traceniem cennego miejsca i może zaciemniać kod. Dlatego można zadeklarować A przed ]]>:

  1.  
  2. class A;
  3.  
  4. template<typename A = ::A>
  5. class B
  6. {
  7. public:
  8.   B(A *a)
  9.   {
  10.     if(a != NULL)
  11.       a->doit();
  12.   }
  13.   void doSomething() {}
  14. };
  15.  
  16. class A
  17. {
  18.   static B<A> b;
  19. public:
  20.   void doit()
  21.   {
  22.     b.doSomething();
  23.   }
  24. };
  25.  

Otagowano: , , ,

Komentarze do wpisu

Możesz śledzić odpowiedzi poprzez kanał RSS. Możesz dodać komentarz lub zostawić ślad (trackback) ze swojego bloga.

#

Bartosz "BTM" Szczec

Skoro działa tylko pod gcc4 to jest to średnie rozwiązanie. Nie lepiej deklaracje funkcji / klas trzymać jak Pan Kompilator przykazał w .h a definicje w .cpp? Wtedy raczej łatwo uniknąć problemu.

06 maja 2009, 16:55:13

#

mina86

Akcesory i mutatory też chcesz trzymać w .cpp? Gratuluję szybkiego programu! Ale też nie podoba mi się rozwiązanie z szablonem, szczególnie, że cała definicja szablonu musi być dostępna. Jednak nawet ukazując (moim zdaniem) jedyne sensowne rozwiązanie, w artykuł wkradło się pewne niedomówienie. Otóż, warto takie metody zadeklarować jako inline (bo zakładam, że metod, których nie deklarowalibyśmy jako inline w pliku .hpp nie ma).

06 maja 2009, 17:31:38

#

dmichael

Czasem wystarczy przemyśleć strukturę klas aby znaleźć rozwiązanie, a czasem może też pomóc kompozycja klas, np:
class A { class B { /* pierdoły klasy B */ }; /* pierdoły klasy A */ };

06 maja 2009, 17:49:22

#

kubarek

A tak spytam z ciekawości … w jakim rzeczywistym konkretnym przypadku może nastąpić taka wzajemna zależność ? To tutaj to przykłady czysto teoretyczne. Są w świecie praktycznym takie przypadki ?

06 maja 2009, 18:13:12

#

dmichael

kubarek: no właśnie się zdarzają i to w najmniej odpowiednich momentach :].

06 maja 2009, 18:34:13

#

mina86

http://github.com/mina86/p2p-chat/blob/f0f6bf214966ef0fb7a357244ac6591792d378cd/src/application.hpp

06 maja 2009, 18:45:43

#

Maciej Piechotka

Bartosz &quot;BTM&quot; Szczeciński: Nie _tylko_ tylko _przynajmniej_. Pod niczym innym *nie* *testowałem*. Dodatkowo w orginalnym problemie jedna z klas była już szablonem co powoduje że się tak nie da. Dodatkowo całość jest często wykorzystywana i wolę dać kompilatorowi wolną rękę przy optymalizacji. mina86: Nie – nie warto. Wolę dać -O2/-Os + odpowiednie flagi i nich kompilator sam sobie optymalizuje. A jak jeszcze zaczne profilować (choć nie – przy kernelu tonie tak łatwo)
dmichael, kubarek: „Pomysł” powastał przy pisaniu mikrojądra w C++0x. W niektórych specificznych przypadkach kontroler pamięci wirtualnej musi zaalokować element. Ale stos wolnych elementów o danym rozmiarze może być pusty więc trzeba zaalokować stronę pamięci, przypiąć ją do reszty i podzielić. Przypięcie jest dokonywane przez kotroler pamięci wirtualnej (UWAGA! To przypięcie może tylko zwolnić element – ale to szczegół techniczny). Więc każdy taki stos korzysta z kontrolera pamięci wirtualnej ale kontroler pamięci wirtualnej korzysta z alokatora który korzysta ze stosu. Podobnie kotroler pamięci fizycznej. Nie da się stosować kompozycji bo to IMHO byłoby złe rozwiązanie z punktu widzenia API.
http://github.com/uzytkownik/nios/blob/f53c2e022f5161147a0bafe3cc1aee6d245a9040/utils/atomic/stack.hh
http://github.com/uzytkownik/nios/blob/f53c2e022f5161147a0bafe3cc1aee6d245a9040/kiapi/kernel/memory/memory.hh

06 maja 2009, 19:05:06

#

mina86

Ah, już wiem co mi się nie zgadzało z tym brakiem inline. Chodzi bowiem nie o optymalizacje, tylko o to, że bez inline kompilator jest zobowiązany wygenerować kod i symbol. Wynika z tego, iż każda jednostka translacji dołączający tak skonstruowany plik nagłówkowy będzie posiadała własną definicję danych metod. W rezultacie, linkowanie się nie powiedzie:

$ cat header.hpp #include <iostream>

struct Bar;

struct Foo { static Foo *create(); void doFoo() { std::cout << „Foo!\n”; } void doBar(); Bar *bar;
};

struct Bar { static Bar *create(); void doFoo(); void doBar() { std::cout << „Bar!\n”; } Foo *foo;
};

void Foo::doBar() { bar->doBar(); }
void Bar::doFoo() { foo->doFoo(); }
$ cat foo.cpp #include „header.hpp”
Foo *Foo::create() { return new Foo(); }
$ cat bar.cpp #include „header.hpp”
Bar *Bar::create() { return new Bar(); }
$ cat main.cpp #include „header.hpp”

int main(void) { Foo *foo = Foo::create(); Bar *bar = Bar::create();

foo->bar = bar; bar->foo = foo;

foo->doBar(); bar->doFoo();

delete foo; delete bar; return 0;
}
$ make main.o foo.o bar.o
g++ -c -o main.o main.cpp
g++ -c -o foo.o foo.cpp
g++ -c -o bar.o bar.cpp
$ g++ -o main main.o foo.o bar.o
foo.o: In function `Bar::doFoo()’:
foo.cpp:(.text+0×106): multiple definition of `Bar::doFoo()’
main.o:main.cpp:(.text+0×106): first defined here
foo.o: In function `Foo::doBar()’:
foo.cpp:(.text+0×120): multiple definition of `Foo::doBar()’
main.o:main.cpp:(.text+0×120): first defined here
bar.o: In function `Bar::doFoo()’:
bar.cpp:(.text+0×106): multiple definition of `Bar::doFoo()’
main.o:main.cpp:(.text+0×106): first defined here
bar.o: In function `Foo::doBar()’:
bar.cpp:(.text+0×120): multiple definition of `Foo::doBar()’
main.o:main.cpp:(.text+0×120): first defined here
collect2: ld returned 1 exit status

06 maja 2009, 19:21:50

#

Maciej Piechotka

W takim razie przepraszam.Zaraz poprawię.

06 maja 2009, 19:27:23

#

Paweł Dziepak

Nie ma potrzeby oznaczania funkcji zdefiniowanych wewnątrz definicji klasy jako inline. To jest w tym przypadku domyślne.

06 maja 2009, 21:25:10

#

mina86

Chodzi o funkcje zdefiniowane poza klasą.

06 maja 2009, 21:26:21

#

Paweł Dziepak

Tak, ale z tego co widzę to Maciek się trochę zagalopował z inline :D

06 maja 2009, 21:27:32

#

Paweł Dziepak

Tak teraz myślę, skoro jedyną rzeczą która nie pozwala umieścić definicji metod w pliku cpp jest kwestia wydajności, to można to rozwiązać bez posiłkowania się szablonami.
Wystarczy że definicję metod zachowamy w pliku hpp, ale wyrzucimy poza definicję klasy (zaznaczając, że są inline). Wtedy można umieścić definicję obu nieszczęsnych klas przed implementacją ich metod (zarówno tych inline jak i „zwykłych”) i wszyscy będą zadowoleni.
Może nie jest to rozwiązanie jakoś znacząco lepsze od tutaj zaprezentowanego, ale jakoś mi się bardziej coś takiego podoba niż zabawa z szablonami.

06 maja 2009, 21:38:58

#

Maciej Piechotka

Macie rację. Coś mi się nie zgadzało ale po dłuższej przerwie z C++ jestem czasem trochę niepewny i może zbyt ‘łatwowierny’ (coś mi się kręciło po głowie że w klasach są metody inline) – dlatego się zagalopowałem.

06 maja 2009, 21:45:01

#

mina86

Tylko, że to mogą być stosunkowo duże klasy (w sensie „posiadające
skomplikowane funkcje”) i trudno wymagać, aby pełną implementację
umieszczać w nagłówku, szczególnie, że plik może być dołączany przez
większość plików projektu i wówczas zmiany w implementacji (która
powinna być ukryta) spowodują rekompilację wszystkiego.

Co więcej, nie ma to najmniejszego sensu, skoro metody, których nie
zamierzamy „inline’ować” nie stanowią dla nas kłopotu. Problem
stanowią jedynie metody zdefiniowane wewnątrz klasy, które odwołują
się do jeszcze nie zdefiniowanego typu — takie należy zadeklarować
jako inline, a ich definicje przerzucić na koniec pliku.

06 maja 2009, 21:56:52

#

Paweł Dziepak

mina86: Właśnie o coś takiego mi chodziło, tylko najwyraźniej zbyt zagmatwanie to opisałem ;)
Domyślam się że w błąd wprowadził Cię fragment „klas przed implementacją ich metod (zarówno tych inline jak i ‘zwykłych’)”, ale zwracam uwagę, że nigdzie nie napisałem, że metody „nie-inline” też umieszczamy w hpp. W sumie to ich ta kwestia kompletnie nie dotyczy bo można sobie z nimi łatwo poradzić.
Tylko funkcje wstawiane w miejscu wykonania stanowią pewnego rodzaju komplikację, którą można rozwiązać przez umieszczenie ich poza definicją klasy (ale w pliku nagłówkowym) już po definicji wszelkich wymaganych zależności.
Chodzi mi o to, że nie widzę powodu aby dodatkowo utrudniać sobie życie skoro można to zrobić w IMO prostszy sposób.

06 maja 2009, 22:02:36

Dodaj komentarz

Textile Lite włączony ( szczegółowy opis znaczników ):