Comparativa de contenedores secuenciales: Deque, List, Forward_list y Array

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.

87

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio