Swift y C++: interoperar, generalizar, suceder
La interoperabilidad bidireccional sin puentes, las plantillas frente a los genéricos con restricciones verificadas en la definición, y el argumento con el que Apple presenta a Swift como sucesor de C++ sin pedir reescrituras. También lo que C++ sigue haciendo mejor.
Ningún otro lenguaje ocupa para Swift la posición que ocupa C++. Con Rust compite en ideas, con Kotlin en parecido, con Go en filosofía; con C++ mantiene una relación de sucesión declarada: Apple no lo presenta como alternativa entre pares, sino como el lenguaje al que quiere migrar código que hoy se escribe en C++, y lo hace sin exigir reescrituras porque construyó una interoperabilidad bidireccional que permite mezclar ambos en el mismo módulo. Esa ambición obliga a Swift a competir en el terreno de C++, no en el suyo: rendimiento sin sorpresas, control sobre la representación, cero recolector y abstracciones que no cuesten nada. La comparación honesta reconoce cuánto ha avanzado y cuánto le queda.
- Usar la interoperabilidad bidireccional y entender por qué las proyecciones son su punto débil.
- Contrastar las plantillas instanciadas con los genéricos verificados en la definición.
- Reconstruir el argumento de sucesión de Apple y evaluar su solidez técnica.
- Delimitar los dominios donde
C++sigue siendo la elección correcta y por qué motivos.
Interoperar sin puente
Desde Swift 5.9 la interoperabilidad con C++ es directa y en ambos sentidos: no hay generación de envoltorios, no hay capa de C en medio, no hay coste de marshalling. Se activa un modo de compilación, se importa el módulo y los tipos aparecen del otro lado con una traducción bastante literal.
// geometria.h
#include <vector>
struct Punto { double x, y; double norma() const; };
std::vector<Punto> muestrear(int n);
import Geometria
import CxxStdlib
let puntos = muestrear(1000) // std::vector importado como secuencia
for p in puntos where p.norma() > 1 { procesar(p) }
let suma = puntos.map(\.x).reduce(0, +)
La traducción por defecto sigue una regla razonable: una clase o estructura de C++ se importa como tipo de valor de Swift, con su constructor de copia y su destructor respetados. Cuando el tipo tiene semántica de referencia hay que decirlo con anotaciones —referencia compartida, referencia inmortal, tipo no copiable— que se colocan en la cabecera y guían al importador. La dirección contraria también funciona: las clases, estructuras y funciones de Swift marcadas como públicas se exponen a C++ con una cabecera generada.
Las anotaciones son el vocabulario con el que el autor de la cabecera le cuenta al importador lo que el sistema de tipos de C++ no expresa: si el objeto se gestiona con conteo de referencias propio, si vive para siempre, si no debe copiarse nunca.
// sesion.h
class SWIFT_SHARED_REFERENCE(retener, liberar) Sesion {
public:
void enviar(const char *mensaje);
};
void retener(Sesion *s);
void liberar(Sesion *s);
let s = crearSesion() // llega como referencia, no como valor copiado
s.enviar("hola") // el importador inserta retener y liberar
Sin esa anotación el importador habría tratado la clase como un valor y habría copiado el objeto en cada paso, con consecuencias que van del rendimiento degradado a la corrupción silenciosa. La regla operativa es simple y poco intuitiva: la interoperabilidad no es un interruptor que se activa, es un contrato que alguien tiene que escribir en las cabeceras.
El punto débil no es la traducción de tipos sino la de la vida útil. C++ no lleva en su sistema de tipos ninguna información sobre cuánto vive lo que devuelve una función, así que todo lo que sea una proyección —un iterador, una referencia, una vista sobre una cadena, un puntero a un elemento de un contenedor— llega a Swift sin garantías, y el compilador no puede impedir que sobreviva al objeto que lo produjo. Por eso el trabajo reciente se concentra en anotar esas dependencias y en marcar explícitamente qué construcciones importadas son inseguras, de forma que un proyecto pueda exigir que cada uso de una de ellas quede señalado en el código.
Un módulo de Swift que llama a C++ no es más seguro que el C++ que llama. La garantía de memoria de Swift termina en la frontera, y esa frontera es exactamente donde conviene concentrar las revisiones y las anotaciones de vida útil.
Plantillas frente a genéricos
Las dos construcciones parecen resolver lo mismo y responden a modelos opuestos de cuándo comprobar la corrección.
Una plantilla de C++ no se comprueba al definirse: es un patrón que se instancia con cada tipo concreto, y solo entonces el compilador verifica si las operaciones usadas existen. Eso le da una expresividad enorme —cualquier expresión válida para el tipo sirve, sin declarar nada— al precio de que un error se manifiesta lejos de donde está la causa, en forma de mensajes de instanciación anidada legendarios por su longitud. Los conceptos de C++ 20 mejoran mucho el diagnóstico al permitir declarar los requisitos, pero no cambian el modelo: siguen comprobándose contra la instanciación.
Un genérico de Swift se comprueba una sola vez, en la definición, contra las restricciones declaradas. Si el cuerpo usa una operación que la restricción no promete, el error aparece ahí, señalando el requisito que falta.
func maximo<C: Collection>(_ c: C) -> C.Element? where C.Element: Comparable {
c.reduce(nil) { acumulado, actual in
guard let a = acumulado else { return actual }
return actual > a ? actual : a
}
}
La consecuencia menos obvia es sobre la compilación separada. Una plantilla vive en la cabecera y se recompila en cada unidad de traducción que la use; un genérico de Swift se compila una vez a código que opera sobre tablas de testigos, y solo se especializa por tipo cuando el optimizador lo considera rentable o cuando se marca como susceptible de incorporarse en línea. C++ obtiene así rendimiento máximo con crecimiento del binario y del tiempo de compilación garantizados; Swift obtiene binarios menores y compilación más contenida a costa de una indirección que a veces sobrevive a la optimización. También pierde expresividad: no hay especialización parcial, y los parámetros que no son tipos han llegado tarde y de forma limitada.
El argumento de la sucesión
La tesis de Apple se apoya en tres patas y conviene evaluarla pata por pata.
La primera es empírica: en bases de código grandes escritas en C y C++, alrededor de dos tercios de las vulnerabilidades graves tienen su origen en la memoria, y esa proporción se ha mantenido estable durante décadas pese a las herramientas, las revisiones y los sanitizadores. El argumento no es que los programadores de C++ sean descuidados, sino que el modelo del lenguaje no permite demostrar la ausencia de esos fallos.
La segunda es de migración: dado que C++ no puede ser reescrito, cualquier sucesor creíble debe permitir adopción incremental, módulo a módulo, sin fronteras costosas. Ahí es donde la interoperabilidad directa deja de ser una comodidad y se convierte en el núcleo de la estrategia, y donde Swift se diferencia de propuestas que exigen puentes explícitos.
La tercera es de alcance: para suceder a C++ no basta con ser seguro, hay que poder bajar donde C++ baja. De ahí Swift embebido, que elimina el metadato de tipos, el reflejo y las excepciones para producir binarios diminutos sin asignación dinámica obligatoria, apuntando a firmware y microcontroladores; de ahí los tipos no copiables, Span y las dependencias de vida útil; y de ahí que Apple haya empezado a usar el lenguaje en componentes de bajo nivel donde antes solo había C.
Hay un matiz sobre la primera pata que conviene no perder: la seguridad de memoria no es lo mismo que la corrección. Un programa de Swift puede tener errores lógicos, fugas por ciclos, bloqueos mutuos y una arquitectura pésima. Lo que la garantía elimina es una familia concreta de fallos —lectura de memoria liberada, desbordamiento de búfer, puntero colgante— que resulta ser, casualmente, la familia que domina los informes de vulnerabilidades explotables. El argumento de sucesión es estadístico, no absoluto, y presentarlo como otra cosa desacredita a quien lo usa.
Las objeciones serias también existen. Swift sigue arrastrando un tiempo de ejecución y ARC en su configuración normal, su presencia fuera de las plataformas de Apple es modesta comparada con la ubicuidad de C++, y en el propio nicho de sucesión tiene competencia consolidada: Rust ocupa ya el espacio de los sistemas, y dentro del comité de C++ conviven propuestas para dotar al lenguaje de garantías por perfiles. La sucesión, si ocurre, será por dominios y no por decreto.
Dónde C++ sigue ganando
Hay razones técnicas, no inerciales, para seguir eligiéndolo. La portabilidad es de otra escala: hay compiladores de C++ para arquitecturas donde Swift no existirá jamás. El control es total —asignadores propios, construcción en memoria dada, disposición exacta de los datos, cálculo en tiempo de compilación arbitrariamente complejo— y no hay ninguna operación implícita equivalente a los contadores de ARC en los caminos calientes. El ecosistema acumula cuatro décadas de bibliotecas numéricas, gráficas y de simulación que nadie va a portar. Y en GPU, motores de juego y computación de alto rendimiento, la combinación de metaprogramación y ausencia de tiempo de ejecución sigue sin rival.
flowchart LR C[Codigo C++ existente] -->|interoperabilidad directa| S[Modulo nuevo en Swift] S -->|cabecera generada| C C --> P1[Plantillas comprobadas al instanciar] S --> P2[Genericos comprobados en la definicion] S --> V[Seguridad de memoria verificada] C --> U[Proyecciones sin vida util conocida] U -.->|frontera insegura| V style V fill:#a6e3a1,color:#11111b style U fill:#f38ba8,color:#11111b
Cuándo se comprueba
La plantilla se verifica al instanciarse; el genérico, al definirse. De ahí vienen los diagnósticos y los binarios.
La frontera manda
Interoperar no propaga la seguridad. Las proyecciones importadas son el punto exacto donde se pierde.
Bajar para suceder
Swift embebido, tipos no copiables y Span existen para poder competir donde ARC no es admisible.
La historia de la informática está llena de lenguajes técnicamente superiores a C++ que nunca lo desplazaron un milímetro, y su fracaso comparte una causa: plantearon la sucesión como sustitución. Exigían reescribir, o al menos cruzar una frontera con coste, y ese requisito los condenó, porque el activo real de C++ no es su sintaxis ni su rendimiento sino cuarenta años de código que funciona, que nadie entiende del todo y que nadie va a tocar. Cualquier sucesor serio debe aceptar, como premisa no negociable, que su competidor no va a desaparecer y que la unidad de migración no es el programa sino el módulo, quizá la función. Esto convierte la interoperabilidad, que suele tratarse como detalle de implementación, en la propiedad central del diseño: determina si la adopción puede ser gradual, y por tanto si puede haber adopción. Pero la premisa tiene una consecuencia incómoda que rara vez se enuncia con claridad, y es que un sistema mixto no hereda las garantías del componente más fuerte sino las del más débil. Un módulo verificado que consume una proyección sin vida útil conocida es tan frágil como el C++ que lo alimenta, y la única defensa honesta consiste en hacer visible esa frontera: anotar las dependencias, marcar lo inseguro, exigir que cada cruce quede señalado en el código para que la revisión pueda concentrarse donde de verdad importa. La enseñanza transferible excede a estos dos lenguajes y se aplica a toda migración —de esquema, de servicio, de plataforma—: mientras dure la convivencia, y siempre dura más de lo previsto, el sistema tiene la solidez de su interfaz más débil, y el trabajo de ingeniería no consiste en celebrar la parte nueva sino en instrumentar la costura. Quien mide el progreso de una migración por el porcentaje de código migrado se engaña; el número que importa es cuántas fronteras quedan sin anotar.
La interoperabilidad es directa y bidireccional, y su punto frágil son las proyecciones importadas, cuya vida útil C++ no expresa. Las plantillas se comprueban al instanciarse —máxima expresividad, diagnósticos tardíos, binarios grandes—; los genéricos se comprueban en la definición —errores locales, compilación separada, alguna indirección residual—. El argumento de sucesión se apoya en la seguridad de memoria, la adopción incremental y la capacidad de bajar con Swift embebido, y C++ sigue ganando en portabilidad, control absoluto y ecosistema.
- Compila un módulo mixto con interoperabilidad activada y consume un contenedor estándar de
C++desde un bucle deSwift. - Importa una función que devuelva una referencia a un elemento de un contenedor y construye deliberadamente un uso tras la liberación.
- Anota esa misma función con la información de vida útil adecuada y comprueba qué cambia en el diagnóstico.
- Escribe el mismo algoritmo como plantilla y como genérico con restricciones, e induce un error de tipo en cada uno para comparar los mensajes.
- Mide el tamaño del binario y el tiempo de compilación de ambas versiones instanciadas con cinco tipos distintos.