A cosa servono le macro C?

Ho scritto un po ‘di C , e posso leggerlo abbastanza bene da avere un’idea generale di quello che sta facendo, ma ogni volta che ho incontrato una macro mi ha buttato completamente. Finisco per dover ricordare che cos’è la macro e sostituirla nella mia testa mentre leggo. Quelli che ho incontrato che erano intuitivi e facili da capire erano sempre come piccole funzioni mini, quindi mi sono sempre chiesto perché non erano solo funzioni.

Posso capire la necessità di definire diversi tipi di build per le compilazioni di debug o cross platform nel preprocessore, ma la capacità di definire sostituzioni arbitrarie sembra essere utile solo per rendere ancora più difficile la comprensione di un linguaggio già difficile.

Perché è stato introdotto un preprocessore così complesso per C ? E qualcuno ha un esempio di utilizzo che mi farà capire perché sembra ancora essere usato per scopi diversi da semplici compilazioni condizionali di stile #debug?

Modificare:

Dopo aver letto un numero di risposte, continuo a non capirlo. La risposta più comune è il codice inline. Se la parola chiave inline non lo fa, o ha una buona ragione per non farlo, o l’implementazione ha bisogno di essere risolta. Non capisco perché sia ​​necessario un meccanismo completamente diverso che significa “veramente in linea questo codice” (a parte il codice che viene scritto prima che fosse in linea). Anche io non capisco l’idea che è stato detto che “se è troppo stupido per essere messo in una funzione”. Sicuramente qualsiasi parte di codice che prende un input e produce un output è meglio inserire una funzione. Penso che forse non lo capisco perché non sono abituato alle micro ottimizzazioni della scrittura C , ma il preprocessore sembra una soluzione complessa a pochi semplici problemi.

Finisco per dover ricordare che cos’è la macro e sostituirla nella mia testa mentre leggo.

Ciò sembra riflettere in modo insufficiente sulla denominazione dei macro. Suppongo che non dovresti emulare il preprocessore se fosse una macro log_function_entry() .

Quelli che ho incontrato che erano intuitivi e facili da capire erano sempre come piccole funzioni mini, quindi mi sono sempre chiesto perché non erano solo funzioni.

Di solito dovrebbero esserlo, a meno che non debbano operare su parametri generici.

 #define max(a,b) ((a)<(b)?(b):(a)) 

funzionerà su qualsiasi tipo con un < operatore.

Più che semplici funzioni, le macro consentono di eseguire operazioni utilizzando i simboli nel file sorgente. Ciò significa che è ansible creare un nuovo nome di variabile o fare riferimento al file sorgente e al numero di riga su cui si trova la macro.

In C99, le macro consentono anche di chiamare funzioni variadiche come printf

 #define log_message(guard,format,...) \ if (guard) printf("%s:%d: " format "\n", __FILE__, __LINE__,__VA_ARGS_); log_message( foo == 7, "x %d", x) 

In cui il formato funziona come printf. Se la guardia è vera, emette il messaggio insieme al file e al numero di riga che ha stampato il messaggio. Se si trattasse di una chiamata di funzione, non conoscerebbe il file e la linea da cui l'hai chiamata, e l'uso di un vaprintf sarebbe un po 'più di lavoro.

Questo estratto riassume molto la mia opinione sull’argomento, confrontando diversi modi in cui vengono utilizzate le macro C e come implementarle in D

copiato da DigitalMars.com

Quando è stata inventata la C , la tecnologia del compilatore era primitiva. L’installazione di un preprocessore di macro di testo sul front-end era un modo semplice e diretto per aggiungere molte potenti funzionalità. Le crescenti dimensioni e complessità dei programmi hanno dimostrato che queste funzionalità hanno molti problemi inerenti. D non ha un preprocessore; ma D fornisce un mezzo più scalabile per risolvere gli stessi problemi.

Macro

