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 queb<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.