Quando sono utili i macro C ++?

Il preprocessore C è giustamente temuto e evitato dalla comunità C ++. Funzioni, consensi e modelli integrati sono in genere un’alternativa più sicura e superiore a un #define .

La seguente macro:

 #define SUCCEEDED(hr) ((HRESULT)(hr) >= 0) 

non è in alcun modo superiore al tipo sicuro:

 inline bool succeeded(int hr) { return hr >= 0; } 

Ma le macro hanno il loro posto, per favore elenca gli usi che trovi per le macro che non puoi fare senza il preprocessore.

Si prega di inserire ogni caso d’uso in una risposta separata in modo che possa essere votato e, se si sa come ottenere una delle risposte senza il prepropositore, indicare come nei commenti di quella risposta.

Come wrapper per le funzioni di debug, per passare automaticamente cose come __FILE__ , __LINE__ , ecc:

 #ifdef ( DEBUG ) #define M_DebugLog( msg ) std::cout << __FILE__ << ":" << __LINE__ << ": " << msg #else #define M_DebugLog( msg ) #endif 

I metodi devono sempre essere completi, codice compilabile; i macro possono essere frammenti di codice. Quindi puoi definire una macro foreach:

 #define foreach(list, index) for(index = 0; index < list.size(); index++) 

E usalo così:

 foreach(cookies, i) printf("Cookie: %s", cookies[i]); 

Dal momento che C ++ 11, questo è sostituito dal ciclo for-based .

Le protezioni dei file di intestazione richiedono macro.

Ci sono altre aree che richiedono macro? Non molti (se ce ne sono).

Ci sono altre situazioni che traggono vantaggio dalle macro? SÌ!!!

Un posto in cui utilizzo le macro è con codice molto ripetitivo. Ad esempio, quando si esegue il wrapping del codice C ++ da utilizzare con altre interfacce (.NET, COM, Python, ecc.), È necessario rilevare diversi tipi di eccezioni. Ecco come lo faccio:

 #define HANDLE_EXCEPTIONS \ catch (::mylib::exception& e) { \ throw gcnew MyDotNetLib::Exception(e); \ } \ catch (::std::exception& e) { \ throw gcnew MyDotNetLib::Exception(e, __LINE__, __FILE__); \ } \ catch (...) { \ throw gcnew MyDotNetLib::UnknownException(__LINE__, __FILE__); \ } 

Devo mettere queste prese in ogni funzione wrapper. Piuttosto che digitare ogni volta i blocchi di cattura completi, digito semplicemente:

 void Foo() { try { ::mylib::Foo() } HANDLE_EXCEPTIONS } 

Ciò facilita anche la manutenzione. Se devo aggiungere un nuovo tipo di eccezione, c’è solo un posto dove devo aggiungerlo.

Ci sono anche altri esempi utili: molti dei quali includono le macro del preprocessore __FILE__ e __LINE__ .

Comunque, i macro sono molto utili se usati correttamente. Le macro non sono malvagie – il loro cattivo uso è cattivo.

All’interno della compilazione condizionale, per superare i problemi di differenze tra i compilatori:

 #ifdef ARE_WE_ON_WIN32 #define close(parm1) _close (parm1) #define rmdir(parm1) _rmdir (parm1) #define mkdir(parm1, parm2) _mkdir (parm1) #define access(parm1, parm2) _access(parm1, parm2) #define create(parm1, parm2) _creat (parm1, parm2) #define unlink(parm1) _unlink(parm1) #endif 

Soprattutto:

  1. Includi guardie
  2. Compilazione condizionale
  3. Segnalazione (macro predefinite come __LINE__ e __FILE__ )
  4. (raramente) Duplicazione di modelli di codice ripetitivi.
  5. Nel codice del tuo concorrente.

