2009-12-10 11 views
10

Oczywiście wiem, że najlepszą odpowiedzią jest "nie pisanie własnego kodu między platformami, ktoś już zrobił to, czego potrzebujesz", ale robię to jako hobby/uczenie się wykonywania, a nie w żadnej opłaconej zdolności. Zasadniczo piszę niewielką aplikację konsolową w C++ i chciałbym, aby była to platforma krzyżowa, zajmująca się takimi plikami jak pliki, gniazda i wątki. OOP wydaje się być świetnym sposobem na poradzenie sobie z tym, ale tak naprawdę nie znalazłem dobrego wzorca do pisania klas, które dzielą tę samą platformę interfejsu.Wieloplatformowa wersja OOP w języku C++

Łatwe podejście to po prostu zaplanowanie meta-interfejsu, użycie go w całej reszcie programu i po prostu skompilowanie tej samej klasy z różnymi plikami w zależności od platformy, ale czuję, że musi być lepiej sposób, który jest bardziej elegancki; przynajmniej coś, co nie pomyli IntelliSense z tym, co jest, byłoby miłe.

Wziąłem spojrzeć na niektóre z mniejszych klas w źródle wxWidgets, a oni korzystają podejście, które wykorzystuje Prywatny członek gospodarstwa danych dla danej klasy, np

class Foo 
{ 
    public: 
     Foo(); 

     void Bar(); 
    private: 
     FooData data; 
}; 

Następnie można skompilować ten po prostu wybierając różne pliki implementacyjne w zależności od platformy. To podejście wydaje mi się dość nieostrożne.

Innym podejściem, które rozważałem jest pisanie interfejsu i zamiana klas, które dziedziczą z tego interfejsu w zależności od platformy. Coś takiego:

class Foo 
{ 
    public: 
     virtual ~Foo() {}; 
     virtual void Bar() = 0; 
}; 

class Win32Foo 
{ 
    public: 
     Win32Foo(); 
     ~Win32Foo(); 
     void Bar(); 
}; 

Oczywiście ten rodzaj śrub aż rzeczywista instancji, ponieważ nie wiem, których realizacja stworzyć przedmiot, ale że można obejść przy użyciu funkcji

Foo* CreateFoo(); 

i zmieniające implementację funkcji w zależności od platformy, na której pracujesz. Nie jestem też wielkim fanem tego, ponieważ nadal wydaje się niezbyt ciekawy zaśmiecanie kodu za pomocą zestawu metod tworzenia instancji (a to byłoby również niespójne z metodą tworzenia obiektów nie-platformowych).

Które z tych dwóch podejść jest lepsze? Czy istnieje lepszy sposób?

Edycja: Moje pytanie nie brzmi: "Jak napisać wieloplatformowe C++?" Przeciwnie, jest to "Jaka jest najlepsza metoda abstrakcji kodu między platformami przy użyciu klas w C++, przy jednoczesnym zachowaniu jak największej korzyści z systemu typów?"

Odpowiedz

10

zdefiniować interfejs, który przekazuje do detail połączeń:

#include "detail/foo.hpp" 

struct foo 
{ 
    void some_thing(void) 
    { 
     detail::some_thing(); 
    } 
} 

Gdzie „detal/foo.hpp” jest coś takiego:

namespace detail 
{ 
    void some_thing(void); 
} 

można by następnie wdrożyć to w detail/win32/foo.cpp lub detail/posix/foo.cpp, iw zależności od platformy dla swojej kompilacji, skompilować jedną lub drugą stronę.

Wspólny interfejs tylko przekazuje połączenia do implementacji implementacji. Jest to podobne do tego, jak działa to doładowanie. Będziesz chciał spojrzeć na boost, aby uzyskać pełne informacje.

+1

To dobry pomysł, a na pewno przyjrzę się temu, co Boost tam robi. Jestem ciekawy, czy ten wzorzec pozwala na zmianę stanu klas między implementacjami? Na przykład uchwyty pliku POSIX vs Win32 mają inny typ - jeśli pisałem otoki pliku, stanowe klasy/klasy musiałyby być inne. – ShZ

+2

Zrobiłbyś klasę 'file_handle' i zwyczajnie wypchnąłeś uchwyt w' void * '. Implementacja może przywrócić ją do tego, czego potrzebuje. Gwarantuje to bezpieczeństwo według standardu. Ta metoda działa również dobrze, jeśli dynamicznie ładujesz implementacje, ponieważ po prostu zamieniasz 'detail :: whatever' na' some_function_pointer', ładowane z biblioteki. – GManNickG

+1

+1: zawsze deleguj do systemu kompilacji, co jest specyficzne dla czasu kompilacji –

15

Aby rzeczywiście zaimplementować funkcje wieloplatformowe, które wymagają obsługi systemu operacyjnego (np. Gniazd), musisz napisać kod, który po prostu nie będzie kompilował na niektórych platformach. Oznacza to, że projekt zorientowany na obiekt będzie musiał zostać uzupełniony o dyrektywy preprocesora.

