Cuando marcas una función con el especificador noexcept [C++11], le estás haciendo una promesa formal al compilador y al runtime: esta función no lanzará ninguna excepción. Si la expresión dentro de noexcept(expr) es verdadera, el compilador sabe que no tiene que generar la infraestructura necesaria para la propagación de excepciones en ese punto, lo que permite optimizaciones de tamaño y velocidad. El operador noexcept es una herramienta de introspección que devuelve un valor booleano en tiempo de compilación, permitiéndote decidir si una función es segura para ser movida o copiada sin riesgo.
Esta promesa es vital por el mecanismo de stack unwinding (desenrollado de la pila). Cuando se lanza una excepción, el runtime recorre la pila de llamadas destruyendo automáticamente todos los objetos locales (usando RAII) en el orden inverso a su construcción. Si el runtime encuentra una función marcada como noexcept que intenta lanzar una excepción, el sistema no puede continuar con el desenrollado de la pila de forma segura y llama inmediatamente a std::terminate(). Por tanto, si rompes la promesa, el programa muere sin siquiera intentar buscar un bloque catch cercano.
Debes usar noexcept siempre que sea posible en constructores de movimiento, operadores de asignación de movimiento y funciones de intercambio (swap), ya que esto es lo que permite que contenedores como std::vector optimicen sus redimensionamientos. Si no lo haces, la biblioteca estándar, ante la duda de si el movimiento podría lanzar una excepción, optará por la vía segura: realizar copias en lugar de movimientos, lo que destruye el rendimiento en aplicaciones de alto rendimiento.
───
#include <iostream>
#include <vector>
#include <exception>
#include <type_traits>
#include <utility>
#include <string>
// Una clase que gestiona un recurso pesado para demostrar la importancia de noexcept
class Buffer {
int* data;
size_t size;
public:
explicit Buffer(size_t s) : size(s), data(new int[s]) {
std::cout << "[Constructor] Buffer de tamaño " << size << " creado\n";
}
// El destructor es implicitamente noexcept desde C++11.
// Nunca debe lanzar excepciones.
~Buffer() {
delete[] data;
std::cout << "[Destructor] Buffer destruido\n";
}
// Prohibir la copia para forzar el uso de movimiento
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
// Constructor de movimiento: CRUCIAL marcarlo como noexcept
// Si no fuera noexcept, std::vector no podría usarlo durante reallocations.
Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
std::cout << "[Move Constructor] Buffer movido\n";
}
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
size_t get_size() const noexcept { return size; }
};
// Función que demuestra el operador noexcept y el especificador condicional
template <typename T>
void check_capabilities() noexcept(std::is_nothrow_move_constructible_v<T>) {
// Esta función tiene la misma garantía de excepción que el constructor de T
std::cout << "Capacidad de movimiento segura confirmada.\n";
}
// Función para demostrar la captura y relanzamiento de excepciones
void exception_handler_demo() {
std::exception_ptr eptr = nullptr;
try {
std::cout << "[Demo] Lanzando excepción...\n";
throw std::runtime_error("Error crítico en la lógica de negocio");
} catch (...) {
// std::current_exception() captura la excepción actual como un std::exception_ptr
eptr = std::current_exception();
}
if (eptr) {
std::cout << "[Demo] Excepción capturada y guardada en exception_ptr\n";
try {
// std::rethrow_exception permite relanzar la excepción fuera del bloque actual
std::rethrow_exception(eptr);
} catch (const std::exception& e) {
std::cout << "[Demo] Excepción relanzada: " << e.what() << "\n";
}
}
}
int main() {
// 1. Demostración de eficiencia en std::vector
// Al tener el constructor de Buffer como noexcept, vector hará 'move' en el resize.
std::vector<Buffer> vec;
vec.reserve(1);
vec.emplace_back(100);
std::cout << "[Vector] Redimensionando vector...\n";
vec.emplace_back(200); // Aquí ocurre el resize y el movimiento eficiente
// 2. Demostración de introspección de tipos
check_capabilities<Buffer>();
// 3. Demostración de gestión de excepciones
exception_handler_demo();
return 0;
}
Este programa ilustra los tres pilares de noexcept. Primero, la clase Buffer implementa un constructor de movimiento marcado con noexcept. Esto es fundamental: cuando el std::vector en el main necesita redimensionarse para alojar el segundo elemento, utiliza std::move_if_noexcept. Como nuestro constructor es noexcept, el vector mueve los elementos existentes en lugar de copiarlos, lo cual es una operación $O(1)$ de punteros en lugar de una $O(N)$ de memoria. Si quitáramos noexcept del constructor de Buffer, el vector realizaría copias, pero como la copia está borrada (delete), el código simplemente no compilaría o fallaría en el intento de asegurar la garantía de excepción de la STL.
Segundo, la función check_capabilities utiliza el operador noexcept dentro de un especificador condicional. La expresión std::is_nothrow_move_constructible_v<T> devuelve un booleano a tiempo de compilación, lo que permite que la firma de la función se adapte dinámicamente a la seguridad de la clase que se le pase.
Tercero, la función exception_handler_demo utiliza std::current_exception() para capturar el estado de una excepción y std::rethrow_exception para relanzarla. Esto es esencial cuando trabajas con hilos (std::thread) o cuando necesitas delegar el manejo de errores a un nivel superior del sistema, manteniendo el contexto de la excepción original.
El error frecuente
Un error clásico y catastrófico es lanzar una excepción dentro de un destructor durante un proceso de stack unwinding. Si una excepción se está propagando porque otra excepción ya ocurrió (por ejemplo, dentro de un bloque try que ya falló), y durante la destrucción de los objetos locales en la pila, uno de esos destructores lanza otra excepción, el runtime no tiene forma de decidir qué error tiene prioridad. En ese momento, el estándar dicta que se debe llamar a std::terminate().
struct BadDestructor {
~BadDestructor() {
throw std::runtime_error("Error en destructor"); // CRASH INMEDIATO
}
};
void danger() {
BadDestructor b;
throw std::runtime_error("Error original");
}
Si intentas compilar esto, el programa terminará abruptamente. Aunque los destructores son noexcept por defecto desde C++11, lanzar una excepción en ellos es una violación directa del contrato que resulta en una terminación inmediata del proceso, sin posibilidad de captura.
N° 66