2009-03-09 10 views
7

Il seguente è un estratto dal libro di Bjarne Stroustrup, il C++ linguaggio di programmazione:Come limitare l'impatto delle funzionalità linguistiche dipendenti dall'implementazione in C++?

Sezione 4.6:

Alcuni degli aspetti di tipi fondamentali C++ s ', come ad esempio la dimensione di un int, sono specifica di esecuzione definito (§C.2). Sottolineo queste dipendenze e spesso raccomando di evitarle o di prendere provvedimenti per minimizzare il loro impatto. Perché dovresti preoccuparti? Le persone che programmano su una varietà di sistemi o utilizzano una varietà di compilatori si preoccupano molto perché se non lo fanno, sono costretti a perdere tempo a trovare e correggere bug oscuri. Le persone che affermano di non preoccuparsi della portabilità di solito lo fanno perché usano solo un singolo sistema e sentono di potersi permettere l'atteggiamento secondo cui "il linguaggio è ciò che il mio compilatore implementa". Questa è una visione ristretta e miope. Se il tuo programma ha successo, è probabile che venga eseguito il porting, quindi qualcuno dovrà trovare e risolvere problemi relativi alle funzionalità dipendenti dall'implementazione. Inoltre, i programmi spesso devono essere compilati con altri compilatori per lo stesso sistema, e anche una futura versione del tuo compilatore preferito potrebbe fare alcune cose in modo diverso da quello attuale. È molto più facile conoscere e limitare l'impatto delle dipendenze di implementazione quando viene scritto un programma piuttosto che tentare di sbrogliare il caos in un secondo momento.

È relativamente facile limitare l'impatto delle funzionalità linguistiche dipendenti dall'implementazione.

La mia domanda è: come limitare l'impatto delle funzionalità linguistiche dipendenti dall'implementazione? Per favore, menziona le funzionalità linguistiche dipendenti dall'implementazione, quindi mostra come limitare il loro impatto.

risposta

4

alcune idee:

  • Purtroppo si dovrà utilizzare le macro per evitare alcuni problemi specifici o specifiche del compilatore piattaforma. Potete guardare le intestazioni di librerie Boost per vedere che si può facilmente ottenere ingombrante, ad esempio esaminare i file:

  • I tipi interi tendono a essere disordinati tra piattaforme diverse, dovrai definire i tuoi typedef o usare qualcosa come Boost cstdint.hpp

  • Se si decide di utilizzare qualsiasi libreria, quindi fare un controllo che la libreria è supportata sulla piattaforma di data

  • Utilizzare le librerie con un buon supporto e il supporto della piattaforma chiaramente documentata (ad esempio Boost)

  • Puoi astrarre te stesso da alcuni problemi specifici dell'implementazione C++ facendo affidamento su librerie come Qt, che forniscono una "alternativa" in termini di tipi e algoritmi. Tentano anche di rendere la codifica in C++ più portabile. Funziona? Non ne sono sicuro.

  • Non tutto può essere eseguito con macro. Il tuo sistema di build dovrà essere in grado di rilevare la piattaforma e la presenza di determinate librerie.Molti suggerirebbe autotools per la configurazione di progetto, io d'altra parte consiglio CMake (linguaggio piuttosto bello, non di più M4)

  • endianness e l'allineamento potrebbe essere un problema se si fa qualche intromissione di basso livello (cioè reinterpret_cast e amici cose simili (gli amici erano una brutta parola in contesto C++)).

  • inserire un sacco di flag di avviso per il compilatore, per gcc, consiglierei almeno -Wall -Wextra. Ma c'è molto di più, vedere la documentazione del compilatore o questo question.

  • è necessario prestare attenzione a tutto ciò che è definito dall'implementazione e dipendente dall'implementazione. Se vuoi la verità, solo la verità, nient'altro che la verità, allora vai allo standard ISO.

+0

Bella risposta. Più roba di quanto ne fossi a conoscenza :) – workmad3

4

Bene, le dimensioni variabili citate sono un problema abbastanza noto, con la soluzione comune di fornire versioni typedeff dei tipi di base che hanno dimensioni ben definite (normalmente pubblicizzate nel nome typedef). A tale scopo, utilizzare macro di preprocessore per fornire una visibilità del codice diversa su piattaforme diverse. Es .:

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

Altri problemi sono normalmente risolti nello stesso modo troppo (cioè usando gettoni preprocessore per eseguire la compilazione condizionale)

+0

Oppure è possibile progettare il codice in modo che non importi le dimensioni effettive. Forse aggiungere un assert per assicurarsi che siano abbastanza grandi. A meno che non si stia interagendo con i driver di basso livello, è possibile creare la maggior parte del codice in modo che sia indipendente dalle dimensioni estese. – KeithB

+0

Le dimensioni non sono normalmente un problema con codice ben scritto, vero ... ma gli intervalli sono frequentemente. Potresti usare asserti e cose come std :: numeric_limits, oppure puoi usare le cose con typedeff come sopra e avere dimensioni e intervalli ben definiti per i tipi 'standard' :) – workmad3

2

Una buona soluzione è quella di utilizzare linee comuni che definiscono tipi typedeff'ed come necessario.

Ad esempio, incluso sys/types.h è un modo eccellente per gestire questo problema, così come l'utilizzo di librerie portatili.

3

La dipendenza di implementazione più ovvia è la dimensione dei tipi di interi. Ci sono molti modi per gestire questo. Il modo più ovvio è quello di utilizzare typedef per creare interi delle varie dimensioni:

typedef signed short int16_t; 
typedef unsigned short uint16_t; 

