Cómo borrar un elemento de std::vector por índice: mirando bajo el capó
\n\n¡Hola, colega! Si alguna vez has programado en C++ y has trabajado con arreglos dinámicos, seguro te has encontrado con la pregunta: "How do I erase an element from std::vector<> by index?" — esta es una de las consultas más populares en Stack Overflow, y con razón. A primera vista, la tarea parece trivial: llamas a erase y listo. Pero, ¿qué sucede realmente en la memoria? ¿Por qué eliminar un elemento del medio del vector es costoso? ¿Y cómo evitar pisar el rastrillo con los iteradores?
En este artículo no solo mostraremos cómo escribir v.erase(v.begin() + index). Analizaremos cómo funciona esta operación bajo el capó, qué estructuras de datos están involucradas y cómo elegir la forma óptima de eliminar. ¡Vamos allá!
1. std::vector: ¿qué clase de bestia es esta?
\n\nAntes de eliminar, recordemos cómo está estructurado std::vector. Es un arreglo dinámico que almacena elementos en un área de memoria contigua. Tiene tres punteros (o iteradores) clave:
- \n
begin— el inicio de la memoria asignada; \n end— el final de la memoria ocupada (después del último elemento); \n capacity— el final de la memoria asignada (el límite hasta el cual puede crecer sin reasignación). \n
Visualmente se ve así:
\n[0] [1] [2] [3] [4] ... [capacity-1]\n ^ ^\n begin end\n\n\nCuando eliminas un elemento del medio, el vector no simplemente lo "descarta" — físicamente desplaza todos los elementos siguientes hacia la izquierda. Este es el principal "dolor" de la operación erase.
2. Eliminación por índice: sintaxis básica
\n\nLa forma más sencilla de eliminar un elemento por índice es usar el método erase con un iterador:
#include <vector>\n#include <iostream>\n\nint main() {\n std::vector<int> v = {10, 20, 30, 40, 50};\n int index = 2; // queremos eliminar el tercer elemento (30)\n\n // Verificamos que el índice esté dentro del rango\n if (index < v.size()) {\n v.erase(v.begin() + index);\n }\n\n for (int x : v) std::cout << x << " ";\n // Salida: 10 20 40 50\n return 0;\n}\n\n\nSimple, ¿verdad? Pero, ¿qué pasa si necesitas eliminar varios elementos consecutivos? Para eso existe la sobrecarga erase(first, last):
v.erase(v.begin() + 1, v.begin() + 3); // eliminará los elementos con índices 1 y 2\n\n\n3. ¿Y qué hay bajo el capó? Análisis detallado
\n\nAhora viene lo interesante. Cuando llamas a v.erase(iterator), internamente sucede lo siguiente:
- \n
- Verificación del iterador (en modo debug) — nos aseguramos de que sea válido y apunte a un elemento del vector. \n
- Desplazamiento de elementos — todos los elementos que están después del eliminado se copian (o mueven, si el tipo soporta move semantics) una posición hacia la izquierda. \n
- Reducción del tamaño — el contador interno
sizese reduce en 1. \n - Destructor — para el elemento "final" (que ahora está duplicado) se llama al destructor (si el tipo no es trivial). \n
Veamos una implementación simplificada (pseudocódigo):
\n\ntemplate<typename T>\ntypename std::vector<T>::iterator\nvector<T>::erase(iterator pos) {\n // 1. Desplazamos todos los elementos después de pos una posición a la izquierda\n for (auto it = pos; it != end() - 1; ++it) {\n *it = std::move(*(it + 1));\n }\n // 2. Reducimos el tamaño\n --size;\n // 3. Llamamos al destructor para el último elemento\n allocator.destroy(end()); // end() ahora apunta al que era el último elemento\n return pos; // devolvemos un iterador al elemento siguiente al eliminado\n}\n\n\nObserva el std::move — el C++ moderno intenta evitar