En C++, no siempre quieres exponer la capacidad total de un contenedor. A veces, necesitas restringir la interfaz para garantizar que los datos se manipulen siguiendo una lógica semántica estricta. Aquí entran los adaptadores de contenedor. No son contenedores en sí mismos, sino envoltorios (wrappers) que toman un contenedor existente y le limitan la interfaz para ofrecer un comportamiento específico: LIFO (Last In, First Out), FIFO (First In, First Out) o un orden basado en prioridad.
Un adaptador funciona mediante la composición: contiene un contenedor interno (el “container”) y delega sus operaciones a este, pero solo permite llamar a las funciones que encajan con su modelo. Por defecto, tanto std::stack como std::queue utilizan std::deque como contenedor base. Se elige std::deque porque permite inserciones y eliminaciones eficientes en los extremos sin necesidad de reasignar bloques de memoria contiguos como lo haría un std::vector, lo cual es ideal para la estructura de una cola.
Debes usar un std::stack cuando tu lógica dependa de un historial donde solo importa el último elemento añadido. Usa std::queue para sistemas de mensajería o buffers de procesamiento donde el orden de llegada es crítico. Utiliza std::priority_queue cuando el orden de procesamiento no dependa de cuándo llegó el elemento, sino de una propiedad intrínseca del objeto (como la urgencia). El error ocurre cuando intentas usar un adaptador para algo que requiere acceso aleatorio o iteración; por ejemplo, si necesitas recorrer todos los elementos de una std::stack, has elegido la herramienta equivocada y deberías haber usado un std::vector o std::deque directamente.
#include <iostream>
#include <stack>
#include <queue>
#include <vector>
#include <string>
// Representa un evento en un sistema de gestión
struct Evento {
int nivel_urgencia; // Menor número = Mayor prioridad
std::string descripcion;
// Para que priority_queue funcione como un min-heap (menor número primero),
// definimos el operador > para que el contenedor pueda comparar prioridades.
bool operator>(const Evento& otro) const {
return nivel_urgencia > otro.nivel_urgencia;
}
};
int main() {
// 1. std::stack (LIFO): Para gestionar el historial de comandos (Undo/Redo)
std::stack<std::string> historial;
historial.push("Dibujar rectangulo");
historial.push("Cambiar color");
historial.push("Mover objeto");
std::cout << "[Stack] Deshaciendo: " << historial.top() << "\n";
historial.pop(); // Eliminamos 'Mover objeto'
std::cout << "[Stack] Ahora el top es: " << historial.top() << "\n\n";
// 2. std::queue (FIFO): Buffer para procesar mensajes de red en orden
std::queue<std::string> buffer_mensajes;
buffer_mensajes.push("Mensaje 1: Hola");
buffer_mensajes.push("Mensaje 2: ¿Cómo estás?");
buffer_mensajes.push("Mensaje 3: Adiós");
std::cout << "[Queue] Procesando: " << buffer_mensajes.front() << "\n";
buffer_mensajes.pop(); // Eliminamos el primero en llegar
std::cout << "[Queue] Siguiente en cola: " << buffer_mensajes.front() << "\n\n";
// 3. std::priority_queue: Gestión de emergencias
// Usamos std::vector como contenedor base y std::greater para convertir
// el max-heap por defecto en un min-heap (prioridad por valor mínimo).
std::priority_queue<Evento, std::vector<Evento>, std::greater<Evento>> despacho;
despacho.push({3, "Usuario pide soporte técnico"});
despacho.push({1, "Fallo crítico de servidor"});
despacho.push({2, "Actualización de sistema programada"});
std::cout << "[Priority Queue] Atendiendo emergencias por orden de urgencia:\n";
while (!despacho.empty()) {
// top() siempre devuelve el elemento con mayor prioridad (el menor nivel_urgencia)
std::cout << " - Atendiendo: " << despacho.top().descripcion
<< " (Urgencia: " << despacho.top().nivel_urgencia << ")\n";
despacho.pop();
}
return 0;
}
Análisis del código
En el ejemplo, std::stack<std::string> historial encapsula un contenedor que no nos permite ver qué hay debajo de top(). Cuando llamamos a historial.pop(), el adaptador llama al pop_back() del contenedor interno. Es crucial notar que pop() en std::stack y std::queue devuelve void; esto se diseñó así para evitar problemas de seguridad con los elementos temporales y mantener la consistencia en la gestión de recursos.
Para std::queue<std::string> buffer_mensajes, el adaptador utiliza las capacidades de std::deque para que push() (que es un push_back) y pop() (que es un pop_front) sean operaciones de tiempo constante $O(1)$, sin mover elementos en memoria.
En la std::priority_queue<Evento, ...>, la lógica es más compleja. Internamente, el adaptador gestiona un heap (montículo) sobre un std::vector. Al usar std::greater<Evento>, estamos alterando la comparación para que el elemento con el “menor” valor sea siempre el que esté en la raíz del árbol (el top()). Cada vez que haces push(), el adaptador invoca algoritmos de reordenación para mantener la propiedad de heap, lo que garantiza que el acceso al elemento de máxima prioridad sea $O(1)$ y la inserción sea $O(\log n)$.
El error frecuente
Un error común y peligroso es llamar a top() o front() cuando el contenedor está vacío.
std::stack<int> s; // ... lógica que por error deja el stack vacío ... int valor = s.top(); // UB: Comportamiento Indefinido
Llamar a top() en un contenedor vacío es comportamiento indefinido (UB). El estándar no exige que se lance una excepción; lo más probable es que el programa acceda a una dirección de memoria inválida en la pila, lo que provocará un crash inmediato o, peor aún, un bug silencioso que solo se manifieste en producción. Herramientas como AddressSanitizer (ASan) con el flag -fsanitize=address son capaces de detectar esto durante las pruebas unitarias. Siempre verifica empty() antes de acceder al elemento superior.
N° 88