Pascal – operator overloading

Várias são as linguagens nas quais podemos fazer overload de funções; esta funcionalidade permite que uma função possua várias versões que admitam diferentes conjuntos de argumentos, ficando o compilador encarregue de seleccionar qual dos overloads é o correcto aquando da invocação dessa função. Uma das linguagens com essa capacidade é o Object Pascal moderno.

Em Pascal, este tipo de polimorfismo também se aplica aos operadores, os quais podem de igual forma ser overloaded.

Na definição da linguagem Pascal segundo a documentação do Free Pascal, os tokens (palavras que constituem o código do nosso programa) podem pertencer a várias categorias, cada uma delas com funções ou características particulares. Uma dessas categorias é a dos operadores, os quais são um símbolo ou conjunto de dois símbolos, com uma função específica, admitindo um ou dois operandos e devolvendo um resultado. Os operadores são habitualmente funções com nomes e sintaxe especiais (nomes compostos por símbolos e invocação infixa, por oposição à tradicional invocação prefixa).

Dentro destes operadores, muitos deles podem ser overloaded, permitindo ao  programador defini-los para novos tipos de operandos, e com comportamentos diferentes do habitual, aumentando assim a expressividade do código. A isto chamamos operator overloading, objecto de estudo do presente artigo.

Todo o código do artigo foi compilado com Free Pascal Compiler, versão 2.6.2,  em ambiente GNU/Linux (Ubuntu 12.04 LTS).

 Overload de operadores aritméticos

Nem todos os operadores podem ser overloaded, mas os mais comuns podem: os operadores de atribuição, aritméticos e de comparação.

Vamos partir de casos práticos para entender como se faz overload de operadores em Free Pascal. Imagine-se, por exemplo, que pretendemos multiplicar os valores de um array de números inteiros, do tipo Integer, por um número também ele Integer, retornando um novo array.

Regra geral, o que nós fazemos é iterar pelos elementos do array, aplicando o dito cálculo. O resultado pode ficar no mesmo array ou ser atribuído a um novo. Decerto o leitor já teve situações nas quais necessitou de realizar aplicação de cálculos simples a um array e tornou-se possivelmente cansativo implementar estes ciclos, mesmo com recurso a funções.

Com a capacidade de overloading dos operadores torna-se possível reduzir o ruído do código. Para tal, implementamos numa unit os overloads que nos forem úteis.

Podemos, então, começar a tirar apontamentos acerca da sintaxe da implementação de operating overloading:

operator simbolo (operando1 : tipo1; operando2 : tipo2) resultado : tipo3;

operator é uma palavra reservada comummente não reconhecida pela esmagadora maioria dos editores de texto e IDEs (embora muitos nos permitam definir palavras reservadas adicionais). No entanto, é uma palavra reservada que equivale essencialmente à function ou procedure. Neste caso, indica ao programa que vai haver um overload de um operador pré-existente, representado por simbolo.

Entre parêntesis indicamos os operandos, tal como o fazemos com os argumentos de uma função (afinal de contas, um operador é uma função com dois argumentos). De seguida, vem algo que difere da sintaxe comum: aparece um novo identificador. Em Pascal (Standard e Free Pascal), o resultado de uma função é atribuído a uma variável local com nome igual ao da própria função; no caso de um operador não existe um identificador alfanumérico mas sim um símbolo (o operador), o qual não é um nome válido para uma variável, e ao qual não pode ser atribuído um valor. Portanto, temos de criar o identificador que represente o resultado. Especificamos o seu nome, por conseguinte, após a declaração dos operandos, seguido do tipo de dados de output.

Vejamos com mais pormenor a seguinte implementação:

operator * (n : integer; list : TIntegerArray)
 res : TIntegerArray;
var i : word;
begin
  SetLength(res, Length(list));
  for i := Low(list) to High(list) do
    res[i] := n * list[i];
end;

Estamos a fazer overload ao operador de multiplicação *, o qual tem dois operandos: n, do tipo integer, que vai ficar à esquerda do operador, e list, do tipo TIntegerArray, que vai ficar à direita. O resultado da operação será atribuído ao identificador res, e é-nos indicado que o output é do tipo TIntegerArray.

Não podemos indicar de forma explícita que o tipo de dados é array of integer, uma vez que o compilador não o aceita. Deste modo, temos de criar um novo tipo de dados de modo a que possamos fornecer este tipo na forma de um identificador.

type TIntegerArray = array of integer;

A partir do momento em que temos este overload definido, podemos utilizar o operador * para multiplicar uma variável do tipo integer por outra do tipo TIntegerArray. No entanto, foi referido que um ficava à direita e outro à esquerda. Vejamos o que acontece se tentarmos compilar o seguinte código (o overload do operador está na unit operover (de Operator Overloading) uma unit própria onde iremos colocar o código).

