¿Qué es C++ y qué es C++/CLI?

Introducción

Suele aparecer con cierta frecuencia en los foros de ayuda cierta confusión entre C++ y C++/CLI. Hay quien pregunta algo de C++ siendo C++/CLI y viceversa, o quien intenta aplicar algo de C++/CLI a C++ (y también al revés), o simplemente se ve completamente ofuscado con el tema. En esta entrada vamos a intentar poner un poco de orden en todo esto.

C++ no es C++/CLI

C++ es el lenguaje de toda la vida, inventando por Stroustrup y que, pese al deseo de muchos, está más vivo que nunca y sigue creciendo en funcionalidades y potencia, como es el hecho de que el próximo estándar va a añadir cosas muy interesantes que veremos por aquí. Si no pasa nada, antes de fin de año estará aprobado, se llamará C++09 y sustituirá a C++03 (No, no os penséis que hay 6 versiones entre uno y otro, no, es que el estándar se llama con el año en que se aprobó –si, ya sé que no es lo óptimo pero es así).

En C++ tenemos punteros, referencias, clases, herencia, polimorfismo y todo lo clásico del lenguaje, y los últimos compiladores de Microsoft lo implementan muy bien. Es decir, con Visual C++ (la versión que sea), se puede seguir programando como hasta ahora, con MFC, con Win32 o con cualquier otra biblioteca existente (QT, wxWidgets, y un largo etcétera).

Luego tenemos la plataforma .NET, en la que C++ no puede entrar directamente ya que hay cosas que .NET no soporta, entre las que caben destacar los punteros y la herencia múltiple. No vamos a entrar en detalles de por qué eso no funciona, sino que vamos a aceptarlo como dogma (quizás en algún momento futuro me dé por explicarlo, aunque tampoco es un tema difícil en exceso).

Por lo tanto Microsoft creó un nuevo lenguaje que llamó C++/CLI[1], es decir, el C++ del CLI. Si bien en su momento fue diseñado para que fuera un lenguaje de primera clase (otra forma de decir que debía soportar todo el conjunto de .NET), con la llegada de la versión 3 del Framework C++/CLI se quedó como una herramienta avanzada de interoperabilidad entre .NET y Win32 (luego veremos más sobre esto).

Existen dos formas para acercarnos a las diferencias entre C++ y C++/CLI, o más bien se trata de dos maneras de explicar los mismos conceptos. Yo al menos no tengo clara cuál es la versión formal, si bien me aboco por la primera. Veamos en detalle estas das aproximaciones.

PRIMERA APROXIMACIÓN: C++ es un subconjunto de C++CLI

C++/CLI incluye a C++. Es decir, todo lo que se puede hacer en C++ también se puede hacer en C++/CLI, y más. Si nosotros cogemos un código fuente escrito en C++ y lo compilamos como C++/CLI obtendremos un programa completamente funcional igual que lo era compilado en C++. No vale intentar llamar a una DLL nativa como si fuera nuestro programa, ni a un fichero objeto ya compilado (no me refiero a un objeto, sino a un fichero .obj), debemos partir del código fuente, todo el código fuente.

De este modo, hemos convertido un programa C++ en otro C++/CLI que necesita el .NET Framework para funcionar pero que simplemente continua siendo, exactamente, el mismo que antes y con el mismo código fuente.

Y entonces podemos añadir código que use cosas del .NET, con ciertas limitaciones que veremos luego, pero lo que sí podemos es utilizar los nuevos conceptos de .NET, como referencias manejadas, liberación automática de memoria, genéricos y demás zarandajas.

Por lo tanto, está claro que C++ es un subconjunto de C++/CLI desde este punto de vista.

SEGUNDA APROXIMACIÓN: Son lenguajes diferentes que funcionan lado a lado

Aquí C++ tiene una parte de su sintaxis compartida con C++/CLI de igual modo que el primero la tiene con C. No obstante, son dos lenguajes completamente diferentes que no sólo pueden interactuar entre sí (como C y C++), sino que incluso pueden escribirse en un mismo código fuente, de manera que unas sentencias estarán en C++ y otras en C++/CLI.

A primera vista puede parecer extraño, pero lo cierto es que se trata de una aproximación también correcta (y lo demostraremos).

Si bien podemos compilar un programa existente como .NET, ese código no usa nada de .NET, y el código que creemos para usar .NET no se puede utilizar con el antiguo, no al menos de forma directa: no podemos tener una referencia manejada a un objeto nativo, ni un puntero nativo a una referencia manejada, no podemos obtener la dirección de un objeto manejado, y muchas otras limitaciones más.

Sin embargo, sí que hay otras cosas que podemos compartir de forma indistinta: muchos de los tipos nativos, podemos alojar un puntero nativo en una clase manejada (al revés también, pero con ciertas complicaciones adicionales), y podemos interactuar con código nativo de forma mucho más sencilla que con otros lenguajes.

RESUMIENDO, que es gerundio

Antes de entrar en otras profundidades, conviene hacer un breve resumen de lo que estamos tratando.

Hemos visto que Microsoft tiene dos lenguajes C++: el tradicional y el .NET, llamado C++/CLI. Con el primero ellos podemos construir programas tradicionales (MFC, QT, Win32, etc.). Con el segundo desarrollamos aplicaciones para .NET.

Por otro lado, podemos mezclarlos, o usarlos de forma combinada para crear aplicaciones mixtas que usen partes en Win32 o nativas y partes en .NET o manejadas.

No obstante, son lenguajes diferentes que comparten bastante sintaxis.

Rizando el rizo

Supongamos el siguiente programa, creado a partir del asistente para "Consola CLR", que es como MS llama a un programa C++/CLI en modo consola.

