Cuando eliges un contenedor para gestionar una secuencia de datos, estás negociando tres variables críticas: localidad de caché, complejidad de inserción y modelo de memoria. No existe el “mejor” contenedor, sino el que mejor se adapta a tu algoritmo. La elección impacta directamente en el rendimiento debido a cómo la CPU interactúa con la jerarquía de memoria (L1/L2/L3/RAM). Si necesitas acceso aleatorio rápido y el tamaño es fijo, necesitas contigüidad; si necesitas insertar constantemente en medio, necesitas punteros. Si ignoras esto, tu programa pasará más tiempo esperando a que los datos lleguen de la RAM (debido a los cache misses) que ejecutando lógica útil.
Para decidir, considera qué estructura describe tu flujo de datos: std::array es un envoltorio de alto nivel sobre un array nativo de C, ideal para tamaños fijos conocidos en tiempo de compilación, garantizando cero sobrecarga y uso del stack. std::deque (double-ended queue) es una estructura híbrida que usa un array de bloques de tamaño fijo para permitir inserciones $O(1)$ tanto al principio como al final, pero a diferencia de un std::vector, sus elementos no son contiguos en memoria, por lo que no ofrece el método .data(). Si tu prioridad es la eficiencia en inserciones y borrados en cualquier posición (siempre que tengas un iterador), std::list es una lista doblemente enlazada que permite mover nodos mediante splice sin copiar datos. Por último, std::forward_list es la versión minimalista de una lista simplemente enlazada, diseñada para cuando el ahorro de memoria es vital y solo necesitas avanzar en una dirección. Un error común es intentar usar std::list para algoritmos de búsqueda que requieren acceso aleatorio, lo que transforma un $O(1)$ en un $O(n)$ desastroso, o asumir que un std::deque es contiguo para pasarlo a una API de C.
#include <iostream>
#include <array>
#include <deque>
#include <list>
#include <forward_list>
#include <string>
#include <algorithm>
struct LogEntry {
int id;
std::string mensaje;
};
int main() {
// 1. std::array: Tamaño fijo, en el stack, sin overhead.
// Ideal para configuraciones que no cambian.
constexpr std::array<int, 3> sensores_ids = {101, 102, 103};
// 2. std::deque: Inserción rápida en ambos extremos.
// Internamente es un array de punteros a bloques.
std::deque<std::string> cola_tareas;
cola_tareas.push_back("Tarea_A");
cola_tareas.push_back("Tarea_B");
cola_tareas.push_front("Tarea_Prioritaria"); // O(1) amortizado
// 3. std::list: Inserción/borrado O(1) con iteradores.
// Perfecta para mover grandes bloques de datos sin copiar.
std::list<LogEntry> historial;
historial.push_back({1, "Inicio"});
historial.push_back({2, "Procesando"});
// std::list::splice: Mueve elementos entre listas en O(1)
// sin copiar los objetos, solo reencauzando punteros.
std::list<LogEntry> auditoria;
auditoria.splice(auditoria.begin(), historial, historial.end());
// 4. std::forward_list: Minimalismo extremo.
// Solo puntero 'next', no tiene size().
std::forward_list<int> estados = {1, 2, 3};
estados.push_front(0);
// Demostración de acceso y recorrido
std::cout << "Sensores: ";
for (int id : sensores_ids) std::cout << id << " ";
std::cout << "\nCola tareas (deque): ";
for (const auto& t : cola_tareas) std::cout << "[" << t << "] ";
std::cout << "\nHistorial (list, tras splice): ";
for (const auto& log : auditoria) std::cout << log.id << " ";
return 0;
}
Desglose del ejemplo
En el código anterior, sensores_ids es un std::array. Al ser de tamaño constexpr, el compilador conoce su tamaño exactamente y lo reserva directamente en el stack, permitiendo que el bucle for sea extremadamente eficiente. No hay asignaciones en el heap.
cola_tareas es un std::deque. A diferencia de un std::vector, cuando haces push_front, el deque no necesita mover todos los elementos existentes a una nueva posición de memoria; simplemente asigna un nuevo bloque de memoria y actualiza su mapa interno de punteros. Esto hace que push_front sea $O(1)$. Sin embargo, ten cuidado: no puedes usar &cola_tareas[0] para obtener un puntero a todo el contenido, porque los elementos de un deque no están en un solo bloque contiguo.
Para historial, hemos usado std::list. Lo más potente aquí es auditoria.splice(...). Esta operación no copia los LogEntry de una lista a otra; simplemente toma los nodos de la lista historial y los “desengancha” de su secuencia para “engancharlos” en auditoria. Es una operación de punteros pura, extremadamente rápida para objetos pesados.
Finalmente, estados usa std::forward_list. Fíjate que no podemos llamar a estados.size(). En una lista simplemente enlazada, calcular el tamaño requeriría recorrer toda la lista ($O(n)$), y la filosofía de forward_list es evitar cualquier overhead innecesario, permitiendo al programador asumir la responsabilidad de contar si lo necesita.
El error frecuente
Un error clásico de nivel intermedio ocurre con std::deque cuando intentamos tratarlo como un array contiguo.
std::deque<int> d = {1, 2, 3, 4, 5};
int* ptr = &d[0];
// ERROR LÓGICO/UB:
// ptr[5] es indefinido. Aunque d tiene 5 elementos,
// el siguiente bloque de memoria de un deque no está
// garantizado después del primero.
Si intentas pasar &d[0] a una función que espera un buffer contiguo (como una API de red o std::span), el programa fallará o leerá basura en cuanto el índice supere el tamaño del bloque interno. Para APIs que requieren contigüidad absoluta, usa std::vector o std::array.
Si el rendimiento es crítico y el tamaño es fijo, el stack es tu mejor aliado.
N° 87