Las macros son directivas para el preprocesador, un motor de sustitución de texto que actúa antes de que el compilador empiece a entender la semántica de tu código. Funcionan mediante un simple reemplazo de tokens; el preprocesador no entiende tipos, ámbitos (scope) ni la lógica del lenguaje.
Debido a esta naturaleza, las macros operan por sustitución ciega. Si defines #define PI 3.14, el preprocesador simplemente buscará la secuencia de caracteres PI y la reemplazará por 3.14 en todo el archivo. Esto significa que las macros no respetan los niveles de visibilidad de un namespace o una clase, pudiendo causar colisiones de nombres difíciles de rastrear.
Deberías utilizarlas con extrema precaución. En el C++ moderno, la mayoría de las funciones que antes requerían macros (como constantes o funciones de utilidad) ahora se implementan mejor con constexpr o template. Sin embargo, todavía existen nichos válidos, como la generación de código repetitivo mediante X-macros o la captura de metadatos del compilador como __FILE__ o __LINE__ para sistemas de logging.
El gran peligro reside en las macros funcionales. Al ser simples expansiones de texto, si olvidas proteger los argumentos con paréntesis o si pasas una expresión con efectos secundarios (como i++), la macro puede evaluar ese argumento más de una vez o romper la precedencia de los operadores.
#include <iostream>
#include <string>
#include <type_traits>
// Macro para stringificación: convierte un token en una cadena de texto literal [C++11]
#define STRINGIFY(x) #x
// Macro para concatenación de tokens: une dos tokens en uno nuevo
#define CONCAT(a, b) a##b
// Idioma "do { ... } while(0)": asegura que la macro se comporte como una única sentencia
// y sea segura al usarse dentro de estructuras if/else sin llaves.
#define LOG_INFO(msg) do { \
std::cout << "[INFO] " << msg << std::endl; \
} while(0)
// Macro funcional PELIGROSA: carece de protección de paréntesis en argumentos
// y es vulnerable a la doble evaluación.
#define BAD_MAX(a, b) ((a) > (b) ? (a) : (b))
// Alternativa moderna para constantes: constexpr garantiza evaluación en tiempo de compilación
// y respeta el scope y el tipado.
constexpr double PI_VAL = 3.1415926535;
// Alternativa moderna para funciones: templates con inline garantizan seguridad de tipos
// y aseguran que cada argumento se evalúe exactamente una vez.
template <typename T>
inline T safe_max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
int i = 10;
int j = 20;
// 1. Uso de stringificación y concatenación
std::cout << "El valor de i es: " << STRINGIFY(i) << " (texto)" << std::endl;
int variable_generada = CONCAT(valor_, i); // Esto daría error si i no fuera un token válido,
// pero aquí ilustramos la mecánica de unión.
// 2. El peligro de la doble evaluación con BAD_MAX
// Queremos el máximo entre i incrementado y j.
// Lo que esperamos: i=11, j=20, resultado=20.
// Lo que ocurre: BAD_MAX expande a ((++i) > (j) ? (++i) : (j))
// El incremento ocurre dos veces en el caso de 'a' si la condición se cumple.
int error_max = BAD_MAX(++i, j);
std::cout << "i tras BAD_MAX: " << i << " (esperado 11, obtenido " << i << ")" << std::endl;
// 3. La solución segura: safe_max
int i_safe = 10, j_safe = 20;
int result_safe = safe_max(++i_safe, j_safe);
std::cout << "i tras safe_max: " << i_safe << " (esperado 11, obtenido " << i_safe << ")" << std::endl;
// 4. Uso de constexpr y logging
LOG_INFO("Iniciando programa");
std::cout << "Pi es: " << PI_VAL << std::endl;
return 0;
}
Para compilar este ejemplo:
g++ -std=c++20 -Wall -Wextra -Wpedantic -o example example.cpp
Análisis del código
Fíjate en cómo BAD_MAX se expande antes de que el compilador pueda analizar la lógica. En la línea int error_max = BAD_MAX(++i, j);, el preprocesador sustituye a por ++i. La expresión resultante es ((++i) > (j) ? (++i) : (j)). Al evaluar la condición, ++i se ejecuta, incrementando i de 10 a 11. Pero como la condición 11 > 20 es falsa, el operador ternario salta a la parte de false, pero si la condición hubiera sido verdadera, el ++i del cuerpo habría vuelto a ejecutarse. En el código escrito, debido a cómo se estructuró la macro sin paréntesis en los argumentos, la lógica de evaluación de expresiones de C++ interactúa con la expansión textual de forma desastrosa.
En cambio, safe_max es una función template. Aquí, los argumentos a y b son nombres de variables locales en el ámbito de la función. Cuando pasamos ++i_safe, se evalúa la expresión una sola vez, el resultado se pasa por valor (o referencia si usáramos T&) y se asigna a a. No hay sustitución de texto, hay una llamada a función real con reglas de evaluación de expresiones estándar.
El uso de do { ... } while(0) en LOG_INFO es un patrón clásico para evitar errores semánticos. Si usaras una macro que contiene dos sentencias sin este envoltorio y la llamas dentro de un if sin llaves, solo la primera sentencia estaría condicionada al if. El do-while fuerza el uso de un punto y coma al final y encapsula todo el bloque como una única unidad lógica.
PI_VAL es una constante constexpr. A diferencia de #define PI_VAL, esta tiene un tipo (double) y reside dentro del ámbito donde fue declarada, lo que permite que el depurador (debugger) vea el símbolo y facilita la detección de errores de tipo por parte del compilador.
El error frecuente
El error más insidioso es la doble evaluación en macros funcionales.
#define SQUARE(x) ((x) * (x))
int main() {
int n = 5;
int result = SQUARE(++n);
// El compilador ve: ((++n) * (++n))
// n termina valiendo 7, no 6.
}
Este error es extremadamente difícil de depurar porque el código parece lógicamente correcto. El compilador no lanzará un error; simplemente el estado de tus variables cambiará de forma no determinista respecto a tu intención original. Si usas herramientas como AddressSanitizer, no detectarán esto porque no es un error de memoria, es un error de lógica derivado de la semántica del lenguaje. Para evitarlo, si necesitas transformar un argumento, usa una función inline o un template de C++.
N° 105