Domanda

IL C il preprocessore è giustamente temuto ed evitato dalla comunità C++.Le funzioni, i const e i modelli incorporati sono generalmente un'alternativa più sicura e superiore a a #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, elenca gli usi che trovi per le macro che usi non posso fare a meno del preprocessore.

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

È stato utile?

Soluzione

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

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

Altri suggerimenti

I metodi devono sempre essere codice completo e compilabile;le macro possono essere frammenti di codice.Quindi puoi definire una macro per ogni:

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

E usarlo in questo modo:

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

Dal C++11, questo è sostituito da ciclo for basato su intervallo.

Le protezioni dei file di intestazione necessitano di macro.

Ci sono altre aree che necessitano 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 confezionamento del codice C++ da utilizzare con altre interfacce (.NET, COM, Python e così via), è 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 inserire queste catture in ogni funzione wrapper.Invece di digitare ogni volta tutti i blocchi catch, digito semplicemente:

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

Ciò facilita anche la manutenzione.Se mai dovessi aggiungere un nuovo tipo di eccezione, c'è solo un posto in cui devo aggiungerlo.

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

In ogni caso, le macro sono molto utili se usate correttamente.I macro non sono malvagi: loro abuso è malvagio.

Soprattutto:

  1. Includere le guardie
  2. Compilazione condizionale
  3. Reporting (macro predefinite come __LINE__ E __FILE__)
  4. (raramente) Duplicazione di modelli di codice ripetitivi.
  5. Nel codice del tuo concorrente.

All'interno della compilazione condizionale, per superare i problemi di differenze tra 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

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

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

Le costanti stringa a volte sono meglio definite come macro poiché puoi fare di più con le stringhe letterali che con a const char *.

per esempio.Le stringhe letterali possono essere facilmente concatenabili.

#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 un const char * fossero utilizzati, sarebbe necessario utilizzare una sorta di classe stringa per eseguire la concatenazione in fase di esecuzione:

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 flusso del programma (return, break E continue) il codice in una funzione si comporta diversamente rispetto al codice effettivamente incorporato 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 è possibile eseguire un cortocircuito degli argomenti della 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 cose ovvie come le protezioni delle intestazioni.

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 consente 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"

Tieni presente che mescolare modelli con macro può portare a risultati ancora migliori (ad es.generando automaticamente i valori affiancati ai nomi delle variabili)

Altre volte, è necessario __FILE__ e/o __LINE__ di codice, ad esempio per generare informazioni di debug.Quello che segue è 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 utilizzando gli operatori di concatenazione # e ##, come generare getter e setter per una proprietà (questo è per casi piuttosto limitati, fino a).

Altre volte, genererai codice che non verrà compilato se utilizzato tramite una funzione, come:

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

Che può essere usato come

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

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

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

#include <string>
#include <iostream>
#include <boost/foreach.hpp>

int main()
{
    std::string hello( "Hello, world!" );

    BOOST_FOREACH( char ch, hello )
    {
        std::cout << ch;
    }

    return 0;
}

(Nota:codice copiato/incollato dalla home page di Boost)

Il che è (IMHO) molto meglio di std::for_each.

Pertanto, le macro sono sempre utili perché esulano dalle normali regole del compilatore.Ma trovo che la maggior parte delle volte che ne vedo uno, sono effettivamente resti di codice C mai tradotti in C++ corretto.

Framework di test unitari per C++ come ProvaUnità++ ruotano praticamente attorno alle macro del preprocessore.Alcune righe di codice di unit test si espandono in una gerarchia di classi che non sarebbe affatto divertente digitare manualmente.Senza qualcosa come UnitTest++ e la sua magia del preprocessore, non so come scriveresti in modo efficiente unit test per C++.

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

Quando si programmano sistemi embedded, C è l'unica opzione a parte l'assemblatore.Dopo aver programmato su desktop con C++ e quindi essere passato a target più piccoli e incorporati, impari a smettere di preoccuparti delle "ineleganze" di così tante semplici funzionalità C (macro incluse) e cerchi solo di capire l'utilizzo migliore e sicuro che puoi ottenere da quelle caratteristiche.

Aleksandr Stepanov dice:

Quando programmiamo in C ++ non dovremmo vergognarci del suo patrimonio C, ma farne pieno.Gli unici problemi con C ++, e persino gli unici problemi con C, sorgono quando essi stessi non sono coerenti con la propria logica.

Noi usiamo il __FILE__ E __LINE__ macro per scopi diagnostici nel lancio, acquisizione e registrazione di eccezioni ricche di informazioni, insieme a scanner automatizzati di file di registro nella nostra infrastruttura di QA.

Ad esempio, una macro di lancio OUR_OWN_THROW potrebbe essere utilizzato con il tipo di eccezione e i parametri del costruttore per tale eccezione, inclusa una descrizione testuale.Come questo:

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

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

Ripetizione del codice.

Dai un'occhiata potenziare la libreria del preprocessore, è una sorta di meta-meta-programmazione.In argomento->motivazione puoi trovare un buon esempio.

Alcune cose molto avanzate e utili possono ancora essere create utilizzando il preprocessore (macro), cosa che non potresti mai fare utilizzando i "costrutti del linguaggio" C++, inclusi i modelli.

Esempi:

Rendere qualcosa sia un identificatore C che una stringa

Un modo semplice per utilizzare variabili di tipo enum come stringa in C

Potenzia la metaprogrammazione del preprocessore

Occasionalmente utilizzo le macro in modo da poter definire le informazioni in un unico posto, ma utilizzarle 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'enumerazione pubblica è possibile definire l'utilizzo del solo nome:

#define FIELD(name, desc, value) FIELD_ ## name,

typedef field_ {

#include "field_list.h"

    FIELD_MAX

} field_en;

E in una funzione di inizializzazione 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"

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

Quindi, in un esempio approssimativo, può avere un mutex multipiattaforma

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

Per le funzioni, sono utili quando si desidera ignorare esplicitamente l'indipendenza dai tipi.Come i numerosi esempi sopra e sotto per eseguire ASSERT.Naturalmente, come molte funzionalità C/C++ puoi darti la zappa sui piedi, ma il linguaggio ti fornisce 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 avere solo per esempio

assert(n == true);

e ottieni il nome del file di origine e il numero di riga del problema stampati nel tuo registro se n è falso.

Se usi una normale chiamata di funzione come

void assert(bool val);

invece della macro, tutto ciò che puoi ottenere è il numero di riga della funzione assert stampato nel registro, il che sarebbe meno utile.

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

A differenza della soluzione modello "preferita" discussa in un thread corrente, puoi utilizzarla come espressione costante:

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

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

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

Compila il codice utilizzando:

gcc -Imemlog_preinclude.h ...

Un collegamento nel tuo memlog.o all'immagine finale.Ora controlli malloc, ecc., magari per scopi di registrazione o per simulare errori di allocazione per test unitari.

Utilizzo 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.

Le macro avranno sempre il loro posto.

Qualcosa che trovo utile è #define DEBUG per la traccia del debug: puoi lasciarlo 1 durante il debug di un problema (o anche lasciarlo attivo durante l'intero ciclo di sviluppo) e poi disattivarlo quando è il momento della spedizione.

Quando si prende una decisione in fase di compilazione sul comportamento specifico del compilatore/sistema operativo/hardware.

Ti consente di creare la tua interfaccia per funzionalità specifiche del compilatore/sistema operativo/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 su uno scanner antivirus.Per semplificarmi il debug, avevo molti log bloccati ovunque, ma in un'app molto richiesta come questa, il costo di una chiamata di funzione è semplicemente troppo costoso.Quindi, mi è venuta in mente questa piccola macro, che mi permetteva comunque di abilitare la registrazione del debug su una versione di rilascio presso il sito di un cliente, senza il costo di una chiamata di funzione avrebbe controllato il flag di debug e sarebbe tornato senza registrare nulla, o se abilitato , eseguirei la registrazione...La macro è stata definita come segue:

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

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

Prima di ciò, utilizzavo una macro in un'applicazione ad alta sicurezza che doveva dire all'utente che non aveva l'accesso corretto e gli avrebbe detto di quale flag aveva 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 in tutta l'interfaccia utente e ti direbbe quali ruoli sono autorizzati a eseguire l'azione che hai tentato di eseguire, se non avevi già quel ruolo.La ragione per due di loro era restituire un valore in alcuni punti e restituire una funzione vuota 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 avrebbe potuto essere aiutato con i modelli...A parte questo, cerco di evitarli, a meno che non sia VERAMENTE necessario.

Ancora un altro macro foreach.T:tipo, c:contenitore, io:iteratore

#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 (concetto mostrato, non reale):

void MultiplyEveryElementInList(std::list<int>& ints, int mul)
{
    foreach(std::list<int>, ints, i)
        (*i) *= mul;
}

int GetSumOfList(const std::list<int>& ints)
{
    int ret = 0;
    foreach_const(std::list<int>, ints, i)
        ret += *i;
    return ret;
}

Migliori implementazioni disponibili:Google "BOOST_FOREACH"

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

Forse il maggiore utilizzo delle macro è nello sviluppo indipendente dalla piattaforma.Pensa ai casi di incoerenza di tipo: con le macro puoi semplicemente utilizzare file di intestazione diversi, come:--WIN_TYPES.H

typedef ...some struct

--POSIX_TYPES.h

typedef ...some another struct

--programma.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, secondo me.

Sembra che VA_ARGS sia stato menzionato solo indirettamente finora:

Quando si scrive codice C++03 generico ed è necessario un numero variabile di parametri (generici), è possibile 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 nome check/throw potrebbe essere incorporato anche nell'ipotetico get_op_from_name funzione.Questo è solo un esempio.Potrebbe esserci altro codice generico che circonda la chiamata VA_ARGS.

Una volta ottenuti i modelli variadici con C++ 11, possiamo risolverlo "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!" <<endl;
DEBUG_ONLY cout <<"This is outputed only in debug mode" <<endl;

Puoi anche definire una macro RELEASE_ONLY.

Puoi #define costanti sulla riga di comando del compilatore utilizzando il file -D O /D opzione.Ciò è spesso utile durante la compilazione incrociata dello stesso software per più piattaforme perché puoi fare in modo che i tuoi makefile controllino quali costanti sono definite per ciascuna piattaforma.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top