Le macro preprocessore aggiungono potenti funzionalità e flessibilità a C Ma hanno un rovescio della medaglia:

  • Le macro non hanno alcun concetto di ambito; sono validi dal punto di definizione fino alla fine della fonte. Esaminano i file .h, il codice annidato e così via. Quando #include decine di migliaia di righe di definizioni macro, diventa problematico evitare espansioni macro involontarie.
  • Le macro sono sconosciute al debugger. Il tentativo di eseguire il debug di un programma con dati simbolici viene compromesso dal debugger che conosce solo le espansioni di macro, non le macro stesse.
  • Le macro rendono imansible la tokenizzazione del codice sorgente, in quanto un precedente cambiamento di macro può arbitrariamente ridimensionare i token.
  • La base puramente testuale delle macro porta a un utilizzo arbitrario e incoerente, rendendo il codice che utilizza le macro incline agli errori. (Alcuni tentativi di risolvere questo problema sono stati introdotti con modelli in C++ ).
  • Le macro sono ancora utilizzate per compensare i deficit delle capacità espressive del linguaggio, come per i “wrapper” attorno ai file di intestazione.

Ecco un’enumerazione degli usi comuni per le macro e la funzione corrispondente in D:

  1. Definire costanti letterali:

    • La via del preprocessore C

       #define VALUE 5 
    • Il modo D

       const int VALUE = 5; 
  2. Creazione di un elenco di valori o contrassegni:

    • La via del preprocessore C

       int flags: #define FLAG_X 0x1 #define FLAG_Y 0x2 #define FLAG_Z 0x4 ... flags |= FLAG_X; 
    • Il modo D

       enum FLAGS { X = 0x1, Y = 0x2, Z = 0x4 }; FLAGS flags; ... flags |= FLAGS.X; 
  3. Impostazione delle convenzioni di chiamata di funzione:

    • La via del preprocessore C

       #ifndef _CRTAPI1 #define _CRTAPI1 __cdecl #endif #ifndef _CRTAPI2 #define _CRTAPI2 __cdecl #endif int _CRTAPI2 func(); 
    • Il modo D

      Le convenzioni di chiamata possono essere specificate in blocchi, quindi non è necessario cambiarle per ogni funzione:

       extern (Windows) { int onefunc(); int anotherfunc(); } 
  4. Semplice programmazione generica:

    • La via del preprocessore C

      Selezione della funzione da utilizzare in base alla sostituzione del testo:

       #ifdef UNICODE int getValueW(wchar_t *p); #define getValue getValueW #else int getValueA(char *p); #define getValue getValueA #endif 
    • Il modo D

      D consente dichiarazioni di simboli che sono alias di altri simboli:

       version (UNICODE) { int getValueW(wchar[] p); alias getValueW getValue; } else { int getValueA(char[] p); alias getValueA getValue; } 

Ci sono altri esempi sul sito Web di DigitalMars .

Sono un linguaggio di programmazione (uno più semplice) in cima a C, quindi sono utili per eseguire metaprogrammazione in tempo di compilazione … in altre parole, puoi scrivere codice macro che genera codice C in meno righe e tempo che ci vorrà scrivendolo direttamente in C.

Sono anche molto utili per scrivere espressioni “simili alla funzione” che sono “polimorfe” o “sovraccaricate”; ad esempio una macro massima definita come:

 #define max(a,b) ((a)>(b)?(a):(b)) 

è utile per qualsiasi tipo numerico; e in C non puoi scrivere:

 int max(int a, int b) {return a>b?a:b;} float max(float a, float b) {return a>b?a:b;} double max(double a, double b) {return a>b?a:b;} ... 

anche se volessi, perché non puoi sovraccaricare le funzioni.

E per non parlare della compilazione e del file condizionale inclusi (che fanno anche parte del linguaggio macro) …

Le macro consentono a qualcuno di modificare il comportamento del programma durante il tempo di compilazione. Considera questo:

  • Le costanti C consentono di correggere il comportamento del programma in fase di sviluppo
  • Le variabili C consentono di modificare il comportamento del programma al momento dell’esecuzione
  • Le macro C consentono di modificare il comportamento del programma al momento della compilazione

Al momento della compilazione significa che il codice non utilizzato non andrà nemmeno nel binario e che il processo di compilazione può modificare i valori, purché sia ​​integrato con il preprocessore macro. Esempio: make ARCH = arm (presuppone la definizione della macro di inoltro come cc -DARCH = arm)