{$mode objfpc}
program artigo44;
uses operover;

var list1 : TIntegerArray = nil;
  list2 : TIntegerArray = nil;
  i : integer;

begin
  (* Código que adiciona valores a “list1” *)

  list2 := list1 * 2;
  for i := Low(list1) to High(list1) do
    writeln('2 * ',list1[i]:2,' = ',list2[i]:2);
end.

O Free Pascal vai indicar o seguinte:

artigo44.pas(16,20) Error: Operator is not overloaded: "TIntegerArray" * "ShortInt"

Nós definimos que o primeiro operando é integer, e nós fornecemos-lhe um TIntegerArray. Isto acontece porque o overloading de operadores é sensível à ordem pela qual recebe os operandos. Vamos, portanto, criar um segundo overload que define a ordem inversa dos operandos:

operator * (list : TIntegerArray; n : integer)
 res : TIntegerArray;
begin
  res := n * list;
end

Desta vez, a variável do tipo TIntegerArray aparece à esquerda do operador, e a integer à direita. Para implementar este overload recorremos ao overload definido anteriormente. A partir deste momento, podemos multiplicar um integer por um TIntegerArray em qualquer ordem. Vamos testar o código anterior, e consideremos que list1 tem os valores {2, 7, 5, 9, 4}; este é o output do programa:

2 * 2 = 4
2 * 7 = 14
2 * 5 = 10
2 * 9 = 18
2 * 4 = 8

Como podemos verificar, list2 possui os valores de list1 multiplicados por 2 e pela ordem original. O nosso operador overloaded funcionou. Podemos fazer o mesmo para a soma. No entanto, a subtracção e a divisão não têm a propriedade comutativa, ou seja, a-b não é o mesmo que b-a, e a/b não é o mesmo que b/a. Nestes casos, talvez seja de vital importância definir bem o overloading para cada ordem de operandos. Isto é, 5-list deverá dar um resultado diferente de list-5.

Vamos continuar com o overload do operador de adição +.

operator + (n : integer; list : TIntegerArray)
 res : TIntegerArray;
var i : word;
begin
  SetLength(res, Length(list));
  for i := Low(list) to High(list) do
    res[i] := n + list[i];
end;

operator + (list : TIntegerArray; n : integer)
 res : TIntegerArray;
begin
  res := n + list;
end;

Como se pode verificar, é em tudo igual ao overload do operador *. De seguida iremos implementar o overload do operador de subtracção -. No entanto, para aumentar a versatilidade dos operadores e tornar, por último, esta implementação mais compacta, iremos primeiro implementar o menos unário (o operador - que indica um valor negativo, como -4):

operator - (list : TIntegerArray)
 res : TIntegerArray;
var i : word;
begin
  SetLength(res, Length(list));
  for i := Low(list) to High(list) do
    res[i] := -list[i];
end;

Este operador só tem um operando, à direita. Neste momento podemos definir o overload do operador -, de subtracção, de forma bastante compacta:

operator - (n : integer; list : TIntegerArray)
 res : TIntegerArray;
begin
  res := n + (-list);
end;

operator - (list : TIntegerArray; n : integer)
 res : TIntegerArray;
begin
  res := (-n) + list;
end;

Repare-se que recorremos ao operador + e, no primeiro caso, ao menos unário para definir estes overloads. Podíamos ter feito com ciclos, mas esta é uma técnica que permite poupar algumas linhas de código, tornando o código mais legível.

Resta-nos o operador de divisão /, que retorna um número real. Todavia, o nosso output tem sido um array of integer. Para este caso, necessitaremos de um novo tipo de dados para o output:

type TRealArray = array of real;

operator / (n : integer; list : TIntegerArray)
 res : TRealArray;
var i : word;
begin
  SetLength(res, Length(list));
  for i := Low(list) to High(list) do
    res[i] := n / list[i];
end;

operator / (list : TIntegerArray; n : integer)
 res : TRealArray;
var i : word;
begin
  SetLength(res, Length(list));
  for i := Low(list) to High(list) do
    res[i] := list[i] / n;
end;

Se pretendermos resultados inteiros, também podemos fazer o overload dos operadores mod e div, ou seja, o resto da divisão e a divisão inteira, respectivamente.

Outros operadores aritméticos que podem ser overloaded são a exponenciação (**) e a diferença simétrica (><).

Overload de operadores de comparação

Na mesma medida em que podemos fazer o overload de operadores aritméticos, os operadores de comparação também têm essa capacidade.

A primeira coisa a saber é que estes operadores só podem retornar um boolean. Caso tentemos que o output seja outro tipo de dados, será emitido um erro de compilação:

artigo44.pas(37,67) Error: Comparative operator must return a boolean value

Para os analisar, vamos considerar o seguinte exercício:

Implemente o overload de todos os operadores de comparação (excepto o de igualdade e o de diferença) de forma a comparar dois arrays. Caso os arrays tenham tamanhos diferentes, deverá retornar False. Caso contrário, se todo o par de elementos cumprir a comparação, devolve True.

Vamos trocar por miúdos: se fizermos {1,2,3}>={1,4,2} iremos obter False. Os arrays têm o mesmo tamanho, mas o segundo par de elementos (o 2 do primeiro array, e o 4 do segundo) não cumprem a comparação: 2>=4 é falso, pelo que a comparação dos arrays vai retornar False.

Não implementamos os operadores de igualdade e de diferença pelo simples motivo de estes, por defeito, já terem a capacidade de comparar arrays.

Para implementar estes overloads de forma compacta, vamos analisar a seguinte propriedade matemática:

  • a>b é o mesmo que b<a;

Portanto, basta implementar um dos operadores, e o outro será a aplicação do  anterior, mas invertido. Em código:

operator > (list1, list2 : TIntegerArray)
 res : boolean;
var i : word;
begin
  res := Length(list1) = Length(list2);
  if res then
    for i := Low(list1) to High(list1) do
      if not(list1[i] > list2[i]) then begin
        res := false;
        break;
      end;
end;

operator < (list1, list2 : TIntegerArray)
 res : boolean;
begin
  res := list2 > list1;
end;

Portanto, para implementar o overload de um operador de comparação com a ordem dos operandos invertidos, recorremos ao overload do operador inverso.

O mesmo se aplica aos operadores <= e >=.

Por fim, os operadores de igualdade (=) e diferença (<>) também podem ser overloaded.

Overload do operador IN

Desde a versão 2.6.0 do Free Pascal Compiler que o operador IN também pode ser overloaded. Este operador verifica, por defeito, se um determinado valor pertence a um set. No entanto, ele não tem a capacidade de fazer esta verificação com um array.

Desta forma, vamos implementar um overload que o permita:

operator in (n : integer; list : TIntegerArray)
 res : boolean;
var elem : integer;
begin
  res := false;
  for elem in list do
    if n = elem then begin
      res := true;
      break;
    end;
end;

Overload do operador de atribuição

Para terminar, só falta abordar um operador que também pode ser overloaded: o operador de atribuição, :=.

A par do menos unário, este operador só tem um operando, o qual se encontra igualmente do seu lado direito. Para demonstrar como se faz overloading deste operador, vamos receber um set of byte e transformá-lo num array of integer.

type TIntegerArray = array of integer;
 TByteSet = set of byte;

operator := (list : TByteSet) res : TIntegerArray;
var elem : byte;
begin
  res := nil;
  for elem in list do begin
    SetLength(res, Length(res)+1);
    res[High(res)] := elem;
  end;
end;

O tipo de dados set implica que os dados fiquem automaticamente por ordem, independentemente da ordem em que os declaramos no código. Isto será visto de seguida.

 Conclusão

Consideremos a implementação de todos estes overloads numa unit denominada operover, tal como foi referido ao longo do artigo. Vamos testá-la com o seguinte programa:

{$mode objfpc}
program artigo44;
uses operover;

var list1 : TIntegerArray = nil;
  list2 : TIntegerArray = nil;
  i : integer;

begin
  list1 := [2,7,5,9,4];
  list2 := -list1 * 2;
  for i := Low(list1) to High(list1) do
    writeln('2 * ',-list1[i]:2,' = ',list2[i]:3);

    writeln('list1 > list2? ':16, list1 > list2);
    writeln('list1 <= list2? ':16, list1 <= list2);
    writeln('-4 in list2? ':16, -4 in list2);
end.

O output deste programa é o seguinte:

2 * -2 = -4
2 * -4 = -8
2 * -5 = -10
2 * -7 = -14
2 * -9 = -18
 list1 > list2? TRUE
list1 <= list2? FALSE
 -4 in list2? TRUE

Como podemos constatar, apesar do set ter sido fornecido numa ordem aleatória, os dados foram colocados por ordem crescente, isto por causa do modo como funciona o tipo de dados set. Podemos portanto concluir que os operadores estão a funcionar conforme era a nossa intenção.

Desta forma terminamos esta incursão pelo mundo do overloading de operadores em Free Pascal. Basicamente qualquer tipo de dados pode ser fornecido a um operador, desde que seja feito o seu overload. Esta é uma ferramenta que permite tornar a linguagem muito mais expressiva, compacta e até intuitiva.

Publicado na edição 44 (PDF) da Revista PROGRAMAR.