2009-03-09 15 views
7

Poniżej znajduje się wyciąg z książki Bjarne Stroustrup, The C++ Programming Language:Jak ograniczyć wpływ funkcji językowych zależnych od implementacji w C++?

Sekcja 4.6:

Niektóre aspekty podstawowych typów C++ 's, takich jak wielkość int są implementation- określone (§C.2). Podkreślam te zależności i często zalecam ich unikanie lub podejmowanie kroków w celu zminimalizowania ich wpływu. Dlaczego miałbyś się przejmować? Ludzie, którzy programują na różnych systemach lub korzystają z różnych kompilatorów, bardzo się przejmują, ponieważ jeśli tego nie robią, są zmuszeni tracić czas na znajdowanie i naprawianie niejasnych błędów. Ludzie, którzy twierdzą, że nie dbają o przenośność, zwykle robią to, ponieważ używają tylko jednego systemu i czują, że mogą sobie pozwolić na postawę, że "język jest tym, co mój kompilator implementuje." To jest wąski i krótkowzroczny widok. Jeśli Twój program zakończy się sukcesem, prawdopodobnie zostanie przeniesiony, więc ktoś będzie musiał znaleźć i naprawić problemy związane z funkcjami zależnymi od implementacji. Ponadto programy często muszą być kompilowane z innymi kompilatorami dla tego samego systemu, a nawet przyszłe wydanie twojego ulubionego kompilatora może zrobić coś inaczej niż bieżący. O wiele łatwiej jest poznać i ograniczyć wpływ zależności implementacyjnych, gdy program jest pisany, niż próbować rozplątać bałagan później.

Ograniczenie wpływu funkcji językowych zależnych od implementacji jest stosunkowo łatwe.

Moje pytanie brzmi: Jak ograniczyć wpływ funkcji językowych zależnych od implementacji? Wymień cechy językowe zależne od implementacji, a następnie pokaż, jak ograniczyć ich wpływ.

Odpowiedz

4

Kilka pomysłów:

  • Niestety trzeba będzie korzystać z makr, aby uniknąć pewnych szczególnych lub kompilatora konkretnych problemów platformy. Możesz zajrzeć nagłówków bibliotek Boost zobaczyć, że można go łatwo dostać uciążliwe, na przykład spojrzeć na plikach:

  • Typy całkowite wydają się być niechlujne między różnymi platformami, trzeba zdefiniować własne typy lub użyć czegoś takiego jak Boost cstdint.hpp

  • Jeśli zdecydujesz się użyć dowolnej biblioteki, a następnie wykonaj test, że biblioteka jest obsługiwana na danej platformie

  • Użyj biblioteki z dobrą obsługą i jasno udokumentowane wsparcia platformy (na przykład Boost)

  • Możesz odciągać się od pewnych problemów związanych z implementacją C++, opierając się w dużej mierze na bibliotekach takich jak Qt, które zapewniają "alternatywę" w sensie typów i algorytmów. Próbują także uczynić kodowanie w C++ bardziej przenośnym. Czy to działa? Nie jestem pewny.

  • Nie wszystko można zrobić za pomocą makr. Twój system kompilacji będzie musiał być w stanie wykryć platformę i obecność pewnych bibliotek.Wiele sugerowałoby autotools do konfiguracji projektu, ja natomiast zalecane CMake (raczej piękny język, nie więcej M4)

  • endianness i wyrównanie może być problemem, jeśli nie trochę niski poziom wtrącanie (tj reinterpret_cast i znajomych podobne rzeczy (znajomi byli złym słowem w kontekście C++)).

  • wrzuć wiele flag ostrzegawczych dla kompilatora, dla gcc polecam przynajmniej -Wall -Wextra. Ale jest o wiele więcej, zobacz dokumentację kompilatora lub tego question.

  • trzeba uważać na wszystko, co jest zależne od implementacji i zależne od implementacji. Jeśli chcesz prawdy, tylko prawdę, tylko prawdę, przejdź do normy ISO.

+0

Dobra odpowiedź. Więcej rzeczy, niż byłem tego świadomy :) – workmad3

4