#include "stdafx.h"

#include <stdio.h>

using namespace System;

int main(array<System::String ^> ^args)
{
    printf("Hello World");
    Console::WriteLine("Hello World");
    return 0;
}

Si nos fijamos atentamente, es una mezcla de código nativo (la inclusión de stdio.h y la línea que contiene printf) y código puramente .NET, y si lo ejecutamos veremos cómo se imprimen ambas líneas por la consola.

Por otro lado, podemos elegir entre tres modos de generar un programa en C++/CLI: la opción por defecto es "/clr", aparte de "/clr:pure" y "/clr:safe". ¿En qué afectan a nuestro modelo? En bastantes cosas. La imagen siguiente nos localiza, dentro de las opciones del proyecto, el lugar en dónde cambiarlo:

clip_image002

La primera de ellas es que la opción "/clr:safe" no nos permite compilar código mixto; es decir, si intentamos compilar el programa de arriba con esa opción obtendremos una muy larga lista de errores en relación a la inclusión de stdio.h y del printf. Si quitamos esas dos líneas y compilamos con dicha opción nuestro programa, se ejecutará sin problemas, y si lo miramos por ejemplo con el .NET Reflector veremos que es idéntico (o casi) a cualquier otro programa equivalente escrito en, por ejemplo, C#.

"/clr:pure" nos dice que nuestro programa, aparte de acceder a código manejado, también puede tener código nativo compilado como código manejado. Y finalmente la opción por defecto nos permite, aparte de lo anterior, acceder a módulos completamente compilados como código nativo y a compilar la parte nativa como tal.

Por ejemplo, si compilamos nuestro ejemplo con cualquiera de las dos opciones reseñadas, obtenemos el siguiente código generado por el .NET Reflector:

int main(int argc, char modopt(IsSignUnspecifiedByte^)** argv)
{
    ::printf(&::?A0x67b7b245.unnamed-global-0);
    Console::WriteLine("Hello World");
    return 0;
}

Vemos cómo hacemos una llamada a la función printf, que es nativa, con una cadena también nativa y luego hacemos una llamada a WriteLine con una nueva cadena, esta vez todo código manejado.

Una primera aproximación nos dice que C++ es un subconjunto de C++/CLI ya que la llamada a printf se realiza desde código manejado, y el parámetro es la dirección de un miembro de una estructura global, o eso parece). Estamos en la primera aproximación.

Segunda vuelta al rizo

Veamos ahora el siguiente código:

#include "stdafx.h"
#include <stdio.h>

using namespace System;

void printk(const char *p)
{
    while(*p)
    putchar(*p++);
}

int main(int argc,char **argv)
{
    printk("Hello World");
    Console::WriteLine("Hello World");
    return 0;
}

Hemos cambiado poco, pero lo suficiente para ver ciertas diferencias en el código compilado resultante. Hemos añadido una función nativa que recibe un puntero a una cadena y la pone en consola.

¿Qué se imagina el lector que pasará al compilar esto tanto con "/clr" como con "/clr:pure" (recordemos que "/clr:safe" no puede contener código nativo de ningún tipo)? ¿Qué pasará con la función printk?

Veámoslo, pero esta vez con el ILDasm:

clip_image004

Da igual que lo compilemos con cualquiera de las dos opciones, de nuevo una función nativa ha sido compilada como manejada, estamos, otra vez, en el primer caso (y recordemos que .NET, junto a C#, permite punteros, algo no muy tenido en cuenta a veces).

El tercer rizo (y punto final)

Bueno, ya sólo nos queda un nuevo ejemplo de código:

#include "stdafx.h"
#include <stdio.h>

using namespace System;

#pragma unmanaged
void printk(const char *p)
{
    while(*p)
    putchar(*p++);
}
#pragma managed

int main(int argc,char **argv)
{
    printk("Hello World");
    Console::WriteLine("Hello World");
    return 0;
}

Hemos colocado nuestra función nativa bajo las directivas del preprocesador indicadas. Si ahora intentamos compilar este programa con la opción "/clr:pure", nos saltará un error diciéndonos que no podemos compilar la función printk como nativa bajo este modelo.

No obstante, si lo hacemos con "/clr", obtendremos el siguiente código para la función printk:

clip_image006

¡Ondia! No está, no hay nada… Realmente está ahí, pero al ser código nativo, ni el ILDasm ni el Reflector son capaces de verlo. Ahora nos hemos acercado un poco más a la segunda aproximación, en la que estamos viendo cómo C++ va por un lado y C++/CLI por el otro.

Sí, ya sé que es una forma un poco retorcida de verlo, pero sigamos…

Podríamos haberlo complicado un poco, compilando dos módulos independientes, uno con las opciones para C++/CLI y otro sin ellas, y haber hecho la llamada del manejado al nativo, pero ¡eso es exactamente lo que ocurría cuando hacíamos la llamada a printf!

O a cualquier otra función de Win32 o de una biblioteca de C++ nativa. Por tanto, la segunda aproximación también es completamente cierta. Alguien podría objetar que “eso pertenece a una DLL”, pero podemos compilar nuestro código con enlace estático al runtime de C y tendríamos el mismo caso: ejecutando código nativo y manejado desde un mismo ejecutable.


[1] Olvidémonos por completo de las managed extensions, haremos como si no existieran ni hubieran existido nunca jamás de los jamases J.

7 comentarios sobre “¿Qué es C++ y qué es C++/CLI?”

  1. Hola como esta por ejemplo en c++ yo tengo esto

    void change_string(string & str){

    str = «cambiando la string»;
    }

    int main(){

    string str;
    change_string(str);
    }

    y obviamente cuando la imprima me va a mostrar el mensaje pero en c++/cli como se haria?

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *