¿Es un uso apropiado de #define para facilitar la escritura de código repetido?

17

¿Hay alguna opinión sobre si usar #define para definir líneas completas de código para simplificar la codificación es una buena o mala práctica de programación? Por ejemplo, si tuviera que imprimir un montón de palabras juntas, me molestaría escribir

<< " " <<

Para insertar un espacio entre palabras en una declaración cout. Solo podría hacer

#define pSpace << " " <<

y escribe

cout << word1 pSpace word2 << endl;

Para mí, esto no agrega ni resta de la claridad del código y hace que escribir sea un poco más fácil. Hay otros casos en los que puedo pensar que escribir será mucho más fácil, generalmente para la depuración.

Tiene alguna idea sobre esto?

EDITAR: ¡Gracias por todas las excelentes respuestas! Esta pregunta me llegó después de escribir mucho de forma repetitiva, pero nunca pensé que habría otras macros menos confusas para usar. Para aquellos que no quieren leer todas las respuestas, la mejor alternativa es usar las macros de su IDE para reducir la escritura repetitiva.

gsingh2011
fuente
74
Está claro para ti porque lo inventaste. Para todos los demás, simplemente se ofuscó. Al principio parece un error de sintaxis. Cuando se compila, pensaría qué diablos y luego descubriría que tiene una macro que no está en mayúsculas. En mi opinión, esto hace que el código sea horrible de mantener. Definitivamente lo rechazaría si viniera a revisar el código y no espero que encuentre muchos que lo acepten. ¡¡¡Y estás guardando 3 caracteres !!!!!!!!!
Martin York
11
En los casos en que no se pueda factorizar la repetitividad utilizando funciones o lo que sea, el mejor enfoque es aprender qué puede hacer su editor o IDE para ayudarlo. Las macros de editor de texto o las teclas de acceso rápido "fragmento" pueden evitar que escriba demasiado sin dañar la legibilidad.
Steve314
2
He hecho esto antes (con trozos más grandes de repetitivo), pero mi práctica era escribir el código, luego ejecutar el preprocesador y reemplazar el archivo original con la salida del preprocesador. Me salvó la escritura, me ahorró (y a otros) la molestia de mantenimiento.
TMN
9
Guardaste 3 personajes y los cambiaste por declaraciones confusas. Muy buen ejemplo de mala macro, en mi humilde opinión: o)
MaR
77
Muchos editores tienen una función avanzada para exactamente este escenario, se llama "Copiar y Pegar"
Chris Burt-Brown

Respuestas:

111

Escribir código es fácil. Leer el código es difícil.

Escribes código una vez. Vive durante años, la gente lo lee cien veces.

Optimice el código para leer, no para escribir.

tdammers
fuente
11
Estoy de acuerdo al 100%. (De hecho, iba a escribir esta respuesta yo mismo). El código se escribe una vez, pero podría leerse docenas, cientos o incluso miles de veces, tal vez por docenas, cientos o incluso miles de desarrolladores. El tiempo que lleva escribir código es totalmente irrelevante, lo único que cuenta es el tiempo para leerlo y comprenderlo.
sbi
2
El preprocesador puede y debe usarse para optimizar el código de lectura y mantenimiento.
SK-logic
2
E incluso si solo está leyendo el código en uno o dos años: olvidará estas cosas usted mismo mientras hace otras cosas en el medio.
johannes
2
"Siempre codifica como si el tipo que termina manteniendo tu código sea un psicópata violento que sabe dónde vives". - (Martin Golding)
Dylan Yaga
@Dylan - de lo contrario, después de unos meses manteniendo ese código, él te encontrará - (Yo)
Steve314
28

Personalmente, lo detesto. Hay varias razones por las que desaliento a las personas de esta técnica:

  1. En tiempo de compilación, los cambios reales en el código pueden ser significativos. El siguiente tipo aparece e incluso incluye un corchete de cierre en su #define o una llamada de función. Lo que está escrito en un cierto punto de código está lejos de lo que va a estar allí después del preprocesamiento.

  2. Es ilegible. Puede ser claro para usted ... por ahora ... si es solo esta una definición. Si se convierte en un hábito, pronto terminará con docenas de #defines y comenzará a perder el rastro usted mismo. Pero lo peor de todo es que nadie más podrá entender qué word1 pSpace word2significa exactamente (sin buscar el #definir).

  3. Puede convertirse en un problema para herramientas externas. Digamos que de alguna manera terminas con un #define que incluye un corchete de cierre, pero no un corchete de apertura. Todo puede funcionar bien, pero los editores y otras herramientas pueden ver algo function(withSomeCoolDefine;bastante peculiar (es decir, informarán errores y demás). (Ejemplo similar: una llamada a una función dentro de una definición: ¿podrán sus herramientas de análisis encontrar esta llamada?)

  4. El mantenimiento se vuelve mucho más difícil. Tiene todos los definidos además de los problemas habituales que conlleva el mantenimiento. Además del punto anterior, el soporte de herramientas para la refactorización también puede verse afectado negativamente.

