Para ayudar a celebrar el 25.º aniversario de C++Builder, vuelvo a publicar un artículo mío muy antiguo sobre las extensiones del lenguaje C++ introducidas por Borland en ese entonces.
Como probablemente haya escuchado, poco después del 27.° aniversario de Delphi, C++ Builder celebra su 25.° aniversario. Puede leer más sobre el historial del producto en esta excelente publicación de blog del PM de C++Builder, David Millington, sobre “¡ Celebración de los 25 años de C++Builder! ” Hay otra publicación de blog muy interesante de David I sobre “ El 25.° aniversario de C++Builder: desarrollo visual, el poder del lenguaje C++ y 2,5 décadas de excelencia continua ”. Incluso si está interesado principalmente en Delphi, ambos artículos ayudan a poner dos productos “gemelos” en la perspectiva correcta.
En el momento en que se lanzó Delphi, estaba centrado principalmente en C++ (y en particular en la biblioteca OWL de Borland C++ para la programación de Windows) y había escrito 3 libros sobre el lenguaje , el IDE de Borland y la biblioteca. Mientras cambié a Delphi, C++ seguía siendo una parte activa de mi enfoque cuando apareció C++Builder. Di una sesión en la SD West Conference sobre las extensiones del lenguaje C++ Builder y escribí bastante sobre este tema, incluso si nunca escribí un libro completo al respecto, ya que estaba bastante ocupado con el lado de Delphi.
Para celebrar el 25.° aniversario de C++Builder, decidí publicar uno de mis artículos sobre el lenguaje exactamente como es. Esto es del material de la conferencia SD 97 West. No todo sigue siendo exacto después de 25 años, pero la mayor parte de la descripción general sigue siendo válida. Pero en lugar de editar a los idiomas de hoy, sentí que era bueno mantenerlo exactamente como se escribió originalmente. Espero que disfrutes leyéndolo.
Table of Contents
Extensiones de lenguaje C++ Builder (o: Programación Delphi en C++)
por Marco Cantú
Cuando se lanzó Delphi por primera vez, muchos programadores se quejaron: “¿Por qué no está basado en el lenguaje C++?”. Algunos de ellos (incluyéndome a mí) respondieron: “Simplemente porque esto no es posible, debido a muchas características que faltan en el lenguaje C++”. Ahora que Borland ha lanzado C++Builder, o Delphi para C++, podrías pensar que voy a empezar a encontrar excusas para mi afirmación incorrecta. ¡No soy! Sigo pensando que mi afirmación es correcta. De hecho, Borland C++ Builder no se basa en el lenguaje ANSI C++, sino en una versión muy ampliada de este lenguaje, que incluye casi todas las características principales del lenguaje Object Pascal que se encuentran en Delphi.
En este artículo voy a discutir todas estas extensiones de lenguaje en detalle, brindando algunos antecedentes para los programadores sin experiencia con Delphi. No voy a cubrir todas las funciones nuevas de C++Builder, como las macros que se usan para implementar parámetros de matriz abierta o la plantilla que se usa para clonar conjuntos de Pascal. Me voy a centrar sólo en las características principales.
¿Qué es una propiedad?
Uno de los elementos clave de la programación visual es la idea de las propiedades de un objeto. Pero, ¿qué es una propiedad? Una propiedad es un nombre con un tipo , relacionado con algunos datos o funciones miembro de acceso a datos de una clase.
Una de las ideas básicas de OOP es que los datos de los miembros siempre deben ser privados. Luego, a menudo escribirá funciones miembro públicas para obtener y establecer ese valor privado. Si luego modifica la implementación, puede cambiar fácilmente el código de estas funciones de acceso y evitar cualquier cambio en el código que usa la clase.
Aquí hay un ejemplo básico (escrito siguiendo las convenciones de nomenclatura estándar para propiedades, campos y funciones de acceso):
El código de estas dos funciones miembro (que no se muestran) simplemente establece o devuelve el valor del miembro de datos privados fTotal . Puede usarlas de la siguiente manera:
[crayon-6762e1c2bba8e792222568/]Este es el enfoque estándar de C++. En C++Builder podemos definir dentro de una clase una propiedad que envuelve estas funciones de acceso:
[crayon-6762e1c2bba8f482180770/]Esto significa que ahora podemos acceder a este valor de una manera más uniforme, ya que usamos la misma notación tanto para leer como para escribir la propiedad:
[crayon-6762e1c2bba90986556223/]Dependiendo de su papel en una declaración, la expresión Form1->Total es traducida por el compilador en una llamada a la función de lectura o escritura. En realidad, el compilador también puede traducir una expresión similar en un acceso directo a los datos. Mira la siguiente declaración de propiedad:
[crayon-6762e1c2bba91855561959/]Aunque este código parece extraño al principio, demuestra completamente el papel de las propiedades como mecanismo de encapsulación. De hecho, podemos cambiar más tarde la declaración de esta propiedad introduciendo una de dos funciones en lugar del acceso directo a los datos. En este caso, necesitaremos recompilar el código de las clases que usan esta propiedad, pero no necesitaremos modificarlo.
Obviamente, las funciones de acceso no están restringidas a leer y escribir datos privados, sino que pueden hacer cualquier cosa. Uno de los efectos típicos de las funciones de escritura es actualizar la interfaz de usuario. Aquí hay una nueva versión de la propiedad Value , seguida del código de su función de escritura (escrito siguiendo el estilo estándar):
[crayon-6762e1c2bba92992454717/]Ahora si escribimos las siguientes afirmaciones:
[crayon-6762e1c2bba93307884214/]el efecto del cambio en los datos se reflejará automáticamente en la interfaz de usuario. Tenga en cuenta que esto también funciona con operadores. Cuando aplica el operador de incremento (++) a la propiedad, se lee su valor y se llama al método Set. ¡Realmente creo que las propiedades son un mecanismo sólido de encapsulación OOP!
Más reglas de idioma para propiedades
Además de la estructura básica que hemos visto, las declaraciones de propiedad le permiten muchas alternativas. Una idea fundamental es que las propiedades tienen un tipo de datos, pero están limitadas a un conjunto determinado de tipos de datos (incluida la mayoría de los tipos, cadenas y clases predefinidos).
Las propiedades pueden ser de lectura y escritura como en los ejemplos anteriores, pero también pueden ser de solo lectura o de solo escritura. Lo que es común es ver propiedades de solo lectura, es decir, valores que puede leer pero no cambiar. Un ejemplo obvio de una propiedad de solo lectura en la VCL (la biblioteca de componentes visuales, la jerarquía de clases de Delphi utilizada también por C++Builder) es la propiedad Handle de los controles basados en ventanas. Es posible que desee solicitar un control de su identificador de Windows, cuando desee llamar directamente a las funciones de la API de Windows, pero se supone que no debe cambiar el identificador de una ventana. Para definir una propiedad de solo lectura, simplemente omita la declaración de escritura :
[crayon-6762e1c2bba94550426568/]Es posible declarar propiedades de matriz, es decir, propiedades con un índice. En este caso, las funciones de lectura y escritura requeridas tienen un parámetro adicional, el propio índice. Este índice también debe usarse para acceder a los valores, ya que no puede acceder a la matriz en su conjunto. Es posible que la matriz no exista como un campo real de la clase: cuando lee los Elementos de un cuadro de lista, en realidad le está preguntando a Windows el valor del elemento (que no está duplicado dentro del objeto TListBox ). También es posible declarar propiedades de matriz con múltiples índices (ver como ejemplo la propiedad Pixels de la clase TCanvas de la VCL).
La visibilidad de acceso de una propiedad
Las propiedades se pueden declarar utilizando cualquiera de los especificadores de acceso, incluido privado (aunque esto tiene poco sentido), protegido y público, además de los dos nuevos especificadores: __publicado y __automatizado. Por cierto, las propiedades de solo lectura no se pueden publicar.
Un campo o método publicado no solo está disponible en tiempo de ejecución (como un elemento público), sino también en tiempo de diseño. El compilador Borland C++Builder genera una identificación de tipo en tiempo de ejecución (RTTI) al estilo Delphi para las propiedades publicadas de clase derivadas de TPersistent . El entorno de tiempo de diseño utiliza esta información de tipo, comenzando con el Inspector de objetos, pero también está disponible para los programadores que desean profundizar en la unidad TypInfo no documentada.
La palabra clave publicada generalmente se usa para propiedades o eventos, pero los formularios generalmente tienen una interfaz publicada que incluye también subcomponentes y métodos de manejo de eventos. El entorno de desarrollo analiza automáticamente esta información y la pone a disposición en el Inspector de objetos incluso antes de compilar el programa.
Por ejemplo, mientras que el Inspector de objetos usa la interfaz publicada de un componente para mostrar y editar valores de propiedad en el momento del diseño, el Inspector de objetos usa la interfaz publicada de un formulario para encontrar componentes compatibles con un tipo de datos y funciones miembro dados. compatible con un evento dado.
Hay un quinto especificador de acceso, automatizado , que se utiliza para definir una interfaz pública con la información de tipo de Automatización OLE correspondiente, lo que permite crear Servidores de Automatización OLE. La palabra clave __automated se usa en las subclases TAutoObject .
Tenga también en cuenta que la visibilidad de una propiedad se puede ampliar en las clases derivadas. Una propiedad protegida, por ejemplo, se puede volver a declarar como una propiedad publicada. Para lograr esto, no necesita redefinir la propiedad, y solo volver a declararla (Borland usa el término “propiedades izadas” para indicar este comportamiento). Al volver a declarar una propiedad, también puede cambiarla, por ejemplo, modificando su valor predeterminado.
Clausuras y Eventos
Cuando las propiedades son del tipo de datos “puntero a una función miembro” (también conocido como cierre ), se denominan eventos. Pero, ¿qué es un cierre? Otra adición al lenguaje C++ estándar. Un cierre es una especie de puntero de función miembro. En realidad, asocia un puntero a una función miembro con un puntero a una instancia de clase, un objeto. El puntero a la instancia de clase se utiliza como puntero this al llamar a la función miembro asociada. Esta es la definición de cierre:
[crayon-6762e1c2bba95927663819/]Dado que en C++ los punteros de función miembro también están disponibles, pero rara vez se usan, es posible que se pregunte para qué necesitamos estas cosas incómodas. Los cierres realmente importan en el modelo Delphi. De hecho, los eventos son cierres, que tienen el valor de una función miembro del formulario que alberga el componente relacionado. Por ejemplo, un botón tiene un cierre, denominado OnClick , y puede asignarle una función miembro del formulario. Cuando un usuario hace clic en el botón, esta función miembro se ejecuta, incluso si la ha definido dentro de otra clase (normalmente, en el formulario). Esto es lo que puede escribir (y C++ Builder generalmente escribe por usted):
[crayon-6762e1c2bba96266311561/]El código anterior intercambia dos controladores de eventos en tiempo de ejecución. También puede declarar explícitamente una variable de un tipo de cierre, como el tipo TNotifyEvent común , y usarla para intercambiar los controladores de dos eventos:
[crayon-6762e1c2bba97173744171/]Esto significa que puede asignar una función miembro (como BtnHelloClick ) a un cierre o evento tanto en tiempo de diseño como en tiempo de ejecución.
Propiedades y objetos de transmisión
Las clases de las derivadas de TPersistent tienen otras características importantes. Puede guardar objetos de estas clases en una secuencia. La VCL no guarda los datos internos de un objeto, sino que simplemente guarda los valores de todas sus propiedades (y eventos) publicadas. Cuando se carga el objeto, la VCL primero crea un nuevo objeto, luego reasigna cada una de sus propiedades (y eventos). El ejemplo más obvio de transmisión de objetos es el uso de archivos DFM. Estos archivos binarios almacenan las propiedades y los componentes de un formulario, y le sugiero que los cargue en el editor C++Builder para estudiar su versión textual (también puede usar el programa de línea de comando CONVERT.EXE para convertir un archivo DFM en un archivo TXT o viceversa)
El proceso de transmisión se puede personalizar de diferentes maneras: agregando una cláusula predeterminada o almacenada a la declaración de propiedades, o anulando el método DefineProperties . Cuando agrega una propiedad a un componente, generalmente agrega a la definición una cláusula predeterminada . Si el valor de una propiedad coincide con su valor predeterminado, la propiedad no se guarda en una secuencia junto con las demás propiedades de un objeto. El constructor de la clase debe inicializar la propiedad con el mismo valor predeterminado, o esta técnica no funcionará. La directiva almacenada , en cambio, indica si una propiedad debe guardarse en un archivo junto con el objeto o no. el almacenado La directiva puede ir seguida de un valor booleano o una función miembro que devuelve un resultado booleano (para que pueda elegir si desea guardar el valor o no según el estado actual del objeto). Finalmente, el método DefineProperties le permite crear pseudo-propiedades, agregar valores adicionales al objeto transmitido y volver a cargarlos correctamente.
Más RTTI ( dynamic_cast y más)
Además de la información RTTI generada por la palabra clave __published , cada objeto tiene varios métodos que puede usar para consultarse a sí mismo. Estos métodos son parte de la clase TObject , la clase base de todo objeto basado en VCL. Puede ver la lista de los métodos de la clase TObject (incluidas las funciones miembro estáticas) en la Tabla 1.
Tabla 1: Funciones miembro de TObject
// funciones miembro públicas
Aobjeto
Gratis
Tipo de clase
Instancia de limpieza
dirección de campo
Después de la construcción
AntesDestrucción
Envío
Controlador predeterminado
Instancia gratuita
~TObjeto
// función de miembros públicos estáticos
InitInstancia
Nombre de la clase
ClassNameIs
ClassParent
Información de clase
Tamaño de instancia
Hereda de
MétodoDirección
Nombre del método
Para verificar si una clase es de un tipo dado, puede usar el método ClassType . Esto es bastante común con el parámetro Sender de un controlador de eventos. Este parámetro hace referencia al objeto que ha generado el evento, pero es del tipo genérico TObject . Cuando asocia el mismo método a eventos de diferentes objetos, es posible que desee verificar el tipo de objeto que ha causado el evento:
[crayon-6762e1c2bba98447144648/]El __classid es otra palabra clave agregada a C++Builder. Devuelve la metaclase del objeto (ver la siguiente sección). La alternativa (no siempre válida) es usar el parámetro Sender de un controlador de eventos y convertirlo en un tipo de datos dado, usando la técnica de transmisión dinámica estándar de C++. Esto tiene sentido si conocemos el tipo de datos o el de una clase ancestro común, como en este caso:
[crayon-6762e1c2bba99831557176/]Como mencioné, además de estas capacidades de RTTI, Delphi y C++ Builder comparten información de tipo extensa, disponible en tiempo de ejecución para cada objeto de una clase derivada de TObject y que tiene campos, propiedades o métodos publicados. Por ejemplo, puede acceder a una propiedad de forma dinámica, por nombre; puede obtener la lista de los nombres de las propiedades de una clase; puede obtener la lista de parámetros de un cierre. Esto permite a los programadores crear herramientas complementarias muy potentes para el entorno, y el propio sistema lo utiliza como base de las herramientas de desarrollo visual, comenzando con el Inspector de objetos (de hecho, he creado un clon en tiempo de ejecución del Inspector de objetos) .
Metaclases y constructores virtuales
Delphi introdujo el concepto de referencia de clase, un puntero a la información de tipo de una clase. En C++ Builder, esto se ha asignado a la idea de una clase TMetaclass, y el tipo TClass no es más que un puntero a esta metaclase. En Delphi TClass tiene un rol similar pero una definición diferente (aunque las dos implementaciones son totalmente compatibles).
Los métodos disponibles para una metaclase corresponden exactamente a los métodos estáticos de la clase TObject (de hecho, no existe una clase TMetaclass en Delphi, sino una clase de referencia, que puede usar los métodos estáticos de TObject directamente). Una vez que tenga una variable TClass , puede asignarle una clase, extraída del objeto (con la función ClassType ) u obtenida de la clase misma usando la palabra clave __classid . Aquí hay un ejemplo:
[crayon-6762e1c2bba9a668750064/]Puede usar una metaclase casi como usa una referencia de clase en Delphi. Lo que no es posible en C++Builder es crear un nuevo objeto basado en una metaclase. Esto es extraño: C++Builder le permite definir constructores virtuales, pero luego no proporciona ninguna forma de llamarlos, a menos que llame a un método Delphi. Esto es lo que sucede cuando define un nuevo componente en C++Builder y luego deja que la VCL lo maneje (por ejemplo, cuando carga un componente desde un flujo, se llama a su constructor virtual).
Para crear un objeto a partir de una referencia de clase, necesitamos agregar una unidad Pascal simple a nuestra aplicación C++Builder. Aquí está el código de la función Pascal que podemos usar:
[crayon-6762e1c2bba9b550823841/]Podemos usar esta función en una aplicación C++Builder. Este es el código simple que puede usar para crear un componente de una clase determinada en tiempo de ejecución y colocarlo dentro de un control ScrollBox:
[crayon-6762e1c2bba9c049346953/]Este es el código que puede usar para crear un componente del mismo tipo del objeto Remitente :
[crayon-6762e1c2bba9d766345730/]Conclusión
Como hemos visto, Borland agregó muchas características nuevas al lenguaje C++ en C++ Builder para que este lenguaje sea compatible con Object Pascal y se adapte a la programación basada en componentes. Extender C++ de esta manera puede parecer un poco incómodo, ya que estamos tratando de mapear las construcciones de un idioma en otro idioma. Sin embargo, la facilidad de uso del entorno y el desarrollo visual probablemente compensan esta complejidad extra, que la mayoría de las veces queda tras bambalinas.
Al mismo tiempo, C++ Builder conserva la compatibilidad total con el estándar ANSI C++, incluidas las plantillas y STL (por nombrar dos funciones que no están disponibles para los programadores de Delphi), y le permite mezclar código basado en VCL con código MFC o OWL, aunque esto hace que sentido sólo si tiene aplicaciones existentes. La biblioteca VCL, de hecho, le permite aprovechar la programación visual y trabajar con una mayor abstracción que las típicas bibliotecas de clases de Windows C++.
Existen muchas otras diferencias entre Delphi y C++Builder, pero la implementación de propiedades es la característica con mayor impacto en términos de extensiones del lenguaje C++. La mayoría de los demás problemas (mapas de mensajes, conjuntos y parámetros de matriz abierta, por nombrar solo algunos) se han resuelto utilizando palabras clave y funciones de lenguaje de C++ existentes.
Marco Cantú’; es un escritor y consultor independiente, con sede en Italia. Ha escrito libros de programación tanto en C++ como en Delphi, traducidos a 10 idiomas en todo el mundo. Contribuye con varias revistas, disfruta hablar en conferencias e imparte seminarios avanzados de Delphi y C++Builder en todo el mundo. Puede comunicarse con él en 100273.2610@ compuserve.com o http:// ourworld. computarizar com/homepages/marcocantú.
No hace falta decir que la información de contacto de Compuserve y el sitio web anterior ya no son válidos, pero decidí dejar el artículo tal como se publicó originalmente, incluida mi biografía.