Ale ponieważ będziesz musiał użyć preprocesora i tak, to jest wątpliwe, czy coś takiego jak win32socket (na przykład), który dziedziczy z klasy bazowej socket jest nawet konieczne lub użyteczne. Funkcja OO jest ogólnie przydatna, gdy różne funkcje będą wybierane polimorficznie w czasie wykonywania. Jednak funkcjonalność między platformami jest zwykle bardziej problemem podczas kompilacji. (Np. nie ma sensu w polimorficznym wywołaniu win32socket::connect, jeśli ta funkcja nie jest nawet kompilowana w systemie Linux!) Dlatego lepiej jest po prostu utworzyć klasę socket, która jest zaimplementowana inaczej, w zależności od platformy z wykorzystaniem dyrektyw preprocesora.

+9

Nie jest koniecznie zaśmiecanie kodu warunkami preprocesora - wystarczy umieścić kod specyficzny dla platformy w osobnych plikach i dokonać wyboru w skryptach makefiles/build. –

+1

Prawo; dyrektywy preprocesora mogą być tak proste, jak dołączanie różnych plików nagłówkowych w zależności od platformy. –

+0

Absolutnie - moje pytanie dotyczyło raczej sposobu tworzenia różnych implementacji dla ogólnej klasy gniazda. Przepraszam za niejednoznaczność. – ShZ

1

Drugie podejście jest lepsze, ponieważ pozwala tworzyć funkcje członkowskie, które będą istnieć tylko na jednej z platform. Następnie możesz użyć klasy abstrakcyjnej w kodzie niezależnym od platformy i konkretnej klasy w kodzie specyficznym dla platformy.

Przykład:

class Foo 
{ 
    public: 
     virtual ~Foo() {}; 
     virtual void Bar() = 0; 
}; 
class Win32Foo : public Foo 
{ 
    public: 
     void Bar(); 
     void CloseWindow(); // Win32-specific function 
}; 

class Abc 
{ 
    public: 
     virtual ~Abc() {}; 
     virtual void Xyz() = 0; 
}; 
class Win32Abc : public Abc 
{ 
    public: 
     void Xyz() 
     { 
      // do something 
      // and Close the window 
      Win32Foo foo; 
      foo.CloseWindow(); 
     } 
}; 
2

Idealnie byłoby skoncentrować się na różnice w najniższym możliwym poziomie.
Twój kod najwyższego poziomu wywołuje Foo() i tylko Foo() musi dbać wewnętrznie o to, z którym systemem jest wywoływany.

ps. Spójrz na „podbicia” zawiera wiele rzeczy do obsługi platform sieciowych systemów plików krzyżowe itp

1

Delegowanie jak w pierwszym przykładzie lub praca GMana całkiem dobrze, szczególnie jeśli specyficzna dla platformy część będzie potrzebować wielu członków specyficznych dla platformy (np. Członków typów, które istnieją tylko w jednej platformie). Minusem jest utrzymanie dwóch klas.

Jeśli klasa będzie prostsza, bez dużej liczby członków platformy, możesz po prostu użyć wspólnej deklaracji klasy i napisać dwie implementacje w dwóch różnych plikach .cc (plus może trzecia .cc dla niezależnej platformy implementacje metod). Być może potrzebujesz #ifdefs w pliku nagłówkowym dla kilku specyficznych dla platformy członków lub członków o typach specyficznych dla platformy. Ta metoda jest bardziej hackowa, ale na początku może być łatwiejsza. Zawsze możesz przejść do delegatów, jeśli wymknie się spod kontroli.

4

Nie musisz iść na OOP, aby być platformą. Platforma krzyżowa jest bardziej paradygmatem architektury i projektowania.

Sugeruję odizolowanie całego kodu platformy od standardowego kodu, takiego jak GUI gniazd. Porównaj je na różnych platformach i napisz warstwę lub opakowanie ogólne. Użyj tej warstwy w swoim kodzie. Utwórz funkcje biblioteczne, aby zaimplementować specyficzną dla platformy implementację warstwy ogólnej.

Funkcje specyficzne dla platformy powinny znajdować się w oddzielnych bibliotekach lub plikach. Twój proces kompilacji powinien korzystać z poprawnych bibliotek dla konkretnych platform. Nie powinno być żadnych zmian w twoim "standardowym" kodzie.

1

Jak zauważyli inni, dziedziczenie to niewłaściwe narzędzie do pracy. Decyzja konkretnego typu do zastosowania w twoim przypadku jest podejmowana w czasie kompilacji, a klasyczny polimorfizm umożliwia podjęcie decyzji w czasie wykonywania (to znaczy w wyniku działania użytkownika).

2

Możesz użyć specjalizacji szablonów do oddzielania kodu dla różnych platform.

enum platform {mac, lin, w32}; 

template<platform p = mac> 
struct MyClass 
{ 
// Platform dependent stuff for whatever platform you want to build for default here... 
}; 

template<> 
struct MyClass<lin> 
{ 
// Platform dependent stuff for Linux... 
}; 

template<> 
struct MyClass<w32> 
{ 
// Platform dependent stuff for Win32... 
}; 

int main (void) 
{ 
MyClass<> formac; 
MyClass<lin> forlin 
MyClass<w32> forw32; 
return 0; 
} 
+0

Tutaj także znajdziesz ten "idiom": http://altdevblog.com/2011/06/27/platforma-abstrakcja-with-cpp-templates / – Julien