Forma Canónica Ortodoxa de una clase

 
Clasificación Intención Otros nombres Motivación Aplicabilidad Estructura
Consecuencias Implementación Usos  Patrones Relacionados  
 

Clasificación

Idiom en C++
 

Intención

Permitir que un objeto en una clase se pueda comportar como un tipo básico, en términos de operaciones de copia entre objetos, paso de parámetros a funciones y arreglos de objetos.
 

Otros nombres

Forma canónica
 

Motivación

Un tipo concreto de datos creado por el usuario debe comportarse igual que cualquier otro tipo en el programa, en particular los tipos predefinidos. El objetivo de todo tipo concreto es extender el sistema de tipos disponible en el lenguaje, buscando que el compilador pueda generar tipos arbitrarios y de una manera segura y eficiente.  Se busca entonces  un marco de referencia común para que el compilador maneje la memoria de tipos nuevos (canónica) y soportada por los mismos mecanismos del lenguaje (ortodoxa). 

Supongamos el desarrollo de una clase String, para manejo de cadenas de caracteres. Con objetos de tipo String debe ser posible hacer las siguientes operaciones, comunes en el tratamiento de cualquier tipo de datos predefinido: 
 
 
void Funcion1( )
{
  String s1( "Hola Mundo" ); 
  String s2 = s1;
  String vec[10];  // vector de Strings
  ...
  vec[0] = Funcion2( s2, ... );
}
Ejemplo 1. Manipulación de un tipo concreto 

En esta función se crea un objeto s1 cuyo contenido es la cadena "Hola Mundo", un s2 como copia de s1, un vector de 10 Strings vacios, se asigna un nuevo valor a s1 y se envía s2 como parámetro de Funcion2. Una clase que permita a sus objetos hacer estas operaciones cumple con este Idiom. 
 

Aplicabilidad

Siempre se debe aplicar este patrón a cualquier nueva clase
 

Estructura

Suponiendo como ejemplo la clase String, se tiene la siguiente definición 
 
 
class String
{
public:
  String( ); // Constructor sin argumentos
  String( const String& ); // Constructor por copia
  String& operator=( const String& ); // asignación
  ~String( ); // Destructor
  // ... Otras operaciones del tipo
private:
  char* rep;  // Implementación del tipo.
};
La clase debe definir los siguientes elementos: 
 
  • Un constructor sin argumentos, que permita inicializar cada uno de los objetos en un arreglo. En este caso el código podría ser de la forma:

  •  
    String::String( )
    {
      rep = new char[1];
      rep[0] = '\0';
    }
    Este constructor es llamado automáticamente para dar un valor inicial a cada uno de los objetos en un vector, como el definido por la sentencia String vec[10]; del ejemplo 1
     
  • Un construtor por copia, que se llama al crear objetos como copia de otros (String s2 = s1;) y algunas veces en el paso y retorno de argumentos de una función ( Funcion2 (s2) ). El código de este constructor es
  • String::String( const String& value )
    {
      rep = new char[::strlen( value.rep ) + 1];
      ::strcpy( rep, value.rep );
    }
     
  • El operador de asignación entre objetos del mismo tipo, el cual es utilizado en cualquier asignación entre objetos después de su declaración. En el caso del ejemplo 1 se llama después de ejecutada Funcion2,  para asignar el valor de retorno a vec[0]. El código de esta operación es
  • String& String::operator=( const String& value )
    {
      if( rep != value.rep )
      {
        delete[] rep;
        rep = new char[::strlen( value.rep ) + 1];
        ::strcpy( rep, value.rep );
      }
      return *this;
    }
     
  • Un destructor, que es llamado cada vez que el ámbito de un objeto termina. En el caso del ejemplo 1 el destructor se llama para todos los objetos al terminar la Funcion1. El cuerpo de la función para la clase String es
  • String::~String( )
    {
      delete[] rep;
    }
     
 
 

Consecuencias

Existen algunos problemas de eficiencia que tienen que ver con los procesos de copia entre objetos, descritos dentro del constructor por copia y de la operación de asignación. Dentro de estas operaciones puede considerarse varias opciones: 
  • Copia superficial vs. copia profunda. Ya que los atributos de un objeto pueden a su vez ser objetos complejos, puede tomarse la decisión de compartir estos objetos en las copias (copia superficial) o crear sus correspondientes clones (copia profunda). La copia superficial es mas rápida y ocupa menos espacio de memoria, pero puede crear problemas en la manipulación de los objetos.
  • Copia física vs. copia lógica. Al realizar una copia física, cada vez que se crea un clon se copia su estado. Una copia lógica da la impresión de tener dos clones, aunque compartan la misma representación; la cual deja de ser compartida al cambiar alguno de los clones. Estos puede ser util por razones de tiempo y porque no se requiere cambiar el valor de muchos de esos clones. Este concepto es tratado en más detalle en los Idioms Manija-Cuerpo (handle-body) y derivados.

Implementación

Una clase XX que cumpla con la forma canónica ortodoxa tiene por lo menos los siguientes métodos 
 
 
class XX
{
public:
  XX( ); // Constructor sin argumentos
  XX( const XX& ); // Constructor por copia
  XX& operator=( const XX& ); // asignación
  ~XX( ); // Destructor
  // ... Otras definiciones del tipo
};
 
 
 
 

Usos conocidos

Siempre que se defina una clase. No se conocen excepciones.

Patrones relacionados