Franco
fuente
44
Finalmente me prohibí casi todas las macros debido a las molestias de tratar de manejarlas directamente en Doxygen. Han pasado bastantes años y, en general, creo que la legibilidad mejoró bastante, independientemente de si estoy usando Doxygen o no.
Steve314
16

Mi pensamiento principal sobre esto es que nunca uso "hacer que escribir sea más fácil" como regla cuando escribo código.

Mi regla principal cuando escribo código es hacerlo fácilmente legible. La razón detrás de esto es simplemente que el código se lee un orden de magnitud más veces que se escribe. Como tal, el tiempo que pierde al escribirlo con cuidado, de manera ordenada y correctamente distribuida se invierte en leer más y comprender mucho más rápido.

Como tal, el #define que usa simplemente rompe la forma habitual de alternar <<y otras cosas . Rompe la regla de la menor sorpresa y, en mi humilde opinión, no es algo bueno.

Didier Trosset
fuente
1
+1: "¡¡El código se lee un orden de magnitud más veces que se escribe" !!!!
Giorgio
14

Esta pregunta ofrece un claro ejemplo de cómo puede usar mal las macros. Para ver otros ejemplos (y entretenerse) vea esta pregunta .

Dicho esto, daré ejemplos del mundo real de lo que considero una buena incorporación de macros.

El primer ejemplo aparece en CppUnit , que es un marco de prueba de unidad. Al igual que cualquier otro marco de prueba estándar, crea una clase de prueba y luego tiene que especificar de alguna manera qué métodos deben ejecutarse como parte de la prueba.

#include <cppunit/extensions/HelperMacros.h>

class ComplexNumberTest : public CppUnit::TestFixture  
{
    CPPUNIT_TEST_SUITE( ComplexNumberTest );
    CPPUNIT_TEST( testEquality );
    CPPUNIT_TEST( testAddition );
    CPPUNIT_TEST_SUITE_END();

 private:
     Complex *m_10_1, *m_1_1, *m_11_2;
 public:
     void setUp();
     void tearDown();
     void testEquality();
     void testAddition();
}

Como puede ver, la clase tiene un bloque de macros como primer elemento. Si agregué un nuevo método testSubtraction, es obvio lo que debe hacer para incluirlo en la ejecución de la prueba.

Estos macrobloques se expanden a algo como esto:

public: 
  static CppUnit::Test *suite()
  {
    CppUnit::TestSuite *suiteOfTests = new CppUnit::TestSuite( "ComplexNumberTest" );
    suiteOfTests->addTest( new CppUnit::TestCaller<ComplexNumberTest>( 
                                   "testEquality", 
                                   &ComplexNumberTest::testEquality ) );
    suiteOfTests->addTest( new CppUnit::TestCaller<ComplexNumberTest>(
                                   "testAddition",
                                   &ComplexNumberTest::testAddition ) );
    return suiteOfTests;
  }

¿Cuál preferirías leer y mantener?

Otro ejemplo está en el marco Microsoft MFC, donde asigna funciones a mensajes:

BEGIN_MESSAGE_MAP( CMyWnd, CMyParentWndClass )
    ON_MESSAGE( WM_MYMESSAGE, OnMyMessage )
    ON_COMMAND_RANGE(ID_FILE_MENUITEM1, ID_FILE_MENUITEM3, OnFileMenuItems)
    // ... Possibly more entries to handle additional messages
END_MESSAGE_MAP( )

Entonces, ¿cuáles son las cosas que distinguen a las "buenas macros" del horrible tipo maligno?

  • Realizan una tarea que no se puede simplificar de otra manera. Escribir una macro para determinar un máximo entre dos elementos es incorrecto, porque puede lograr lo mismo utilizando un método de plantilla. Pero hay algunas tareas complejas (por ejemplo, mapear códigos de mensajes a funciones miembro) que el lenguaje C ++ simplemente no maneja con elegancia.

  • Tienen un uso extremadamente estricto y formal. En ambos ejemplos, los macrobloques se anuncian al iniciar y finalizar las macros, y las macros intermedias solo aparecerán dentro de estos bloques. Tiene C ++ normal, se disculpa brevemente con un bloque de macros y luego vuelve a la normalidad nuevamente. En los ejemplos de "macros malvadas", las macros están dispersas por todo el código y el desafortunado lector no tiene forma de saber cuándo se aplican las reglas de C ++ y cuándo no.