Esempi semplici: (da glibc limits.h, definisci il più grande valore di long)

 #if __WORDSIZE == 64 #define LONG_MAX 9223372036854775807L #else #define LONG_MAX 2147483647L #endif 

Verifica (usando #define __WORDSIZE) in fase di compilazione se stiamo compilando per 32 o 64 bit. Con una toolchain multilib, l’utilizzo dei parametri -m32 e -m64 può modificare automaticamente la dimensione del bit.

(Richiesta versione POSIX)

 #define _POSIX_C_SOURCE 200809L 

Richieste durante il tempo di compilazione supporto POSIX 2008. La libreria standard può supportare molti standard (incompatibili) ma con questa definizione fornirà i prototipi di funzione corretti (esempio: getline (), no gets (), ecc.). Se la libreria non supporta lo standard, può dare un errore #error durante la compilazione, invece di crash durante l’esecuzione, ad esempio.

(percorso hardcoded)

 #ifndef LIBRARY_PATH #define LIBRARY_PATH "/usr/lib" #endif 

Definisce, durante la compilazione, una directory di hardcode. Potrebbe essere cambiato con -DLIBRARY_PATH = / home / user / lib, per esempio. Se quello fosse un const char *, come lo configureresti durante la compilazione?

(pthread.h, definizioni complesse in fase di compilazione)

 # define PTHREAD_MUTEX_INITIALIZER \ { { 0, 0, 0, 0, 0, 0, { 0, 0 } } } 

Grandi pezzi di testo che altrimenti non sarebbero semplificati possono essere dichiarati (sempre in fase di compilazione). Non è ansible farlo con funzioni o costanti (in fase di compilazione).

Per evitare di complicare davvero le cose e per evitare di suggerire stili di codifica scadenti, non fornirò un esempio di codice che compila in sistemi operativi diversi e incompatibili. Usa il tuo sistema cross-build per quello, ma dovrebbe essere chiaro che il preprocessore lo consente senza l’aiuto del sistema di compilazione, senza interrompere la compilazione a causa di interfacce assenti.

Infine, pensa all’importanza della compilazione condizionale su sistemi embedded, in cui la velocità e la memoria del processore sono limitate e i sistemi sono molto eterogenei.

Ora, se lo chiedi, è ansible sostituire tutte le definizioni di costanti macro e le chiamate di funzione con definizioni appropriate? La risposta è sì, ma non renderà semplicemente necessaria la modifica del comportamento del programma durante la compilazione. Il preprocessore sarebbe ancora richiesto.

Ricorda che le macro (e il pre-processore) provengono dai primi tempi di C. Erano l’UNICO modo di fare “funzioni” in linea (perché, ovviamente, inline è una parola chiave molto recente), e sono ancora il unico modo per forzare qualcosa da inline.

Inoltre, le macro sono l’unico modo per eseguire tali trucchi come l’inserimento di file e linee in costanti di stringa in fase di compilazione.

Al giorno d’oggi, molte delle cose che le macro erano l’unico modo di fare sono gestite meglio attraverso meccanismi più nuovi. Ma hanno ancora il loro posto, di volta in volta.

Oltre all’allineamento per l’efficienza e la compilazione condizionale, è ansible utilizzare macro per aumentare il livello di astrazione del codice C di basso livello. C non ti isola realmente dai dettagli nitidi della memoria e della gestione delle risorse e dal layout esatto dei dati, e supporta forms molto limitate di occultamento delle informazioni e altri meccanismi per la gestione di sistemi di grandi dimensioni. Con le macro, non si è più limitati a utilizzare solo i costrutti di base nel linguaggio C: è ansible definire le proprie strutture dati e costrutti di codifica (comprese classi e modelli!) Mentre si scrive ancora nominalmente C!

Le macro preprocessore offrono in realtà un linguaggio completo di Turing eseguito in fase di compilazione. Uno degli impressionanti (e un po ‘spaventosi) esempi di questo è finita sul lato C ++: la libreria Boost Preprocessor usa il preprocessore C99 / C ++ 98 per build costrutti di programmazione (relativamente) sicuri che vengono poi estesi a qualunque dichiarazione e codice sottostante inserisci, se C o C ++.

In pratica, consiglierei la programmazione del preprocessore come ultima risorsa, quando non si ha la latitudine di usare costrutti di alto livello in linguaggi più sicuri. Ma a volte è bello sapere cosa si può fare se la schiena è contro il muro e la donnola si sta chiudendo in …!

Dalla stupidità del computer :

Ho visto questo estratto di codice in molti programmi di gioco freeware per UNIX:

/ *
* Valori bit.
* /
#define BIT_0 1
#define BIT_1 2
#define BIT_2 4
#define BIT_3 8
#define BIT_4 16
#define BIT_5 32
#define BIT_6 64
#define BIT_7 128
#define BIT_8 256
#define BIT_9 512
#define BIT_10 1024
#define BIT_11 2048
#define BIT_12 4096
#define BIT_13 8192
#define BIT_14 16384
#define BIT_15 32768
#define BIT_16 65536
#define BIT_17 131072
#define BIT_18 262144
#define BIT_19 524288
#define BIT_20 1048576
#define BIT_21 2097152
#define BIT_22 4194304
#define BIT_23 8388608
#define BIT_24 16777216
#define BIT_25 33554432
#define BIT_26 67108864
#define BIT_27 134217728
#define BIT_28 268435456
#define BIT_29 536870912
#define BIT_30 1073741824
#define BIT_31 2147483648

Un modo molto più semplice per ottenere questo è:

#define BIT_0 0x00000001
#define BIT_1 0x00000002
#define BIT_2 0x00000004
#define BIT_3 0x00000008
#define BIT_4 0x00000010

#define BIT_28 0x10000000
#define BIT_29 0x20000000
#define BIT_30 0x40000000
#define BIT_31 0x80000000

Un modo più semplice è quello di lasciare che il compilatore faccia i calcoli:

#define BIT_0 (1)
#define BIT_1 (1 << 1)
#define BIT_2 (1 << 2)
#define BIT_3 (1 << 3)
#define BIT_4 (1 << 4)

#define BIT_28 (1 << 28)
#define BIT_29 (1 << 29)
#define BIT_30 (1 << 30)
#define BIT_31 (1 << 31)

Ma perché passare alla difficoltà di definire 32 costanti? Il linguaggio C ha anche macro parametrizzate. Tutto ciò di cui hai veramente bisogno è:

#define BIT (x) (1 << (x))

Ad ogni modo, mi chiedo se un tizio che ha scritto il codice originale abbia usato una calcolatrice o lo abbia appena calcolato su carta.

Questo è solo un ansible uso di Macro.

Uno dei casi in cui i macros brillano davvero è quando si crea codice con loro.

Lavoravo su un vecchio sistema C ++ che utilizzava un sistema di plugin con il proprio modo di passare parametri al plugin (usando una struttura simile a una mappa). Sono stati usati alcuni semplici macro per essere in grado di gestire questa stranezza e ci ha permesso di usare classi e classi C ++ reali con normali parametri nei plugin senza troppi problemi. Tutto il codice della colla generato da macro.

Aggiungerò a ciò che è già stato detto.

Poiché le macro funzionano sulle sostituzioni di testo, ti permettono di fare cose molto utili che non sarebbero possibili con le funzioni.

Ecco alcuni casi in cui le macro possono essere davvero utili:

 /* Get the number of elements in array 'A'. */ #define ARRAY_LENGTH(A) (sizeof(A) / sizeof(A[0])) 

Questa è una macro molto popolare e usata frequentemente. Questo è molto utile quando per esempio è necessario iterare attraverso un array.

 int main(void) { int a[] = {1, 2, 3, 4, 5}; int i; for (i = 0; i < ARRAY_LENGTH(a); ++i) { printf("a[%d] = %d\n", i, a[i]); } return 0; } 

Qui non importa se un altro programmatore aggiunge altri cinque elementi a nella decelerazione. Il for loop eseguirà sempre iterate su tutti gli elementi.

Le funzioni della libreria C per confrontare la memoria e le stringhe sono piuttosto brutte da usare.

Scrivi:

 char *str = "Hello, world!"; if (strcmp(str, "Hello, world!") == 0) { /* ... */ } 

o

 char *str = "Hello, world!"; if (!strcmp(str, "Hello, world!")) { /* ... */ } 

Per verificare se str punta su "Hello, world" . Personalmente penso che entrambe queste soluzioni siano abbastanza brutte e confuse (specialmente !strcmp(...) ).

Ecco due macro ordinate che alcune persone (incluso I) usano quando devono confrontare stringhe o memoria usando strcmp / memcmp :

 /* Compare strings */ #define STRCMP(A, o, B) (strcmp((A), (B)) o 0) /* Compare memory */ #define MEMCMP(A, o, B) (memcmp((A), (B)) o 0) 

Ora puoi scrivere il codice in questo modo:

 char *str = "Hello, world!"; if (STRCMP(str, ==, "Hello, world!")) { /* ... */ } 

Ecco l'intenzione molto più chiara!

Questi sono casi in cui le macro sono utilizzate per cose che le funzioni non possono realizzare. Le macro non devono essere utilizzate per sostituire le funzioni, ma hanno altri buoni usi.

Dati i commenti nella tua domanda, potresti non apprezzare appieno il fatto che chiamare una funzione può comportare una buona quantità di spese generali. I parametri e i registri dei tasti potrebbero dover essere copiati nello stack mentre ci si avvicina e lo stack si è svolto all’uscita. Questo era particolarmente vero per i vecchi chip Intel. Le macro consentono al programmatore di mantenere l’astrazione di una funzione (quasi), ma evitano il costoso sovraccarico di una chiamata di funzione. La parola chiave inline è di tipo consultivo, ma il compilatore potrebbe non avere sempre ragione. La gloria e il pericolo di “C” è che di solito puoi piegare il compilatore alla tua volontà.

Nel tuo pane e burro, la programmazione quotidiana dell’applicazione questo tipo di micro-ottimizzazione (evitando chiamate di funzione) è generalmente peggio che inutile, ma se stai scrivendo una funzione critica chiamata dal kernel di un sistema operativo, allora può fare un’enorme differenza.

A differenza delle funzioni normali, puoi eseguire il controllo del stream (se, mentre, per, …) nei macro. Ecco un esempio:

 #include  #define Loop(i,x) for(i=0; i 

Sfruttando la manipolazione del testo del preprocessore C si può build l’equivalente C di una struttura di dati polimorfici. Usando questa tecnica possiamo build una cassetta degli strumenti affidabile di strutture dati primitive che possono essere utilizzate in qualsiasi programma C, poiché sfruttano la syntax C e non le specifiche di una particolare implementazione.

Una spiegazione dettagliata su come utilizzare le macro per la gestione della struttura dei dati è disponibile qui: http://multi-core-dump.blogspot.com/2010/11/interesting-use-of-c-macros-polymorphic.html

È utile per integrare il codice ed evitare l’overhead delle chiamate di funzione. Così come usarlo se vuoi cambiare il comportamento in seguito senza modificare molti punti. Non è utile per cose complesse, ma per linee semplici di codice che vuoi allineare, non è male.

Le macro ti consentono di eliminare frammenti copiati, che non puoi eliminare in nessun altro modo.

Ad esempio (il codice reale, syntax del compilatore VS 2010):

 for each (auto entry in entries) { sciter::value item; item.set_item("DisplayName", entry.DisplayName); item.set_item("IsFolder", entry.IsFolder); item.set_item("IconPath", entry.IconPath); item.set_item("FilePath", entry.FilePath); item.set_item("LocalName", entry.LocalName); items.append(item); } 

Questo è il posto in cui si passa un valore di campo con lo stesso nome in un motore di script. Questo copia-incollato? Sì. DisplayName viene utilizzato come stringa per uno script e come nome di campo per il compilatore. È così male? Sì. Se ti LocalName codice e rinomina LocalName in RelativeFolderName (come ho fatto io) e dimentichi di fare lo stesso con la stringa (come ho fatto io), lo script funzionerà in un modo che non ti aspetti (in effetti, nel mio esempio dipende dal fatto che hai dimenticato di rinominare il campo in un file di script separato, ma se lo script è utilizzato per la serializzazione, sarebbe un errore del 100%).

Se usi una macro per questo, non ci sarà spazio per il bug:

 for each (auto entry in entries) { #define STR_VALUE(arg) #arg #define SET_ITEM(field) item.set_item(STR_VALUE(field), entry.field) sciter::value item; SET_ITEM(DisplayName); SET_ITEM(IsFolder); SET_ITEM(IconPath); SET_ITEM(FilePath); SET_ITEM(LocalName); #undef SET_ITEM #undef STR_VALUE items.append(item); } 

Sfortunatamente, questo apre una porta per altri tipi di bug. Puoi fare un refuso scrivendo la macro e non vedrai mai un codice viziato, perché il compilatore non mostra come appare dopo la pre-elaborazione. Qualcun altro potrebbe usare lo stesso nome (è per questo che “rilascio” macro ASAP con #undef ). Quindi, usalo saggiamente. Se vedi un altro modo di eliminare il codice incollato sulla copia (come le funzioni), usa questo metodo. Se vedi che eliminare il codice incollato con copia con i macro non vale il risultato, mantieni il codice incollato sulla copia.

Uno dei motivi ovvi è che utilizzando una macro, il codice verrà espanso in fase di compilazione e si otterrà una pseudo-funzione chiamata senza il sovraccarico della chiamata.

Altrimenti, puoi anche usarlo per le costanti simboliche, in modo da non dover modificare lo stesso valore in più punti per cambiare una piccola cosa.

Macro … per quando il tuo & # (* $ e il compilatore si rifiuta semplicemente di incorporare qualcosa.

Quello dovrebbe essere un poster motivazionale, no?

In tutta serietà, l’ abuso del preprocessore google (potresti vedere una domanda simile a SO come il risultato n. 1). Se sto scrivendo una macro che va oltre la funzionalità di assert (), di solito provo a vedere se il mio compilatore in realtà incorpori una funzione simile.

Altri discuteranno contro l’uso di #if per la compilazione condizionale … preferirebbero voi:

 if (RUNNING_ON_VALGRIND) 

piuttosto che

 #if RUNNING_ON_VALGRIND 

.. per scopi di debug, dal momento che puoi vedere if () ma non #if in un debugger. Quindi ci immergiamo in #ifdef vs #if.

Se è inferiore a 10 righe di codice, provare a inserirlo. Se non può essere in linea, prova a ottimizzarlo. Se è troppo sciocco per essere una funzione, crea una macro.

Anche se non sono un grande fan dei macro e non tendo più a scrivere molto C, basato sul mio attuale compito, qualcosa di simile (che potrebbe ovviamente avere alcuni effetti collaterali) è conveniente:

 #define MIN(X, Y) ((X) < (Y) ? (X) : (Y)) 

Ora non ho scritto niente del genere da anni, ma le "funzioni" del genere erano su tutto il codice che avevo mantenuto prima nella mia carriera. Immagino che l'espansione potrebbe essere considerata conveniente.

Non ho visto nessuno menzionare questo, per quanto riguarda funzioni come macro, ad esempio:

#define MIN(X, Y) ((X) < (Y) ? (X) : (Y))

In genere è consigliabile evitare l'uso di macro quando non è necessario, per molte ragioni, la leggibilità è la preoccupazione principale. Così:

Quando dovresti usare questi su una funzione?

Quasi mai, poiché c'è un'alternativa più leggibile che è in inline , vedere https://www.greenend.org.uk/rjk/tech/inline.html o http://www.cplusplus.com/articles/2LywvCM9/ (il il secondo link è una pagina C ++, ma il punto è applicabile ai compilatori c per quanto ne so).

Ora, la leggera differenza è che le macro sono gestite dal pre-processore e inline è gestita dal compilatore, ma al giorno d'oggi non c'è alcuna differenza pratica.

quando è opportuno utilizzarli?

Per piccole funzioni (massimo due o tre fodere). L'objective è ottenere un certo vantaggio durante il tempo di esecuzione di un programma, in quanto funzioni come macro (e funzioni inline) sono sostituzioni di codice eseguite durante la pre-elaborazione (o compilazione in caso di inline) e non sono funzioni reali che vivono nella memoria, quindi non c'è alcuna funzione chiamata overhead (maggiori dettagli nelle pagine collegate).