Il trucco è quello di scegliere una convenzione e bastone ad esso. Quale convenzione è la parte difficile: INT16, int16, int16_t, t_int16, Int16, ecc. C99 ha il file stdint.h che utilizza lo stile int16_t. Se il tuo compilatore ha questo file, usalo.

Allo stesso modo, si dovrebbe essere pedanti sull'utilizzo di altri standard definisce come size_t, time_t, ecc

L'altro trucco è sapere quando non utilizzare queste typedef. Una variabile di controllo del ciclo usata per indicizzare un array, dovrebbe semplicemente prendere i tipi int grezzi in modo che la compilazione generi il codice migliore per il tuo processore. for (int32_t i = 0; i < x; ++ i) potrebbe generare un sacco di codice inutile su un processore a 64-bit, proprio come usare int16_t su un processore a 32-bit.

+0

In effetti, il miglior tipo da usare per iterare attraverso un array su qualsiasi la piattaforma in cui stdint.h è disponibile sarà offset_t. quel tipo è sempre il tipo corretto per il puntatore aritmetico – SingleNegationElimination

+0

Sapevo che c'era del tipo per quello ma non ero sicuro di cosa. size_t ha un senso ma non ha abbastanza senso :) Grazie per queste informazioni. – jmucchiello

1

Uno dei modi principali per evitare la dipendenza da particolari dimensioni dei dati è leggere & scrivere dati permanenti come testo, non binari. Se i dati binari devono essere utilizzati, tutte le operazioni di lettura/scrittura devono essere centralizzate in alcuni metodi e approcci come i typedef già descritti qui utilizzati.

Un secondo ry che è possibile fare è abilitare tutti gli avvisi del compilatore. ad esempio, l'utilizzo del flag -pedantic con g ++ ti avviserà di molti potenziali problemi di portabilità.

+0

In realtà, la modularizzazione del codice è una buona pratica, non limitata all'utilizzo di dati binari. – Arafangion

+0

Ovviamente, non intendevo suggerire il contrario, ma molte persone cospargono di letture e scritture attraverso il loro codice. –

0

Se sei preoccupato per la portabilità, cose come la dimensione di un int possono essere determinate e gestite senza troppe difficoltà. Molti compilatori C++ supportano anche funzionalità C99 come i tipi int: int8_t, uint8_t, int16_t, uint32_t, ecc. Se il tuo non li supporta in modo nativo, puoi sempre includere <cstdint> o <sys/types.h>, che, il più delle volte, ha quelli typedef ed. <limits.h> ha queste definizioni per tutti i tipi di base.

Lo standard garantisce solo la dimensione minima di un tipo su cui è sempre possibile fare affidamento: sizeof(char) < sizeof(short) <= sizeof(int) <= sizeof(long). char deve essere di almeno 8 bit. short e int devono essere di almeno 16 bit. long deve essere di almeno 32 bit.

Altre cose che potrebbero essere definite dall'implementazione includono l'ABI e gli schemi di manomissione dei nomi (in particolare il comportamento di export "C++"), ma a meno che non si lavori con più di un compilatore, di solito è un non-problema.

2

Ci sono due approcci a questo:

  • definire tipi con una dimensione nota e li usa al posto di tipi built-in (come typedef int Int32 # IF-ED per diverse piattaforme)
  • usare tecniche che non dipendono dalle dimensioni tipo

la prima è molto popolare, tuttavia il secondo, quando possibile, di solito si traduce in un codice più pulito. Ciò include:

  • non date per scontato puntatore può essere gettato in int
  • non assumono si conosce la dimensione in byte dei singoli tipi, sempre utilizzare sizeof di check it
  • durante il salvataggio dei dati in file o trasferendoli attraverso la rete, utilizzare tecniche che sono portabili tra diverse dimensioni dei dati (come salvataggio/caricamento di file di testo)

Un esempio recente di questo è scrivere codice che può essere compilato per entrambe le piattaforme x86 e x64. La parte pericolosa qui è il puntatore e la dimensione size_t - sii preparato che può essere 4 o 8 a seconda della piattaforma, quando lanciare o puntatore differenziale, cast mai a int, usa invece intptr_t e tipi typedef simili.

0

Quello che segue è anche un estratto dal libro di Bjarne Stroustrup, il C++ linguaggio di programmazione:

Sezione 10.4.9:

Nessuna garanzia di implementazione indipendenti sono fatte circa l'ordine di costruzione di oggetti non locali in diverse unità di compilazione. Ad esempio:

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

Sia tbl1 è costruito prima tbl2 o viceversa è dipendente. L'ordine non è nemmeno garantito per essere fissato in ogni implementazione particolare. Il collegamento dinamico, o anche un piccolo cambiamento nel processo di compilazione, può alterare la sequenza. L'ordine di distruzione è anch'esso dipendente dall'implementazione.

Un programmatore può garantire l'inizializzazione corretta implementando la strategia che le implementazioni normalmente impiegano per oggetti statici locali: un primo cambio.Per esempio:

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

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

Se ci sono molte funzioni che hanno bisogno di testare l'interruttore per la prima volta, questo può essere noioso, ma spesso è gestibile. Questa tecnica si basa sul fatto che gli oggetti allocati staticamente senza costruttori vengono inizializzati su . Il caso veramente difficile è quello in cui la prima operazione può essere time-critical in modo che il sovraccarico dei test e la possibile inizializzazione possano essere seri. In tal caso, sono necessari ulteriori trucchetti (§21.5.2).

Un approccio alternativo per un oggetto semplice è quello di presentare come una funzione (§9.4.1):

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

interruttori prima volta non gestiscono ogni situazione. Ad esempio, è possibile creare oggetti che si riferiscono l'un l'altro durante la costruzione. Tali esempi sono meglio evitati. Se tali oggetti sono necessari, devono essere costruiti con cura per fasi.

Problemi correlati