std::array es un contenedor de tamaño fijo que encapsula un array nativo de C (T[N]) proporcionando una interfaz de la STL. A diferencia de un array convencional, es un tipo completo que puede ser pasado por valor, devuelto de funciones, y utilizado con algoritmos como std::sort o std::find. Al ser un aggregate [C++11], su inicialización es directa y su estructura es idéntica a la de un array de C, lo que garantiza un coste cero (zero-overhead) en términos de rendimiento y memoria.
En sistemas de tiempo real, sistemas embebidos o motores de alta frecuencia, el uso de la memoria dinámica (heap) mediante new o malloc es problemático porque el asignador de memoria no es determinista: el tiempo que tarda en encontrar un bloque libre puede variar, y la fragmentación puede hacer que una asignación falle incluso si hay memoria total disponible. Por ello, optamos por la programación stack-only (solo en pila). std::array es la herramienta fundamental aquí, ya que permite reservar buffers de tamaño conocido directamente en el stack.
Debes usar std::array cuando el tamaño de tus datos se conoce en tiempo de compilación y quieres evitar la latencia de la memoria dinámica. Si necesitas una interfaz que permita manipular subconjuntos de ese buffer sin copiar los datos, la solución es std::span [C++20], que actúa como una “vista” ligera compuesta únicamente por un puntero y un tamaño. Si necesitas una estructura que crezca pero que no toque el heap, el estándar nos ofrece std::pmr::monotonic_buffer_resource [C++17], que permite usar un buffer de std::array como un pool de memoria rápido y determinista.
El mayor riesgo es el desbordamiento de la pila (stack overflow). El stack suele ser muy pequeño (típicamente entre 1 MB y 8 MB dependiendo del OS). Si declaras un std::array<char, 10'000'000> dentro de una función, intentarás reservar 10 MB en la pila, lo que provocará un crash inmediato. Para buffers grandes, el heap es inevitable; para buffers pequeños y de tamaño conocido, el stack es tu mejor aliado.
#include <array>
#include <iostream>
#include <span>
#include <vector>
#include <memory_resource>
#include <cstddef>
#include <algorithm>
#include <cstdint>
// Representa un buffer de datos procesable sin usar el heap
struct DataProcessor {
// Usamos std::span para que la función sea agnóstica a la fuente
// (puede recibir std::array, std::vector, o un array nativo)
void process(std::span<const std::byte> data) const {
std::cout << "Procesando " << data.size() << " bytes...\n";
for (auto b : data) {
// Operación simulada
if (b == std::byte{0xFF}) {
std::cout << " ¡Flag especial detectado!\n";
break;
}
}
}
};
int main() {
// 1. Buffer estático en el stack mediante std::array
std::array<std::byte, 64> raw_buffer = {
std::byte{0x01}, std::byte{0x02}, std::byte{0xFF}, std::byte{0x04}
};
// 2. Uso de std::span para crear una vista sobre el buffer
// No hay copia de datos, solo una estructura de puntero + tamaño
std::span<const std::byte> view = raw_buffer;
DataProcessor processor;
processor.process(view);
// 3. Técnica de "Heap-less dynamic allocation" usando std::pmr
// Simulamos un pool de memoria pequeño dentro del stack
std::array<std::byte, 1024> pool_storage;
// El monotonic_buffer_resource es un "bump allocator" ultra rápido.
// No libera memoria individualmente, solo resetea el puntero al final.
std::pmr::monotonic_buffer_resource resource(pool_storage.data(), pool_storage.size());
// Este vector parece dinámico, pero su memoria reside en pool_storage
// Evitamos llamadas al sistema y bloqueos por el mutex del heap global.
std::pmr::vector<int> stack_vec(&resource);
stack_vec.push_back(10);
stack_vec.push_back(20);
stack_vec.push_back(30);
std::cout << "Valores en el vector (en stack): ";
for (int n : stack_vec) std::cout << n << " ";
std::cout << "\n";
return 0;
}
Desglose del ejemplo
En el código anterior, raw_buffer es un std::array que reside íntegramente en el stack de main. Al pasarlo a process, no estamos copiando los 64 bytes, sino que std::span<const std::byte> crea una vista que contiene un puntero a raw_buffer.data() y un entero con el tamaño. Esto es lo que llamamos una abstracción de coste cero.
La parte más avanzada es el uso de std::pmr::monotonic_buffer_resource. Fíjate en pool_storage, un std::array de 1024 bytes. Le pasamos este array al recurso de memoria. Cuando creamos std::pmr::vector<int> stack_vec(&resource), el vector no llama a operator new para pedir memoria al sistema operativo. En su lugar, le pide memoria al resource, que simplemente desplaza un puntero interno dentro de pool_storage (una operación de un solo ciclo de CPU). Esto es extremadamente determinista y rápido, ideal para bucles de procesamiento donde necesitas contenedores que crezcan pero no quieres la latencia del heap.
El error frecuente
Un error común al intentar optimizar es el uso de alloca() para asignar memoria en el stack de forma “dinámica”. Aunque alloca() existe en C, es peligroso en C++ porque no se puede usar con RAII: si una función lanza una excepción después de llamar a alloca(), la memoria en el stack no se libera correctamente (aunque el stack se limpie, los destructores de objetos construidos en esa memoria no se llamarán), y es muy difícil de depurar.
Otro error crítico es el tamaño desmedrado. Fíjate en este error:
void error_function() {
// ERROR: Intentar alojar 80MB en el stack
// Esto causará un Segmentation Fault (Stack Overflow) casi seguro.
std::array<double, 10'000'000> massive_array;
massive_array[0] = 1.0;
}
Si necesitas manejar datos grandes, usa std::vector (que usa el heap) o, si quieres control manual sobre la ubicación, usa std::unique_ptr<T[]> para asegurar que la memoria se libere mediante RAII.
N° 141