Pregunta

El C El preprocesador es justificadamente temido y rechazado por la comunidad C++.Las funciones, constantes y plantillas integradas suelen ser una alternativa superior y más segura a una #define.

La siguiente macro:

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

no es de ninguna manera superior al tipo seguro:

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

Pero las macros tienen su lugar. Enumere los usos que encuentra para las macros que no poder prescindir del preprocesador.

Coloque cada caso de uso en una respuesta separada para que pueda ser votado y, si sabe cómo lograr una de las respuestas sin que el preprocesador, indique cómo en los comentarios de esa respuesta.

¿Fue útil?

Solución

Como contenedores para funciones de depuración, para pasar automáticamente cosas como __FILE__, __LINE__, etc:

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

Otros consejos

Los métodos siempre deben ser código completo y compilable;Las macros pueden ser fragmentos de código.Así puedes definir una macro foreach:

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

Y utilícelo así:

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

Desde C++ 11, esto es reemplazado por el bucle for basado en rango.

Las protecciones de archivos de encabezado requieren macros.

¿Hay otras áreas que necesitar macros?No muchos (si es que hay alguno).

¿Hay otras situaciones que se benefician de las macros?¡¡¡SÍ!!!

Un lugar donde uso macros es con código muy repetitivo.Por ejemplo, al empaquetar código C++ para usarlo con otras interfaces (.NET, COM, Python, etc.), necesito detectar diferentes tipos de excepciones.Así es como lo hago:

#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__); \
}

Tengo que poner estas capturas en cada función contenedora.En lugar de escribir los bloques catch completos cada vez, simplemente escribo:

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

Esto también facilita el mantenimiento.Si alguna vez tengo que agregar un nuevo tipo de excepción, solo hay un lugar donde debo agregarlo.

También hay otros ejemplos útiles:muchos de los cuales incluyen el __FILE__ y __LINE__ Macros de preprocesador.

De todos modos, las macros son muy útiles cuando se usan correctamente.Las macros no son malas: sus mal uso es malo.

Principalmente:

  1. incluir guardias
  2. Compilación condicional
  3. Informes (macros predefinidos como __LINE__ y __FILE__)
  4. (raramente) Duplicar patrones de código repetitivos.
  5. En el código de su competidor.

Dentro de la compilación condicional, para superar problemas de diferencias entre compiladores:

#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

Cuando desea crear una cadena a partir de una expresión, el mejor ejemplo para esto es assert (#x convierte el valor de x a una cuerda).

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

Las constantes de cadena a veces se definen mejor como macros, ya que se pueden hacer más con cadenas literales que con una const char *.

p.ej.Los literales de cadena pueden ser fácilmente concatenado.

#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);

si un const char * Si se usaran, entonces se tendría que usar algún tipo de clase de cadena para realizar la concatenación en tiempo de ejecución:

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);

Cuando desee cambiar el flujo del programa (return, break y continue) el código en una función se comporta de manera diferente que el código que realmente está integrado en la función.

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

Lo obvio incluye guardias.

#ifndef MYHEADER_H
#define MYHEADER_H

...

#endif

No se pueden realizar cortocircuitos en los argumentos de las llamadas a funciones mediante una llamada a función normal.Por ejemplo:

#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

Digamos que ignoraremos cosas obvias como los protectores de cabeza.

A veces, desea generar código que el precompilador debe copiar y pegar:

#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)

que le permite codificar esto:

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

Y puede generar mensajes como:

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

Tenga en cuenta que mezclar plantillas con macros puede conducir a resultados aún mejores (es decir,generando automáticamente los valores al lado de sus nombres de variables)

Otras veces, necesita el __FILE__ y/o el __LINE__ de algún código, para generar información de depuración, por ejemplo.El siguiente es un clásico de Visual C++:

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

Como con el siguiente código:

#pragma message(WRNG "Hello World")

genera mensajes como:

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

Otras veces, necesita generar código usando los operadores de concatenación # y ##, como generar captadores y definidores para una propiedad (esto es para casos bastante limitados).

Otras veces, generará código que no se compilará si se usa a través de una función, como:

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

Que se puede utilizar como

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

(aún así, sólo vi este tipo de código correctamente usado una vez)

Por último, pero no menos importante, el 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:código copiado/pegado de la página de inicio de boost)

¿Cuál es (en mi humilde opinión) mucho mejor que std::for_each.

Por lo tanto, las macros siempre son útiles porque están fuera de las reglas normales del compilador.Pero encuentro que la mayoría de las veces que veo uno, en realidad son restos de código C que nunca se tradujeron al C++ adecuado.