Cóż, wspomniane wielkości zmiennych są dość dobrze znanym problemem, z powszechnym rozwiązaniem polegającym na dostarczaniu wersji typhesffed podstawowych typów, które mają dobrze zdefiniowane rozmiary (zwykle są anonsowane w nazwie typedef). Odbywa się to przy użyciu makr preprocesora w celu zapewnienia różnych widoczności kodu na różnych platformach. Np .:

#ifdef __WIN32__ 
typedef int int32; 
typedef char char8; 
//etc 
#endif 
#ifdef __MACOSX__ 
//different typedefs to produce same results 
#endif 

inne problemy są rozwiązywane zwykle w tym samym zbyt (to znaczy za pomocą żetonów preprocesora wykonać kompilację warunkowych)

+0

Albo możesz zaprojektować swój kod, aby nie dbał o rzeczywiste rozmiary. Może dodać twierdzenie, aby upewnić się, że są wystarczająco duże. Jeśli nie łączysz się z niskopoziomowymi sterownikami, większość kodu może być wykonana niezależnie od wielkości wymiarów. – KeithB

+0

Rozmiary nie są normalnie problemem z dobrze napisanym kodem, prawda ... ale zakresy często są. Możesz użyć asserts i rzeczy takich jak std :: numericallimits, lub możesz użyć elementów typedffed jak wyżej i mieć dobrze zdefiniowane rozmiary i zakresy dla "standardowych" typów :) – workmad3

2

Dobrym rozwiązaniem jest stosowanie wspólne nagłówki definiujące typedeff'ed Wykonania neccessary.

Na przykład, w tym sys/types.h to doskonały sposób na radzenie sobie z tym, podobnie jak korzystanie z bibliotek przenośnych.

3

Najbardziej oczywistą zależność wdrażania to wielkość typów całkowitych. Jest na to wiele sposobów. Najbardziej oczywistym sposobem jest użycie typedefs stworzyć ints różnych rozmiarach:

typedef signed short int16_t; 
typedef unsigned short uint16_t; 

Sztuką jest, aby wybrać konwencję i trzymać się go. Która konwencja jest najtrudniejsza: INT16, int16, int16_t, t_int16, Int16, itd. C99 ma plik stdint.h, który używa stylu int16_t. Jeśli twój kompilator ma ten plik, użyj go.

Podobnie powinno być pedantyczny o użyciu innych standardowych definiuje takie jak size_t, time_t itp

Drugi Sztuką jest wiedzieć, kiedy nie korzystać z tych typedef. Zmienna kontrolna pętli używana do indeksowania tablicy, powinna po prostu przyjmować surowe typy int, aby kompilacja wygenerowała najlepszy kod dla twojego procesora. for (int32_t i = 0; i < x; ++ i) może generować wiele niepotrzebnego kodu na 64-bitowym procesorze, tak jak przy użyciu int16_t na 32-bitowym procesorze.

+0

W rzeczywistości najlepszy typ do użycia do iterowania przez tablicę na dowolnym platforma, w której dostępny jest stdint.h, będzie offset_t. ten typ jest zawsze poprawnym typem dla wskaźnika arithmatic – SingleNegationElimination

+0

Wiedziałem, że był typ, ale nie był pewien co. size_t miało jakiś sens, ale nie wystarczająco dużo sensu :) Dzięki za te informacje. – jmucchiello

1

Jednym z kluczowych sposobów unikania zależności od określonych rozmiarów danych jest odczytanie trwałych danych jako tekstu, a nie binarnych. Jeśli dane binarne muszą być używane, wszystkie operacje odczytu/zapisu muszą być scentralizowane w kilku metodach i podejściach, takich jak opisane wcześniej używane tutaj typy.

Drugą rzeczą, którą możesz zrobić, to włączyć ostrzeżenia wszystkich swoich kompilatorów. na przykład użycie flagi -pedantic z g ++ ostrzeże Cię o potencjalnych problemach z przenośnością.

+0

Właściwie, modularyzacja kodu jest dobrą praktyką, nie ograniczając się do korzystania z danych binarnych. – Arafangion

+0

Oczywiście, nie chciałem sugerować inaczej, ale wiele osób posypuje odczyty i zapisy w całym kodzie. –

0