Quando vuoi creare una stringa da un’espressione, il migliore esempio è assert ( #x trasforma il valore di x in una stringa).

 #define ASSERT_THROW(condition) \ if (!(condition)) \ throw std::exception(#condition " is false"); 

Le costanti di stringa sono talvolta meglio definite come macro poiché puoi fare di più con valori letterali stringa che con un const char * .

es. i letterali stringa possono essere facilmente concatenati .

 #define BASE_HKEY "Software\\Microsoft\\Internet Explorer\\" // Now we can concat with other literals RegOpenKey(HKEY_CURRENT_USER, BASE_HKEY "Settings", &settings); RegOpenKey(HKEY_CURRENT_USER, BASE_HKEY "TypedURLs", &URLs); 

Se è stato utilizzato un const char * per eseguire la concatenazione in fase di runtime è necessario utilizzare una sorta di class stringa:

 const char* BaseHkey = "Software\\Microsoft\\Internet Explorer\\"; RegOpenKey(HKEY_CURRENT_USER, (string(BaseHkey) + "Settings").c_str(), &settings); RegOpenKey(HKEY_CURRENT_USER, (string(BaseHkey) + "TypedURLs").c_str(), &URLs); 

Quando si desidera modificare il stream del programma ( return , break e continue ) il codice di una funzione si comporta in modo diverso rispetto al codice effettivamente inserito nella funzione.

 #define ASSERT_RETURN(condition, ret_val) \ if (!(condition)) { \ assert(false && #condition); \ return ret_val; } // should really be in a do { } while(false) but that's another discussion. 

L’ovvio include le guardie

 #ifndef MYHEADER_H #define MYHEADER_H ... #endif 

Non è ansible eseguire un cortocircuito di argomenti di chiamata di funzione utilizzando una normale chiamata di funzione. Per esempio:

 #define andm(a, b) (a) && (b) bool andf(bool a, bool b) { return a && b; } andm(x, y) // short circuits the operator so if x is false, y would not be evaluated andf(x, y) // y will always be evaluated 

Diciamo che ignoreremo le cose ovvie come le guardie di intestazione.

A volte, vuoi generare codice che deve essere copiato / incollato dal precompilatore:

 #define RAISE_ERROR_STL(p_strMessage) \ do \ { \ try \ { \ std::tstringstream strBuffer ; \ strBuffer << p_strMessage ; \ strMessage = strBuffer.str() ; \ raiseSomeAlert(__FILE__, __FUNCSIG__, __LINE__, strBuffer.str().c_str()) \ } \ catch(...){} \ { \ } \ } \ while(false) 

che ti permette di codificare questo:

 RAISE_ERROR_STL("Hello... The following values " << i << " and " << j << " are wrong") ; 

E può generare messaggi come:

 Error Raised: ==================================== File : MyFile.cpp, line 225 Function : MyFunction(int, double) Message : "Hello... The following values 23 and 12 are wrong" 

Nota che mescolare modelli con macro può portare a risultati ancora migliori (cioè generare automaticamente i valori fianco a fianco con i loro nomi di variabili)

Altre volte, è necessario il __FILE__ e / o il __LINE__ di qualche codice, ad esempio per generare informazioni di debug. Il seguente è un classico per Visual C ++:

 #define WRNG_PRIVATE_STR2(z) #z #define WRNG_PRIVATE_STR1(x) WRNG_PRIVATE_STR2(x) #define WRNG __FILE__ "("WRNG_PRIVATE_STR1(__LINE__)") : ------------ : " 

Come con il seguente codice:

 #pragma message(WRNG "Hello World") 

genera messaggi come:

 C:\my_project\my_cpp_file.cpp (225) : ------------ Hello World 

Altre volte, è necessario generare codice usando gli operatori di concatenazione # e ##, come generare getter e setter per una proprietà (questo è per casi piuttosto limitati, attraverso).

Altre volte, genererai un codice che non verrà compilato se utilizzato attraverso una funzione, ad esempio:

 #define MY_TRY try{ #define MY_CATCH } catch(...) { #define MY_END_TRY } 

Quale può essere usato come

 MY_TRY doSomethingDangerous() ; MY_CATCH tryToRecoverEvenWithoutMeaningfullInfo() ; damnThoseMacros() ; MY_END_TRY 

(ancora, ho visto solo questo tipo di codice usato correttamente una volta )

Ultimo, ma non meno importante, il famoso boost::foreach !!!

 #include  #include  #include  int main() { std::string hello( "Hello, world!" ); BOOST_FOREACH( char ch, hello ) { std::cout << ch; } return 0; } 

(Nota: copia del codice / incollato dalla homepage di boost)

Quale è (IMHO) molto meglio di std::for_each .

Quindi, le macro sono sempre utili perché sono al di fuori delle normali regole del compilatore. Ma trovo che la maggior parte delle volte che ne vedo uno, sono effettivamente dei residui di codice C mai tradotto in C ++ corretto.

I framework di test unitari per C ++ come UnitTest ++ ruotano praticamente attorno alle macro del preprocessore. Alcune righe di codice di test unitario si espandono in una gerarchia di classi che non sarebbe affatto divertente digitare manualmente. Senza qualcosa come UnitTest ++ e la sua magia per il preprocessore, non so come scrivere efficientemente test unitari per C ++.

Temere il preprocessore C è come temere le lampadine a incandescenza solo perché otteniamo lampadine fluorescenti. Sì, il primo può essere {elettricità | orario del programmatore} inefficiente. Sì, puoi ottenerlo (letteralmente) bruciato da loro. Ma possono portare a termine il lavoro se lo gestisci correttamente.

Quando si programmano i sistemi incorporati, C usa per essere l’unica opzione a parte l’assemblatore di moduli. Dopo aver programmato su desktop con C ++ e poi passati a obiettivi embedded più piccoli, impari a smettere di preoccuparti delle “inesattezze” di così tante funzionalità bare C (incluse le macro) e cerca solo di capire l’utilizzo migliore e sicuro che puoi ottenere da quelli Caratteristiche.

Alexander Stepanov dice :

Quando programmiamo in C ++ non dovremmo vergognarci della sua eredità C, ma sfruttarla appieno. Gli unici problemi con C ++, e anche gli unici problemi con C, sorgono quando essi stessi non sono coerenti con la loro stessa logica.

Utilizziamo i macro __FILE__ e __LINE__ per scopi diagnostici in termini di lancio di eccezioni, cattura e registrazione delle informazioni, insieme a scanner di file di log automatizzati nella nostra infrastruttura di QA.

Ad esempio, è ansible utilizzare una macro di lancio OUR_OWN_THROW con i parametri del tipo di eccezione e del costruttore per tale eccezione, inclusa una descrizione testuale. Come questo:

 OUR_OWN_THROW(InvalidOperationException, (L"Uninitialized foo!")); 

Naturalmente questa macro genererà l’eccezione InvalidOperationException con la descrizione come parametro costruttore, ma scriverà anche un messaggio in un file di registro che consiste del nome del file e del numero di riga in cui si è verificato il lancio e la sua descrizione testuale. L’eccezione generata riceverà un ID, che viene anche registrato. Se l’eccezione viene mai rilevata da qualche altra parte nel codice, verrà contrassegnata come tale e il file di registro indicherà quindi che quell’eccezione specifica è stata gestita e che quindi non è probabilmente la causa di eventuali arresti anomali che potrebbero essere registrati in seguito. Le eccezioni non gestite possono essere facilmente rilevate dalla nostra infrastruttura QA automatizzata.

Alcune cose molto avanzate e utili possono ancora essere costruite usando il preprocessore (macro), che non saresti mai in grado di fare usando i “costrutti del linguaggio” del c ++ inclusi i modelli.

Esempi:

Creare qualcosa sia un identificatore C che una stringa

Modo semplice per usare le variabili dei tipi enum come stringa in C

Boom Preprocessor Metaprogramming

Di tanto in tanto utilizzo le macro in modo da poter definire le informazioni in un unico posto, ma usarle in modi diversi in diverse parti del codice. È solo leggermente malvagio 🙂

Ad esempio, in “field_list.h”:

 /* * List of fields, names and values. */ FIELD(EXAMPLE1, "first example", 10) FIELD(EXAMPLE2, "second example", 96) FIELD(ANOTHER, "more stuff", 32) ... #undef FIELD 

Quindi per un enume pubblico può essere definito solo il nome:

 #define FIELD(name, desc, value) FIELD_ ## name, typedef field_ { #include "field_list.h" FIELD_MAX } field_en; 

E in una funzione di init privata, tutti i campi possono essere utilizzati per popolare una tabella con i dati:

 #define FIELD(name, desc, value) \ table[FIELD_ ## name].desc = desc; \ table[FIELD_ ## name].value = value; #include "field_list.h" 

Ripetizione del codice

Dai un’occhiata per migliorare la libreria del preprocessore , è una sorta di meta-meta-programmazione. In tema-> motivazione puoi trovare un buon esempio.

Un uso comune è per rilevare l’ambiente di compilazione, per lo sviluppo multipiattaforma è ansible scrivere un set di codice per Linux, ad esempio, e un altro per Windows quando non esiste già una libreria multipiattaforma per i propri scopi.

Quindi, in un esempio approssimativo, un mutex cross-platform può avere

 void lock() { #ifdef WIN32 EnterCriticalSection(...) #endif #ifdef POSIX pthread_mutex_lock(...) #endif } 

Per le funzioni, sono utili quando si desidera ignorare esplicitamente la sicurezza del tipo. Ad esempio i molti esempi sopra e sotto per fare ASSERT. Ovviamente, come molte delle funzionalità di C / C ++, puoi spararti ai piedi, ma il linguaggio ti offre gli strumenti e ti consente di decidere cosa fare.

Qualcosa di simile a

 void debugAssert(bool val, const char* file, int lineNumber); #define assert(x) debugAssert(x,__FILE__,__LINE__); 

In modo che tu possa, per esempio, avere

 assert(n == true); 

e ottenere il nome del file di origine e il numero di riga del problema stampato sul log se n è falso.

Se si utilizza una normale chiamata di funzione come

 void assert(bool val); 

invece della macro, tutto quello che puoi ottenere è il numero di linea della tua funzione di asserzione stampata sul log, che sarebbe meno utile.

 #define ARRAY_SIZE(arr) (sizeof arr / sizeof arr[0]) 

A differenza della soluzione di template “preferita” discussa in un thread corrente, è ansible utilizzarla come espressione costante:

 char src[23]; int dest[ARRAY_SIZE(src)]; 

È ansible utilizzare #defines per facilitare il debug e gli scenari di test unitari. Ad esempio, crea speciali varianti di registrazione delle funzioni di memoria e crea uno speciale memlog_preinclude.h:

 #define malloc memlog_malloc #define calloc memlog calloc #define free memlog_free 

Compila il tuo codice usando:

 gcc -Imemlog_preinclude.h ... 

Un link nel tuo memlog.o all’immagine finale. Ora controlli malloc, ecc., Forse per scopi di logging, o per simulare fallimenti di allocazione per i test unitari.

Uso le macro per definire facilmente le eccezioni:

 DEF_EXCEPTION(RessourceNotFound, "Ressource not found") 

dove DEF_EXCEPTION è

 #define DEF_EXCEPTION(A, B) class A : public exception\ {\ public:\ virtual const char* what() const throw()\ {\ return B;\ };\ }\ 

I compilatori possono rifiutare la tua richiesta di inline.

I macro avranno sempre il loro posto.

Qualcosa che trovo utile è #define DEBUG per il tracciamento del debug – puoi lasciarlo 1 durante il debug di un problema (o lasciarlo attivo durante l’intero ciclo di sviluppo) e poi spegnerlo quando è il momento di spedirlo.

Quando si prende una decisione in fase di compilazione su comportamento specifico di Compiler / OS / Hardware.

Ti permette di creare la tua interfaccia con le caratteristiche specifiche di Comppiler / OS / Hardware.

 #if defined(MY_OS1) && defined(MY_HARDWARE1) #define MY_ACTION(a,b,c) doSothing_OS1HW1(a,b,c);} #elif define(MY_OS1) && defined(MY_HARDWARE2) #define MY_ACTION(a,b,c) doSomthing_OS1HW2(a,b,c);} #elif define(MY_SUPER_OS) /* On this hardware it is a null operation */ #define MY_ACTION(a,b,c) #else #error "PLEASE DEFINE MY_ACTION() for this Compiler/OS/HArdware configuration" #endif 

Nel mio ultimo lavoro, stavo lavorando a uno scanner antivirus. Per rendere la cosa più facile per me fare il debug, avevo un sacco di registrazioni bloccate dappertutto, ma in un’app molto richiesta come quella, la spesa di una chiamata di funzione è troppo costosa. Quindi, mi è venuta in mente questa piccola Macro, che mi permetteva comunque di abilitare la registrazione di debug su una versione di versione sul sito di un cliente, senza il costo di una chiamata di funzione si controllerebbe il flag di debug e si tornerebbe senza loggare nulla, o se abilitato , farebbe il log … La macro è stata definita come segue:

 #define dbgmsg(_FORMAT, ...) if((debugmsg_flag & 0x00000001) || (debugmsg_flag & 0x80000000)) { log_dbgmsg(_FORMAT, __VA_ARGS__); } 

A causa del VA_ARGS nelle funzioni di registro, questo era un buon caso per una macro come questa.

Prima di ciò, ho usato una macro in un’applicazione ad alta sicurezza che aveva bisogno di dire all’utente che non avevano l’accesso corretto, e avrebbe detto loro quale flag avevano bisogno.

Le macro definite come:

 #define SECURITY_CHECK(lRequiredSecRoles) if(!DoSecurityCheck(lRequiredSecRoles, #lRequiredSecRoles, true)) return #define SECURITY_CHECK_QUIET(lRequiredSecRoles) (DoSecurityCheck(lRequiredSecRoles, #lRequiredSecRoles, false)) 

Quindi, potremmo semplicemente cospargere i controlli nell’interfaccia utente e ti dirò quali ruoli sono stati autorizzati a eseguire l’azione che hai provato a fare, se non avevi già quel ruolo. La ragione per due di loro era di restituire un valore in alcuni punti e di ritornare da una funzione di vuoto in altri …

 SECURITY_CHECK(ROLE_BUSINESS_INFORMATION_STEWARD | ROLE_WORKER_ADMINISTRATOR); LRESULT CAddPerson1::OnWizardNext() { if(m_Role.GetItemData(m_Role.GetCurSel()) == parent->ROLE_EMPLOYEE) { SECURITY_CHECK(ROLE_WORKER_ADMINISTRATOR | ROLE_BUSINESS_INFORMATION_STEWARD ) -1; } else if(m_Role.GetItemData(m_Role.GetCurSel()) == parent->ROLE_CONTINGENT) { SECURITY_CHECK(ROLE_CONTINGENT_WORKER_ADMINISTRATOR | ROLE_BUSINESS_INFORMATION_STEWARD | ROLE_WORKER_ADMINISTRATOR) -1; } ... 

Ad ogni modo, è così che li ho usati, e non sono sicuro di come questo possa essere stato aiutato con i modelli … A parte questo, cerco di evitarli, a meno che non sia VERAMENTE necessario.

Ancora un altro foreach macro. T: type, c: container, i: iterator

 #define foreach(T, c, i) for(T::iterator i=(c).begin(); i!=(c).end(); ++i) #define foreach_const(T, c, i) for(T::const_iterator i=(c).begin(); i!=(c).end(); ++i) 

Utilizzo (rappresentazione del concetto, non reale):

 void MultiplyEveryElementInList(std::list& ints, int mul) { foreach(std::list, ints, i) (*i) *= mul; } int GetSumOfList(const std::list& ints) { int ret = 0; foreach_const(std::list, ints, i) ret += *i; return ret; } 

Migliori implementazioni disponibili: Google “BOOST_FOREACH”

Buoni articoli disponibili: Conditional Love: FOREACH Redux (Eric Niebler) http://www.artima.com/cppsource/foreach.html

Forse l’uso fantastico delle macro è nello sviluppo indipendente dalla piattaforma. Pensa ai casi di incoerenza del tipo: con le macro, puoi semplicemente utilizzare diversi file di intestazione, ad esempio: –WIN_TYPES.H

 typedef ...some struct 

–POSIX_TYPES.h

 typedef ...some another struct 

–program.h

 #ifdef WIN32 #define TYPES_H "WINTYPES.H" #else #define TYPES_H "POSIX_TYPES.H" #endif #include TYPES_H 

Molto più leggibile che implementarlo in altri modi, a mio parere.

Sembra che VA_ARGS sia stato menzionato solo indirettamente finora:

Quando si scrive codice C ++ 03 generico e si ha bisogno di un numero variabile di parametri (generici), è ansible utilizzare una macro anziché un modello.

 #define CALL_RETURN_WRAPPER(FnType, FName, ...) \ if( FnType theFunction = get_op_from_name(FName) ) { \ return theFunction(__VA_ARGS__); \ } else { \ throw invalid_function_name(FName); \ } \ /**/ 

Nota: in generale, il controllo / lancio del nome potrebbe anche essere incorporato nell’ipotetica funzione get_op_from_name . Questo è solo un esempio. Potrebbe esserci un altro codice generico che circonda la chiamata VA_ARGS.

Una volta ottenuti modelli variadici con C ++ 11, possiamo risolvere questo “correttamente” con un modello.

Penso che questo trucco sia un uso intelligente del preprocessore che non può essere emulato con una funzione:

 #define COMMENT COMMENT_SLASH(/) #define COMMENT_SLASH(s) /##s #if defined _DEBUG #define DEBUG_ONLY #else #define DEBUG_ONLY COMMENT #endif 

Quindi puoi usarlo in questo modo:

 cout <<"Hello, World!" < 

È inoltre ansible definire una macro RELEASE_ONLY.

È ansible #define costanti sulla riga di comando del compilatore usando l’opzione -D o /D This is often useful when cross-compiling the same software for multiple platforms because you can have your makefiles control what constants are defined for each platform.