Andrew Shepherd
fuente
5

Será, por supuesto, mejor si sintoniza su editor de texto / IDE favorito para insertar fragmentos de código que le resulta tedioso volver a escribir una y otra vez. Y mejor es el término "educado" para comparar. En realidad, no puedo pensar en ningún caso similar cuando el preprocesamiento supera las macros del editor. Bueno, puede ser uno, cuando por razones misteriosas e infelices está constantemente usando un conjunto diferente de herramientas para la codificación. Pero no es una justificación :)

También puede ser una mejor solución para escenarios más complejos, cuando el preprocesamiento de texto puede hacer cosas mucho más ilegibles y complicadas (piense en la entrada parametrizada).

shabunc
fuente
2
+1. De hecho: deja que el editor haga el trabajo por ti. Obtendrá lo mejor de ambos mundos si, por ejemplo, hace una abreviatura de << " " <<.
unperson325680
-1 para "También puede ser una mejor solución para escenarios más complejos, cuando el preprocesamiento de texto puede hacer cosas mucho más ilegibles y complicadas (piense en la entrada parametrizada)" - Si es tan complejo, cree un método para ello, incluso entonces, haga Un método para ello. Por ejemplo, este mal que encontré recientemente en el código ..... #define printError (x) {puts (x); return x}
mattnz
@mattnz, me refería a construcciones de bucles, construcciones if / else, plantillas para la creación de comparadores, etc., ese tipo de cosas. En IDEs, este tipo de entrada parametrizada le ayuda no solo a escribir rápidamente algunas líneas de código, sino también a iterar rápidamente a través de parámetros. Nadie intenta competir con los métodos. método es método)))
shabunc
4

Los otros ya han explicado por qué no debes hacerlo. Su ejemplo obviamente no merece ser implementado con una macro. Pero, hay una amplia gama de casos en los que tiene que usar macros por razones de legibilidad.

Un ejemplo notorio de una aplicación inteligente de tal técnica es el proyecto Clang : vea cómo .defse usan los archivos allí. Con las macros #include, puede proporcionar una definición única, a veces totalmente declarativa, para una colección de cosas similares que se desenrollarán en declaraciones de tipos, casedeclaraciones cuando corresponda, inicializadores predeterminados, etc. Aumenta significativamente la capacidad de mantenimiento: nunca olvidará agregar nuevos casedeclaraciones en todas partes cuando ha agregado una nueva enum, por ejemplo.

Entonces, como con cualquier otra herramienta poderosa, debe usar el preprocesador C con cuidado. No hay reglas genéricas en el arte de la programación, como "nunca debe usar esto" o "siempre debe usar eso". Todas las reglas no son más que pautas.

SK-logic
fuente
3

Nunca es apropiado usar #defines así. En su caso, podría hacer esto:

class MyCout 
{
public:
  MyCout (ostream &out) : m_out (out), m_space_pending (false)
  {
  }

  template <class T>
  MyCout &operator << (T &value)
  { 
    if (m_space_pending)
      m_out << " ";

    m_out << value;
    m_space_pending = false;
    return *this;
  }

  MyCout &operator << (const char *value)
  {
    if (m_space_pending)
      m_out << " ";

    m_out << value;
    m_space_pending = true;
    return *this;
  }

  MyCout &operator << (char *value) { return operator << (static_cast <const char *> (value)); }
  MyCout &operator << (ostream& (*fn)(ostream&)) { m_out << fn; return *this; }

private:
  ostream
    &m_out;

  bool
    m_space_pending;
};

int main (int argc, char *argv [])
{
  MyCout
    space_separated (cout);

  space_separated << "Hello" << "World" << endl;
}
Skizz
fuente
2

No.

Para las macros destinadas a usarse en código, una buena guía para probar la idoneidad es rodear su expansión con paréntesis (para expresiones) o llaves (para código) y ver si aún se compilará:

// These don't compile:

#define pSpace (<< " " <<)
cout << word1 pSpace word2 << endl;

#define space(x) (" " << (x))
cout << word1 << space(word2) << endl;

// These do:

#define FOO_FACTOR (38)
x = y * FOO_FACTOR;

#define foo() (cout << "Foo" << endl)
foo();

#define die(c) { if ((c)) { exit(1); } }
die(foo > 8);