Jeśli niepokoi Cię przenośność, rzeczy takie jak rozmiar int można określić i rozwiązać bez większych trudności. Wiele kompilatorów C++ obsługuje również funkcje C99, takie jak typy int: int8_t, uint8_t, int16_t, uint32_t itd. Jeśli twój nie obsługuje ich natywnie, zawsze możesz dołączyć <cstdint> lub <sys/types.h>, który najczęściej zawiera te typedef ed. <limits.h> ma te definicje dla wszystkich podstawowych typów.

Standard gwarantuje jedynie minimalny rozmiar typu, na którym zawsze można polegać: sizeof(char) < sizeof(short) <= sizeof(int) <= sizeof(long). char musi mieć co najmniej 8 bitów. short i int musi mieć co najmniej 16 bitów. long musi mieć co najmniej 32 bity.

Inne rzeczy, które mogą być definiowane przez implementację, obejmują ABI i schematy wymuszania nazw (w szczególności zachowanie export "C++"), ale chyba że pracujesz z więcej niż jednym kompilatorem, zwykle nie jest to problem.

2

Istnieją dwa podejścia do tego:

  • definiować własne typy o znanej wielkości i używać ich zamiast wbudowanych typów (jak typedef int Int32 # IF-ED dla różnych platform)
  • wykorzystywania technik, które nie są zależne od wielkości typu

pierwszym z nich jest bardzo popularny, jednak po drugie, jeśli to możliwe, zazwyczaj prowadzi do czystszego kodu. Obejmuje to:

  • nie ponoszą wskaźnik może być rzucony na int
  • nie ponoszą wiesz rozmiar bajtów poszczególnych typów, zawsze używaj sizeof sprawdzić to
  • podczas zapisywania danych do plików lub przenosząc je w całej sieci, użyj technik, które są przenośne przy zmieniających się rozmiarach danych (jak zapisywanie/ładowanie plików tekstowych).

Jednym z ostatnich przykładów jest pisanie kodu, który można skompilować na platformach x86 i x64. Niebezpieczną częścią jest tutaj wskaźnik i size_t size - bądź przygotowany, może to być 4 lub 8 w zależności od platformy, podczas rzutowania lub różnicowania wskaźnika, rzutowania nigdy na int, użyj intptr_t i podobnych typedef-ed typów.

0

Poniżej również fragment książki Bjarne Stroustrup, The C++ Programming Language:

Sekcja 10.4.9:

Brak implementacji niezależne gwarancje są o kolejności budowy nielokalnych obiektów w różnych jednostkach kompilacji. Np

// file1.c: 
    Table tbl1; 
// file2.c: 
    Table tbl2; 

czy TBL1 jest wykonana przed TBL2 lub odwrotnie zależne od implementacji. Nie można zagwarantować, że zamówienie zostanie określone w każdej konkretnej implementacji. Dynamiczne łączenie, a nawet niewielka zmiana w procesie kompilacji, może zmienić sekwencję. Kolejność destrukcji jest podobnie zależna od implementacji.

Programista może zapewnić właściwą inicjalizację poprzez wdrożenie strategii, którą implementacje zwykle stosują dla lokalnych obiektów statycznych: po raz pierwszy.Na przykład:

class Zlib { 
    static bool initialized; 
    static void initialize() { /* initialize */ initialized = true; } 
public: 
    // no constructor 

    void f() 
    { 
     if (initialized == false) initialize(); 
     // ... 
    } 
    // ... 
}; 

Jeśli istnieje wiele funkcji, które trzeba przetestować przełącznik pierwszym razem może to być uciążliwe, ale często jest to do opanowania. Ta technika polega na tym, że statycznie przydzielone obiekty bez konstruktorów są inicjowane do . Naprawdę trudnym przypadkiem jest ten, w którym pierwsza operacja może być czasowo krytyczna, tak że narzut testowania i możliwa inicjalizacja może być poważna. W takim przypadku wymagane jest dalsze oszustwo (§ 21.5.2).

Alternatywnym podejściem do prostego przedmiotu jest przedstawienie go w funkcji (§9.4.1)

int& obj() { static int x = 0; return x; } // initialized upon first use 

przełączniki po raz pierwszy, nie obsługują każdej sytuacji. Na przykład możliwe jest tworzenie obiektów, które odnoszą się do siebie podczas budowy. Tych przykładów najlepiej unikać. Jeśli takie obiekty są niezbędne, muszą być starannie konstruowane etapami.

Powiązane problemy