1¿Cómo funcionan los deleters personalizados en smart pointers, y cuándo son útiles para la interoperabilidad C/C++?
Un deleter personalizado es lógica de limpieza invocable que usa un smart pointer en lugar de la operación delete predeterminada cuando el puntero libera su recurso poseído. Es útil para la interoperabilidad C/C++ cuando un recurso debe liberarse con una función específica como fclose, close, curl_easy_cleanup, SSL_free, free o una función destroy de biblioteca. Para unique_ptr, el tipo del deleter forma parte del tipo de unique_ptr y puede afectar a su tamaño; los deleters sin estado pueden optimizarse hasta no ocupar espacio, mientras que los deleters puntero a función o con estado añaden almacenamiento. Para shared_ptr, el deleter se almacena en el bloque de control y se ejecuta cuando el último propietario fuerte libera el objeto. Los deleters personalizados permiten que los recursos de C participen en RAII de forma segura.
2Explica weak_ptr y la mecánica del bloque de control de shared_ptr, incluidos ciclos, enable_shared_from_this y costes de conteo de referencias.
Un shared_ptr gestiona la propiedad compartida mediante un bloque de control que contiene contadores de referencias fuertes y débiles, además de información de limpieza como el deleter y el asignador. Copiar o destruir objetos shared_ptr incrementa o decrementa el contador fuerte, normalmente con operaciones atómicas, por lo que objetos shared_ptr separados pueden manipularse de forma segura entre hilos, pero cada actualización del contador de referencias tiene coste. Cuando el contador fuerte llega a cero, el objeto gestionado se destruye; el bloque de control permanece hasta que las referencias débiles también desaparecen. weak_ptr apunta al mismo bloque de control sin extender la vida útil del objeto; lock() devuelve un shared_ptr si el objeto sigue vivo y un shared_ptr vacío en caso contrario. Los ciclos formados solo por shared_ptr fugan memoria porque los contadores fuertes nunca llegan a cero, así que weak_ptr se usa para punteros inversos o enlaces observadores. enable_shared_from_this permite que un objeto que ya está poseído por shared_ptr cree un nuevo shared_ptr a sí mismo usando el bloque de control existente, evitando bloques de control separados peligrosos. Los costes de conteo de referencias incluyen incrementos/decrementos atómicos, contención de caché, asignación del bloque de control y sobrecarga en rutas críticas.
3Explica las categorías de valor de C++ y el perfect forwarding, y por qué importan para APIs genéricas eficientes.
Las categorías de valor de C++ describen expresiones: los lvalues tienen identidad y pueden referenciarse después de la expresión; los prvalues son rvalues puros, como muchos temporales/valores calculados; los xvalues son objetos en expiración cuyos recursos pueden reutilizarse. El perfect forwarding es la técnica de plantillas que consiste en recibir una referencia de reenvío, normalmente T&& donde T se deduce, y reenviar con std::forward<T>(arg) para preservar la categoría de valor del llamador: los lvalues siguen siendo lvalues y los rvalues siguen siendo rvalues. Las reglas de colapso de referencias hacen esto posible. Importa para APIs backend genéricas porque envoltorios, factorías, despachadores y funciones estilo emplace pueden evitar copias innecesarias y preservar la selección de sobrecargas y el comportamiento de movimiento.
4Compara auto, decltype, decltype(auto) y la deducción de argumentos de plantilla en código backend común.
auto usa una deducción similar a la de plantillas para variables: auto simple normalmente descarta referencias y const de nivel superior salvo que la declaración las solicite, como auto&, const auto& o auto&&. decltype(expr) inspecciona de forma más exacta el tipo declarado o el tipo de la expresión: una id-expression sin paréntesis da el tipo declarado, mientras que otras expresiones lvalue producen T&, los xvalues producen T&& y los prvalues producen T. decltype(auto) deduce usando las reglas de decltype, a menudo para tipos de retorno cuando deben preservarse referencias. La deducción de argumentos de plantilla es similar a auto, pero depende de la forma del parámetro, como T, T&, const T& o T&&, y tiene sus propias reglas. Los inicializadores entre llaves son una diferencia común: auto x = {1,2} deduce std::initializer_list<int>, mientras que un parámetro de plantilla simple generalmente no puede deducir T a partir de un inicializador entre llaves desnudo salvo que el parámetro espere un initializer_list u otro tipo adecuado.
5Compara std::variant con polimorfismo basado en herencia para modelar mensajes o eventos heterogéneos.
std::variant es un tipo valor que contiene exactamente una alternativa de un conjunto fijo y cerrado de tipos y suele manejarse con std::visit o consultas explícitas de tipo. Es útil para mensajes de protocolo o eventos cuando el conjunto de clases de mensajes es conocido y quieres manejo con seguridad de tipos sin despacho virtual y a menudo sin asignación en heap por objeto. El polimorfismo basado en herencia usa una clase base y funciones virtuales para despachar mediante una interfaz común; es mejor cuando el conjunto de tipos de mensajes derivados está abierto, es extensible de forma independiente, tipo plugin, u oculto detrás de una interfaz estable. Variant favorece tipos suma cerrados, localidad y comprobación en tiempo de compilación; la herencia favorece extensibilidad, polimorfismo en tiempo de ejecución y diseño basado en interfaces.
6¿Qué es el comportamiento indefinido en C++ y cómo puede aparecer en incidentes de producción backend?
El comportamiento indefinido es un comportamiento para el cual el estándar de C++ no impone ningún requisito después de que ocurre una operación inválida. El programa puede parecer funcionar, fallar, corromper datos, exponer fallos de seguridad u optimizarse hasta producir un comportamiento sorprendente. Los compiladores asumen que el UB no ocurre y optimizan basándose en esa suposición, por lo que los problemas pueden aparecer solo en compilaciones de lanzamiento o bajo tráfico de producción. Los incidentes backend pueden provenir de punteros/referencias colgantes, use-after-free, violaciones de ciclo de vida de objetos, acceso fuera de límites, overflow de enteros con signo, data races, casts inválidos, lecturas no inicializadas, dobles free o violaciones de strict aliasing. La mitigación incluye RAII y un diseño claro de propiedad/ciclo de vida, abstracciones más seguras y comprobaciones de límites, pruebas/fuzzing, revisión de código, análisis estático y sanitizers como ASan, UBSan y TSan.
7Diferencia el comportamiento indefinido, no especificado y definido por la implementación con ejemplos relevantes para backend.
El comportamiento indefinido significa que el estándar de C++ no impone requisitos; los resultados pueden incluir fallos, corrupción de datos, errores de seguridad o compilaciones incorrectas dependientes del optimizador. Algunos ejemplos son el acceso fuera de límites, el uso después de liberar memoria, el desbordamiento de enteros con signo y las carreras de datos. El comportamiento no especificado significa que el estándar permite más de un resultado y la implementación no tiene que documentar cuál se elige; un ejemplo común es el orden de evaluación de muchos argumentos de función, por lo que el código no debería depender de que los efectos secundarios se evalúen en un orden particular. El comportamiento definido por la implementación significa que la implementación debe elegir y documentar el comportamiento; algunos ejemplos son si `char` simple tiene signo, los tamaños/rangos de algunos tipos fundamentales dentro de los límites del estándar y cierto comportamiento del desplazamiento a la derecha con signo. En código backend, el UB es un riesgo de corrección/seguridad, mientras que el comportamiento no especificado y el definido por la implementación son riesgos de portabilidad que deben evitarse o aislarse en lógica de protocolos, almacenamiento y multiplataforma.
8Explica los niveles de seguridad ante excepciones y cómo noexcept afecta a las operaciones de movimiento, los contenedores y la generación de código.
Los niveles de seguridad ante excepciones describen qué sigue siendo cierto si una operación lanza. La garantía básica significa que se preservan los invariantes y no se filtran recursos, aunque el estado puede haber cambiado. La garantía fuerte significa semántica de commit-or-rollback: en caso de fallo, el estado observable no cambia. La garantía nothrow significa que la operación no lanza. Técnicas comunes incluyen RAII, realizar trabajo sobre temporales, copy-and-swap y ordenar las mutaciones para que el commit ocurra solo después de que el trabajo que puede lanzar haya tenido éxito. `noexcept` es un contrato: si una función `noexcept` lanza, se llama a `std::terminate`. También afecta al código genérico y a los contenedores: por ejemplo, durante una realocación `std::vector` puede mover elementos cuando el constructor de movimiento es `noexcept`; de lo contrario puede copiarlos, a menudo mediante `std::move_if_noexcept`, para preservar garantías de excepción. `noexcept` también puede ayudar a la generación de código u optimización al reducir las rutas necesarias de propagación de excepciones, pero solo cuando la promesa es correcta.
9Explica std::expected o tipos de resultado equivalentes como alternativas a las excepciones para operaciones falibles.
`std::expected<T, E>` representa o bien un valor exitoso `T` o un error explícito `E`, haciendo que el fallo forme parte del tipo de retorno de la función en lugar de depender de la propagación de excepciones. Es útil para operaciones falibles donde los errores son esperados y los llamadores deberían manejarlos localmente, como parsing, validación, llamadas de red, búsquedas en almacenamiento y operaciones backend reintentables. El tipo de error debería llevar información estructurada como un código de error, categoría, reintentabilidad, mensaje o mapeo HTTP/RPC. Los tipos expected/outcome se componen comprobando y propagando errores, y en APIs de estilo C++23 pueden usar operaciones monádicas como `and_then`, `transform` y `or_else` para encadenar pasos sin condicionales profundamente anidados. En comparación con las excepciones, hacen explícitos el flujo de control y los contratos de API y funcionan bien a través de límites sin excepciones o de ABI; las excepciones aún pueden ser apropiadas para fallos raros, no locales o verdaderamente excepcionales según la política del proyecto.
10Describe las reglas de vida de objetos para objetos automáticos, dinámicos, temporales y referenciados de forma asíncrona.
Los objetos automáticos viven desde su construcción hasta el final de su ámbito; los punteros o referencias a ellos quedan colgantes después de que ese ámbito termina. Los objetos dinámicos viven desde la asignación/construcción hasta que se destruyen explícitamente o hasta que un objeto propietario los destruye; los punteros sin procesar y las referencias no extienden la vida. Los temporales normalmente viven hasta el final de la expresión completa, con reglas específicas de extensión de vida cuando se enlazan a referencias, pero no en todos los usos. El trabajo asíncrono como callbacks, hilos, temporizadores o corrutinas puede ejecutarse después de que el objeto referenciado haya sido destruido, por lo que las capturas y referencias almacenadas necesitan gestión explícita de vida para evitar referencias colgantes y use-after-free.
11Describe los problemas de orden de inicialización estática y cómo constinit, los magic statics y la inyección de dependencias los mitigan.
Los problemas de orden de inicialización estática ocurren porque los objetos de ámbito de namespace o estáticos inicializados dinámicamente en distintas unidades de traducción tienen un orden relativo de inicialización no especificado. Un global puede usar otro antes de que haya sido construido; el orden de destrucción también puede crear problemas similares al apagado. constinit fuerza a que una variable estática o thread-local tenga inicialización estática/constante o el programa queda mal formado, evitando para esa variable la dependencia del orden de inicialización dinámica. Los magic statics, o estáticos locales a función, se inicializan en el primer uso y son seguros para hilos desde C++11. La inyección de dependencias evita dependencias globales ocultas construyendo objetos en un orden controlado y pasando dependencias explícitamente.
12¿Qué son copy elision, NRVO y RVO, y cuándo puedes depender de ellos?
Copy elision significa construir un objeto directamente en su destino final en lugar de crear un temporal separado y copiarlo o moverlo. RVO normalmente se refiere a devolver un temporal sin nombre o prvalue, como return T{}; en C++17 y posteriores muchos de estos casos son obligatorios porque el prvalue inicializa directamente el objeto resultado. NRVO es devolver un objeto local con nombre, como return x; el compilador tiene permitido construir ese local directamente en el slot de retorno del llamador, pero esto no está garantizado. Puedes depender de la elisión obligatoria de prvalues de C++17 en los casos especificados, pero no de que NRVO ocurra siempre; evita std::move sobre un valor de retorno local con nombre porque puede impedir NRVO.
13¿Cómo afectan los requisitos de comparadores, igualdad y funciones hash a la corrección de los contenedores asociativos?
Los contenedores asociativos dependen de la comparación, la igualdad y el hashing para definir la identidad de las claves y mantener sus invariantes internos. Los contenedores ordenados como std::map requieren que el comparador imponga un orden débil estricto; las claves se consideran equivalentes cuando ninguna compara como menor que la otra, no necesariamente mediante operator==. Los contenedores no ordenados requieren que la igualdad sea una relación de equivalencia, y cualesquiera dos claves consideradas iguales deben producir el mismo valor hash. Las claves no deben mutarse mientras están almacenadas de una forma que cambie su ordenación, igualdad o hash, porque eso puede hacer incorrectos el comportamiento de búsqueda, borrado y unicidad.
14Explica la búsqueda heterogénea en contenedores asociativos ordenados y no ordenados.
La búsqueda heterogénea permite buscar en un contenedor asociativo usando un tipo distinto del tipo de clave, evitando construir claves temporales. En contenedores ordenados, esto requiere un comparador transparente, por ejemplo std::less<> o un comparador personalizado con un marcador is_transparent. En contenedores no ordenados, requiere tanto funciones hash como funciones de igualdad transparentes que puedan manejar de forma coherente el tipo de clave y el tipo de búsqueda. Un ejemplo común en backend es un contenedor con clave std::string que puede buscarse con std::string_view o const char* sin asignar una std::string temporal.
15Explica el modelo de memoria de C++: carreras de datos, happens-before y garantías de sincronización.
El modelo de memoria de C++ define cuándo las operaciones en distintos hilos están ordenadas y cuándo las escrituras se vuelven visibles. Una carrera de datos ocurre cuando dos hilos acceden concurrentemente a la misma ubicación de memoria, al menos un acceso es una escritura, y los accesos no están ordenados por happens-before ni se hacen seguros de otra forma mediante operaciones atómicas; una carrera de datos causa comportamiento indefinido. Happens-before es la relación de orden que hace visibles los efectos secundarios anteriores a operaciones posteriores. Las operaciones de sincronización crean este orden: por ejemplo, desbloquear un mutex synchronizes-with un lock exitoso posterior del mismo mutex, y operaciones atómicas release/acquire adecuadas pueden sincronizar entre hilos. Los programas correctos usan mutexes, atómicos u otra sincronización para establecer happens-before para estado compartido.