Marcos de prueba unitaria para C++ como Prueba unitaria++ prácticamente giran en torno a macros de preprocesador.Unas pocas líneas de código de prueba unitaria se expanden en una jerarquía de clases que no sería nada divertido escribir manualmente.Sin algo como UnitTest++ y su magia del preprocesador, no sé cómo escribirías pruebas unitarias de manera eficiente para C++.

Temer al preprocesador C es como temer a las bombillas incandescentes sólo porque tenemos bombillas fluorescentes.Sí, el primero puede ser {Electricidad | Tiempo del programador} ineficiente.Sí, te pueden quemar (literalmente).Pero pueden hacer el trabajo si lo manejas adecuadamente.

Cuando se programan sistemas integrados, C suele ser la única opción aparte del ensamblador.Después de programar en el escritorio con C++ y luego cambiar a destinos integrados más pequeños, aprende a dejar de preocuparse por las "faltas de elegancia" de tantas características básicas de C (macros incluidas) y simplemente intenta descubrir el mejor y más seguro uso que puede obtener de esas funciones. características.

Alejandro Stepánov dice:

Cuando programamos en C ++, no debemos avergonzarse de su herencia C, sino que lo use por completo.Los únicos problemas con C ++, e incluso los únicos problemas con C, surgen cuando ellos mismos no son consistentes con su propia lógica.

Usamos el __FILE__ y __LINE__ macros con fines de diagnóstico en el lanzamiento, captura y registro de excepciones ricas en información, junto con escáneres automatizados de archivos de registro en nuestra infraestructura de control de calidad.

Por ejemplo, una macro de lanzamiento OUR_OWN_THROW podría usarse con el tipo de excepción y los parámetros del constructor para esa excepción, incluida una descripción textual.Como esto:

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

Esta macro, por supuesto, arrojará el InvalidOperationException excepción con la descripción como parámetro del constructor, pero también escribirá un mensaje en un archivo de registro que consta del nombre del archivo y el número de línea donde ocurrió el lanzamiento y su descripción textual.La excepción lanzada obtendrá una identificación, que también se registra.Si alguna vez se detecta la excepción en algún otro lugar del código, se marcará como tal y el archivo de registro indicará que esa excepción específica ha sido manejada y que, por lo tanto, no es probable que sea la causa de ningún bloqueo que pueda registrarse más adelante.Nuestra infraestructura automatizada de control de calidad puede detectar fácilmente las excepciones no controladas.

Repetición de código.

Echa un vistazo a aumentar la biblioteca de preprocesador, es una especie de meta-meta-programación.En tema->motivación puedes encontrar un buen ejemplo.

Algunas cosas muy avanzadas y útiles aún se pueden construir usando preprocesador (macros), lo que nunca podrías hacer usando las "construcciones de lenguaje" de C++, incluidas las plantillas.

Ejemplos:

Hacer que algo sea tanto un identificador C como una cadena

Manera fácil de usar variables de tipos de enumeración como cadena en C

Impulsar la metaprogramación del preprocesador

Ocasionalmente uso macros para poder definir información en un solo lugar, pero la uso de diferentes maneras en diferentes partes del código.Es sólo un poco malvado :)

Por ejemplo, en "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

Luego, para una enumeración pública, se puede definir usar solo el nombre:

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

typedef field_ {

#include "field_list.h"

    FIELD_MAX

} field_en;

Y en una función de inicio privada, todos los campos se pueden usar para completar una tabla con los datos:

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

#include "field_list.h"

Un uso común es para detectar el entorno de compilación; para el desarrollo multiplataforma, puede escribir un conjunto de código para Linux, por ejemplo, y otro para Windows cuando ya no exista una biblioteca multiplataforma para sus propósitos.

Entonces, en un ejemplo aproximado, un mutex multiplataforma puede tener

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

Para las funciones, son útiles cuando desea ignorar explícitamente la seguridad de tipos.Como los muchos ejemplos anteriores y siguientes para hacer ASSERT.Por supuesto, como muchas características de C/C++, puedes dispararte en el pie, pero el lenguaje te brinda las herramientas y te permite decidir qué hacer.

Algo como

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

Para que puedas, por ejemplo, tener

assert(n == true);

y obtenga el nombre del archivo fuente y el número de línea del problema impreso en su registro si n es falso.

Si utiliza una llamada de función normal como

void assert(bool val);

en lugar de la macro, todo lo que puede obtener es el número de línea de su función de aserción impreso en el registro, lo que sería menos útil.

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

A diferencia de la solución de plantilla 'preferida' analizada en un hilo actual, puedes usarla como una expresión constante:

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