#define space(x) (" " + string((x)))
cout << "foo" << space("bar") << endl;

Las macros utilizadas en las declaraciones (como el ejemplo en la respuesta de Andrew Shepherd) pueden salirse con la suya con un conjunto de reglas más flexibles siempre que no alteren el contexto circundante (por ejemplo, cambiar entre publicy private).

Blrfl
fuente
1

Es una cosa razonablemente válida para hacer en un programa puro "C".

Es innecesario y confuso en un programa C ++.

Hay muchas formas de evitar la escritura repetitiva de código en C ++. Desde el uso de las facilidades proporcionadas por su IDE (incluso con vi, un simple " %s/ pspace /<< " " <</g" ahorraría tanto tipeo y aún produciría un código legible estándar). Podría definir métodos privados para implementar esto o, para casos más complejos, la plantilla C ++ sería más limpia y sencilla.

James Anderson
fuente
2
No, no es una cosa razonablemente válida para hacer en C. pura Valores individuales o expresiones independientes completas que se basan solo en los parámetros macro, sí, y para este último una función puede ser incluso una mejor opción. Tal construcción a medio cocer como en el ejemplo, de ninguna manera.
Seguro
@secure - Estoy de acuerdo en que no es una buena idea en el caso del ejemplo dado. Pero dada la falta de plantillas, etc., existen usos válidos para las macros "#DEFINE" en C.
James Anderson
1

En C ++ esto se puede resolver con sobrecarga del operador. O incluso algo tan simple como una función variadic:

lineWithSpaces(word1, word2, word3, ..., wordn)es simple y te ahorra escribir pSpacesuna y otra vez.

Entonces, aunque en su caso puede no parecer un gran problema, hay una solución que es más simple y más robusta.

En general, hay pocos casos en los que el uso de una macro es significativamente más corto sin introducir la ofuscación, y en su mayoría hay una solución lo suficientemente corta usando las características del lenguaje real (las macros son más que una mera sustitución de cadenas).

back2dos
fuente
0

¿Hay alguna opinión sobre si usar #define para definir líneas completas de código para simplificar la codificación es una buena o mala práctica de programación?

Si, es muy malo. Incluso he visto gente haciendo esto:

#define R return

para guardar la escritura (lo que está tratando de lograr).

Tal código pertenece solo en lugares como este .

BЈовић
fuente
-1

Las macros son malvadas y solo deben usarse cuando realmente sea necesario. Hay algunos casos en los que son aplicables las macros (principalmente depuración). Pero en C ++ en la mayoría de los casos, puede usar funciones en línea .

sakisk
fuente
2
No hay nada intrínsecamente malo en ninguna técnica de programación. Se pueden usar todas las herramientas y métodos posibles, siempre que sepa lo que está haciendo. Se aplica a los infames goto, a todos los macro sistemas posibles, etc.
SK-logic
1
Esa es exactamente la definición del mal en este caso: "algo que debes evitar la mayor parte del tiempo, pero no algo que debes evitar todo el tiempo". Se explica en el enlace que señala el mal .
sakisk
2
Creo que es contraproducente calificar cualquier cosa como "malvada", "potencialmente dañina" o incluso "sospechosa". No me gusta la noción de "mala práctica" y "código de olor". Todos y cada uno de los desarrolladores tienen que decidir en cada caso específico, qué práctica es perjudicial. El etiquetado es una práctica perjudicial : las personas tienden a no pensar más si algo ya ha sido etiquetado por los demás.
SK-logic
-2

No, no puedes usar macros para guardar la escritura .

Sin embargo, tiene permiso, incluso debe usarlos para separar parte del código que no cambia de los que cambian, y reducir la redundancia. Para esto último, debe pensar en alternativas y elegir macro solo si las mejores herramientas no funcionan. (Para la práctica, la macro está bastante al final de la línea, por lo que tenerla implica el último recurso ...)

Para reducir la escritura, la mayoría de los editores tienen macros, incluso fragmentos de código inteligentes.

Balog Pal
fuente
"No, no puedes usar macros para guardar la escritura ". - ¡Buen trabajo, no voy a escuchar tu pedido aquí! Si tengo una pila de objetos que necesito declarar / definir / mapear / switch/ etc, puedes apostar que usaré macros para evitar volverme loco y aumentar la legibilidad. Usar macros para guardar la escritura en palabras clave, controlar el flujo, etc. es estúpido, pero decir que nadie puede usarlas para guardar las pulsaciones de teclas en contextos válidos es igualmente estúpido.
underscore_d