Número 86
6 de marzo de 2024

Como gañar nos despachos

Todos os libros de programación orientada a obxectos inclúen o mesmo exemplo: temos os animais; os gatos son animais e os cans tamén son animais. Os animais fan un son: o son dos gatos é “miau” e o dos cans é “guau”. O conto di que daquela, para modelar está situación deberiamos crear unha clase “Animal” cun método “Son” e dúas subclases que herdan desta clase: unha chamada “Gato” na que o método “Son” devolve “miau” e outra chamada “Can” na que devolve “guau”.

Unha lebre.E esta lebre, que son fai?

Levamos máis de cincuenta anos a costas con este exemplo e, neste tempo, moitas cousas cambiaron. Por exemplo, daquela diciamos que lle mandabamos unha mensaxe aos obxectos para que os animais fixeran cadanseu son; hoxe dicimos que “chamamos” o método. Outra maneira na que cambiou é que hoxe os expertos pensan que ese exemplo é malo e dá unha idea incorrecta de como deseñar un sistema orientado a obxectos.

(Cando lles pides un exemplo mellor, moitos deses expertos calan. Pero que conste que teñen razón nunha cousa: durante moito tempo púxose moito énfase na herdanza e ignorouse a composición. Pero igual agora estamos no extremo oposto: moitos deseños usan herdanza só para declarar interfaces e composición para todo o demais. Eu digo: nin tanto arre que fuxa nin tanto xo que se deite.)

As linguaxes de programación que usamos hoxe en día están firmemente metidas no bando das que chaman aos métodos. Por exemplo, en C++, cada método de cada clase fica convertido nunha función diferente e o compilador chama a unha función ou á outra dependendo da clase á que pertenza o obxecto.

As primeiras versións de C++ convertían, literalmente, o código á linguaxe C. Un podía escribir algo como isto:

class Animal {
  public:
    void Son() {
      puts("depende");
    }
};

class Gato : public Animal {
  public:
    void Son() {
      puts("Miau");
    }
};

class Can : public Animal {
  public:
    void Son() {
      puts("Guau");
    }
};

E o compilador convertíao a algo que se asemellaba a isto:

struct Animal {};
void Animal_Son(Animal *this) {
  puts("depende");
}

struct Gato { Animal super; };
void Gato_Son(Gato *this) {
  puts("Miau");
}

struct Can { Animal super; };
void Can_Son(Can *this) {
  puts("Guau");
}

E despois escribiamos algo así:

Gato gato;
gato.Son();
Can can;
can.Son();

O compilador convertía as dúas chamadas aos métodos de “Gato” e “Can” en chamadas ás funcións correspondentes:

struct Gato gato;
Gato_Son(&gato);
struct Can can;
Can_Son(&can);

Esta estratexia na que o compilador detecta a clase á que pertence o obxecto para chamar á súa correspondente función chámase “despacho durante a compilación” (“despacho” de “despachar”), ou “compile-time dispatch” en inglés. Esta foi unha das claves do éxito de C++: unha chamada a un método dunha clase era tan rápida e eficiente como unha chamada a unha función normal.

Outros sistemas de programación orientada a obxectos daquel tempo usaban paso de mensaxes, que era case tan complicado como soa. Neles, o obxecto destinatario recibía a mensaxe e despois decidía como executala: isto chámase “despacho durante a execución” (“run-time dispatch”).

Non sempre é posible despachar as chamadas a métodos durante a compilación porque non sempre se sabe a clase á que pertence un obxecto. No exemplo anterior había unha chamada ao método “Son” dun obxecto da clase “Can” e outro da clase “Gato”, pero como un gato é un animal e un can tamén é un animal, podemos ter un animal indeterminado, chamar ao seu método “Son” e esperar ter o son correspondente á súa especie:

// A función devolve un gato ou un can.
Animal* animal = sacaOAnimalDoSaco();
animal->Son();

Que vai facer este programa? Vai miañar ou ladrar? Pois se a función se despacha durante a compilación, ningún dos dous, porque o código equivalente en C é este:

struct Animal *animal = sacaOAnimalDoSaco();
Animal_Son(animal);

E non sei que animal vai ser o que faga o son, pero o que sei seguro é que é galego.

A nosa intención era que, ao chamar ao método “Son” da clase “Animal”, se chamara ao da clase correspondente: “Gato” ou “Can”. Para acadalo, temos que lle dicir a C++ que despache a función durante a execución, e iso faise coa directiva “virtual”.

class Animal {
  public:
    virtual void Son() {
      puts("depende");
    }
};

class Gato : public Animal {
  public:
    void Son() override {
      puts("Miau");
    }
};

class Can : public Animal {
  public:
    void Son() override {
      puts("Guau");
    }
};

C++ despacha métodos durante a execución usando “vtables”, que son estruturas que conteñen punteiros a funcións. Cada clase ten unha vtable e os obxectos que pertencen a esa clase levan un punteiro á vtable. Cando chamades a unha función virtual, o compilador convirte esa chamada a unha consulta na vtable seguida dunha chamada. En C, o exemplo anterior quedaría así:

struct Animal *animal = sacaOAnimalDoSaco();
animal->vtable->Son(animal);

Como podedes imaxinar, o despacho durante a execución é algo menos eficiente porque hai que consultar un punteiro para facelo: non é unha simple chamada a función. A cambio, é máis flexible porque non é necesario saber a clase concreta á que pertence o obxecto.

Isto do despacho durante a compilación ou execución non está limitado ás linguaxes orientadas a obxectos. En Rust, por exemplo, hai unha distinción entre “impl traits” e “dyn traits”. Nos primeiros, o tipo concreto é coñecido durante a compilación, así que Rust despacha as súas funcións durante a compilación. Os segundos sempre chegan a través de punteiros no que a información do tipo non existe durante a compilación, así que Rust pasa unha vtable para despachar as funcións durante a execución.

Anexo para a xente con moita, moita curiosidade

(Xa sabedes o que a curiosidade lle fixo ao gato.)

O equivalente en “C” das definicións das clases cos métodos virtuais sería semellante a isto:

struct vtable_Animal {
  void (*Son)(struct Animal* this);
};

void Animal_Son(Animal *this) {
  puts("depende");
}

struct vtable_Animal _vtable_Animal = {
  .Son = Animal_Son
};

struct Animal {
  struct vtable_Animal *vtable = &_vtable_Animal;
};

void Gato_Son(Gato *this) {
  puts("Miau");
}

struct vtable_Animal _vtable_Gato = {
  .Son = Gato_Son
};

struct Gato {
  union {
    Animal super;
    struct vtable_Animal *vtable = &_vtable_Gato;
  };
};

void Can_Son(Can *this) {
  puts("Guau");
}

struct vtable_Animal _vtable_Can = {
  .Son = Can_Son
};

struct Can {
  union {
    Animal super;
    struct vtable_Animal *vtable = &_vtable_Can;
  };
};

Máis ou menos. Agora xa sabedes por que a xente cabal non fai programación orientada a obxectos en C puro.

A ilustración desta Folla procede de “Types du règne animal. Buffon en estampes”.