Puede utilizar #defines para ayudar con escenarios de depuración y pruebas unitarias.Por ejemplo, cree variantes de registro especiales de las funciones de memoria y cree un memlog_preinclude.h especial:

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

Compile su código usando:

gcc -Imemlog_preinclude.h ...

Un enlace en tu memlog.o a la imagen final.Ahora controla malloc, etc., tal vez con fines de registro o para simular fallas de asignación para pruebas unitarias.

Utilizo macros para definir fácilmente excepciones:

DEF_EXCEPTION(RessourceNotFound, "Ressource not found")

donde DEF_EXCEPTION es

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

Los compiladores pueden rechazar su solicitud de inclusión en línea.

Las macros siempre tendrán su lugar.

Algo que encuentro útil es #define DEBUG para el seguimiento de depuraciones: puedes dejarlo en 1 mientras depuras un problema (o incluso dejarlo activado durante todo el ciclo de desarrollo) y luego desactivarlo cuando llegue el momento de enviarlo.

Cuando toma una decisión en tiempo de compilación sobre el comportamiento específico del compilador/sistema operativo/hardware.

Le permite crear su interfaz para características específicas del Compilador/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

En mi último trabajo, estaba trabajando en un escáner de virus.Para facilitarme la depuración, tenía muchos registros atascados por todas partes, pero en una aplicación de alta demanda como esa, el gasto de una llamada a función es demasiado caro.Entonces, se me ocurrió esta pequeña macro, que aún me permitía habilitar el registro de depuración en una versión de lanzamiento en el sitio de un cliente, sin el costo de una llamada de función, verificaría el indicador de depuración y simplemente regresaría sin registrar nada, o si estaba habilitado , haría el registro...La macro se definió de la siguiente manera:

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

Debido a VA_ARGS en las funciones de registro, este era un buen caso para una macro como esta.

Antes de eso, usaba una macro en una aplicación de alta seguridad que necesitaba decirle al usuario que no tenía el acceso correcto y le decía qué bandera necesitaba.

Las macros definidas como:

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

Luego, podríamos simplemente esparcir las comprobaciones por toda la interfaz de usuario y le indicaría qué roles tenían permiso para realizar la acción que intentó realizar, si aún no tenía ese rol.El motivo de dos de ellos era devolver un valor en algunos lugares y regresar de una función nula en otros...

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;
   }
...

De todos modos, así es como los he usado, y no estoy seguro de cómo se podría haber ayudado con las plantillas...Aparte de eso, trato de evitarlos, a menos que REALMENTE sea necesario.

Otra macro más para cada uno.T:tipo, c:contenedor, yo:iterador

#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)

Uso (que muestra el concepto, no es real):

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;
}

Mejores implementaciones disponibles:Google "BOOST_FOREACH"

Buenos artículos disponibles: Amor condicional:FOREACHRedux (Eric Niebler) http://www.artima.com/cppsource/foreach.html

Quizás el mayor uso de macros sea en el desarrollo independiente de la plataforma.Piense en casos de inconsistencia de tipos: con las macros, simplemente puede usar diferentes archivos de encabezado, como:--WIN_TYPES.H

typedef ...some struct

--POSIX_TYPES.h

typedef ...some another struct

--programa.h

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

#include TYPES_H

En mi opinión, es mucho más legible que implementarlo de otras maneras.

Parece que VA_ARGS sólo se ha mencionado indirectamente hasta ahora:

Al escribir código C++03 genérico y necesita un número variable de parámetros (genéricos), puede utilizar una macro en lugar de una plantilla.

#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: En general, el control/lanzamiento de nombre también podría incorporarse al hipotético get_op_from_name función.Este es sólo un ejemplo.Es posible que haya otro código genérico relacionado con la llamada VA_ARGS.

Una vez que obtengamos plantillas variadas con C++ 11, podemos resolver esto "correctamente" con una plantilla.

Creo que este truco es un uso inteligente del preprocesador que no se puede emular con una función:

#define COMMENT COMMENT_SLASH(/)
#define COMMENT_SLASH(s) /##s

#if defined _DEBUG
#define DEBUG_ONLY
#else
#define DEBUG_ONLY COMMENT
#endif

Entonces puedes usarlo así:

cout <<"Hello, World!" <<endl;
DEBUG_ONLY cout <<"This is outputed only in debug mode" <<endl;

También puede definir una macro RELEASE_ONLY.

Puede #define constantes en la línea de comando del compilador usando el -D o /D opción.Esto suele ser útil al realizar una compilación cruzada del mismo software para múltiples plataformas porque puede hacer que sus archivos MAKE controlen qué constantes se definen para cada plataforma.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top