Pascal – array de argumentos

A definição da linguagem Pascal, desde os seus primórdios, estabelece regras bastante rígidas acerca da passagem de argumentos a uma função ou procedimento. No seu conjunto, uma das consequências destas regras é a impossibilidade de se implementarem funções variádicas, isto é, funções sem um número definido de argumentos. Na prática, isto traduz-se na possibilidade de se poder fornecer virtualmente uma infinidade de argumentos à função, não havendo a restrição de ser necessário passar apenas N argumentos em determinada ordem e com determinados tipos.

Várias linguagens permitem a implementação de funções variádicas, como por exemplo C e até Haskell (com recurso a alguns truques na definição de tipos de dados e suas heranças).

 O facto de Pascal não permitir a implementação de funções variádicas pode suscitar algumas dúvidas. Métodos standard que formam a base do Pascal são aparentemente variádicos, como o writeln e o readln. No entanto, estes métodos não são exactamente funções ou procedimentos. A sua construção está a cargo do próprio compilador em compile time, não sendo uma característica da linguagem.

Todavia, o rio não encontra a sua foz neste ponto. Apesar de esta funcionalidade não ser permitida, existe uma forma de a simular. Compiladores e dialectos mais recentes, como o Delphi e o Free Pascal, permitem a passagem de arrays de argumentos, a qual tem por princípio a passagem de arrays abertos, conceito que será brevemente revisto.

Todo o código presente neste artigo foi compilado recorrendo ao Free Pascal Compiler, versão 2.6.2, em ambiente Windows®, sendo totalmente portável para outras plataformas.

Passagem de open arrays

Quando o Pascal foi dado a conhecer ao mundo em 1971, era obrigatória a definição de tipos de dados com a palavra reservada type para se poder passar um array como argumento. Mais ainda, este array era estático, com uma dimensão bem definida, uma vez que na altura não fora ainda introduzido o conceito de array dinâmico.

Com o evoluir da linguagem, este conceito foi implementado, e pela primeira vez os arrays não necessitavam de ter uma dimensão bem definida – esta podia ser controlada em runtime. Dadas as suas características, tornou-se uma forma de criar o equivalente a pequenas listas ligadas com muito maior segurança uma vez que a gestão de recursos não fica a cargo do programador.

Da mesma forma, deixou de ser obrigatória a criação de tipos de dados para a passagem de arrays em argumentos. Se antes era necessário um tipo para cada dimensão, e um procedimento para cada tipo, o trabalho do programador fora imensamente facilitado.

Por exemplo, pode-se receber um array de números inteiros de pequena dimensão e sem sinal da seguinte forma:

function Average(list : array of byte) : real;

Recorrendo às funções Low e High, e mais recentemente com a estrutura de repetição for-in, torna-se simples iterar os elementos do array.

function Average(list : array of byte) : real
var i : byte;
begin
    Average := 0.0;
    for i:=Low(list) to High(list) do
        Inc(Average, list[i]);
    Average := Average / Length(list);
end;

Outra característica de elevado interesse é o facto de se poder passar parcialmente um array. Vejamos o seguinte exemplo:

var xs : array[1..20] of byte;
// ...
writeln(Average(xs[10..20]):0:3);

Neste caso, apenas é do nosso interesse calcular a média da segunda metade do array xs. Portanto, definimo-lo com a sintaxe xs[10..20]. Desta forma, apenas os elementos do índice 10 ao 20, inclusive, são passados à função Average.

Todas estas funcionalidades compõe aquilo a que se denomina de passagem de arrays abertos (ou, mais correctamente em inglês, open arrays): a possibilidade de receber um array sem conhecimento prévio da sua dimensão, bem como a possibilidade de realizar a passagem parcial de um array.

Passagem de um array de argumentos

A evolução da passagem de arrays abertos levou naturalmente à implementação de uma funcionalidade útil: o array of const.

Classicamente, os elementos de um array são de um tipo bem definido. No entanto, const é uma palavra reservada que define constantes. Desta forma, um array of const define algo diferente: é um array cujos elementos são constantes de tipo indefinido.

Como primeira nota, há que referir que estruturas não podem ser passadas nestes arrays. Apenas tipos de dados simples (tipos numéricos, alfanuméricos e apontadores), objectos, classes e interfaces podem.

Vamos criar um procedimento que recebe um array of const e nos dê informações acerca dos argumentos recebidos:

procedure Foo(args : array of const);

Quando o procedimento recebe este array, ocorre uma conversão dos seus elementos para um record variante com características particulares. Segundo a documentação do Free Pascal, esta é a sua definição:

Type
Ptrint = LongInt;
PVarRec = ^TVarRec;
TVarRec = record
  case VType : Ptrint of
    vtInteger :
      (VInteger: Longint);
    vtBoolean :
      (VBoolean: Boolean);
    vtChar :
      (VChar: Char);
    vtWideChar :
      (VWideChar: WideChar);
    vtExtended :
      (VExtended: PExtended);
    vtString :
      (VString: PShortString);
    vtPointer :
      (VPointer: Pointer);
    vtPChar :
      (VPChar: PChar);
    vtObject :
      (VObject: TObject);
    vtClass :
      (VClass: TClass);
    vtPWideChar :
      (VPWideChar: PWideChar);
    vtAnsiString :
      (VAnsiString: Pointer);
    vtCurrency :
      (VCurrency: PCurrency);
    vtVariant :
      (VVariant: PVariant);
    vtInterface :
      (VInterface: Pointer);
    vtWideString :
      (VWideString: Pointer);
    vtInt64 :
      (VInt64: PInt64);
    vtQWord :
      (VQWord: PQWord);
  end;

Portanto, cada elemento será um record no qual o campo VType indica qual o tipo de dados do argumento, e conforme o valor deste campo haverá um segundo que contém o valor do argumento. Por exemplo, se VType for vtString, então o argumento é do tipo ShortString (comummente designado apenas como string), e o seu valor pode ser acedido através do campo VString.

Uma vez que cada elemento pode ter um tamanho em bytes diferente dos restantes, o armazenamento dos dados referentes aos argumentos não pode ser feito da forma tradicional: um array clássico tem os seus elementos armazenados em áreas contíguas de memória em que cada elemento ocupa exactamente N bytes. Neste caso, cada argumento terá a sua localização num bloco da memória distinto. Isto explica o facto de haver um apontador (o tipo PVarRec).

Conclui-se que um array of const é, em última instância, um array of TVarRec dentro do procedimento ou função. Mas atenção, nunca se deve declarar o argumento desta forma!

procedure Foo(args : array of TVarRec);

Para implementar esta função, vamos primeiramente definir o seu objectivo:

  • Receber um array of const e devolver, no monitor, o tipo de dados de cada argumento, assim como o seu valor ou nome.

Para tal, será útil controlar quantos argumentos foram passados. Esta informação pode ser obtida com a função Length. Caso não haja argumentos, iremos mostrar a mensagem “Sem argumentos.”

procedure Foo(args : array of const);
var i : smallint;
begin
  if Length(args) > 0 then
    // análise dos argumentos
  else
    writeln('Sem argumentos.');
end;

Para proceder à análise dos argumentos, iremos iterar os elementos do array. Sendo este um open array, não conhecemos a sua dimensão, pelo que necessitaremos das funções Low e High, as quais devolvem, respectivamente, os índices menor e maior.

for i:=Low(args) to High(args) do
  // análise dos argumentos

Não sendo objectivo do presente artigo fazer uma exploração exaustiva de todos os tipos de dados possíveis de serem passados no array of const, iremos tratar apenas os mais comuns e alguns que possuem algumas particularidades.

Como foi referido, os elementos do array são convertidos ao record infra-apresentado. Pelo que possuem campos. Naturalmente, uma estrutura de decisão case-of irá analisar o valor do campo vtype, informando acerca do tipo de dados do argumento. Caso não seja conhecido, mostrar-se-á a mensagem “desconhecido”. Comecemos por analisar um caso simples: o argumento é um Integer. Desta forma, vtype irá assumir o valor vtInteger (uma constante do tipo LongInt):

case args[i].vtype of
  vtinteger :
    writeln('Integer = ',
    args[i].vinteger);
else
   writeln('desconhecido!');
end

Sendo um Integer, o campo variante vinteger possui o valor deste argumento. Notificamos acerca do tipo de dados do argumento, seguido do valor. O mesmo se aplica aos tipos de dados Boolean e Char.

No entanto, alguns tipos de dados necessitam de uma forma diferente de aceder ao valor. Alguns campos variantes são, na verdade, apontadores (o seu tipo de dados começa por P, como por exemplo PShortString). Para alguns destes necessitamos de recorrer ao operador ^, o qual nos indica o valor armazenado no ponteiro que lhe fornecemos (apontador^). Para outros casos, necessitaremos de fazer type casting uma vez que os seus tipos de dados permitem a recepção de um apontador, devolvendo automaticamente o valor lá armazenado (por exemplo, o tipo de dados AnsiString).

Comecemos por analisar o tipo de dados Extended:

vtextended :
  writeln('Extended = ',
  args[i].VExtended^);

Este é um caso simples. No entanto, as strings têm algumas diferenças. No Pascal moderno (leia-se Free Pascal, Object Pascal e Delphi), não existe apenas um tipo de dados string. O tipo de dados string apareceu após o aparecimento do Pascal, e apenas podia armazenar 255 caracteres. Hoje em dia, existem vários tipos de dados da família da string, sendo os mais proeminentes os seguintes:

  • ShortString – apenas permite 255 caracteres;
  • AnsiString – é null-terminated e não tem limite de caracteres;
  • WideString – semelhante ao AnsiString, cada caracter ocupa 2 bytes ao invés de apenas 1, e permite armazenar caracteres no formato UTF-16.

Desta forma, uma ShortString é o equivalente a um array of char com 255 elementos. Naturalmente, o argumento será um ponteiro para a localização deste array. Internamente, o compilador trata as strings de forma automática, não sendo, portanto, responsabilidade do programador determinar onde esta começa e termina (isto difere do C, por exemplo). Bastará, portanto, invocar o valor do apontador, e ser-nos-á devolvido o conteúdo da ShortString:

vtString :
  writeln('ShortString = ',
  args[i].VString^);

O tipo AnsiString necessita de ser tratado com um processo ligeiramente diferente. Não tendo um comprimento máximo, não basta recorrer ao operador ^. Na verdade, este nem sequer é utilizado uma vez que o tipo de dados do campo VAnsiString é Pointer. Como foi dito, o compilador trata deste processo automaticamente, e sempre que necessário fornece ao próprio programa ferramentas para gerir estas strings de forma autónoma. Portanto, bastará neste caso realizar type casting – o programa irá aceder à localização na memória onde a AnsiString está armazenada, e automaticamente vai processá-la:

vtAnsiString :
  writeln('AnsiString = ',
  AnsiString(Args[i].VAnsiString));

Para apontadores, e tendo em conta a quantidade de endereços que actualmente uma memória RAM possui, fazemos type casting para o tipo LongInt:

vtPointer :
  writeln('Pointer = ',
  Longint(Args[i].VPointer));

Para finalizar, iremos analisar as classes e os objectos. Estes não possuem um valor intrínseco dado o facto de não serem tipos de dados simples. Apesar disso, é possível passar um objecto ou uma classe como argumento num array of const. Ambos os tipos de dados TObject e TClass possuem uma propriedade denominada ClassName. Desta forma, o nosso objectivo neste procedimento é determinar o nome da classe ou objecto a que pertence o nosso argumento:

vtObject :
  writeln('Object = ',
  Args[i].VObject.Classname);
vtClass :
  writeln('Class reference = ',
  Args[i].VClass.Classname);

O nosso procedimento Foo terá então este aspecto:

procedure Foo(args : array of const);
var i : smallint;
begin
  if Length(args) > 0 then
    for i:=Low(args) to High(args) do
      case args[i].vtype of
        vtinteger :
          writeln('Integer = ',
          args[i].vinteger);
        vtboolean : 
          writeln('Boolean = ',
          args[i].vboolean);
        vtchar :
          writeln('Char = ',
          args[i].vchar);
        vtextended :
          writeln('Extended = ',
          args[i].VExtended^:0:10);
        vtString :
          writeln('ShortString = ',
          args[i].VString^);
        vtAnsiString :
          writeln('AnsiString = ',
          AnsiString(Args[i].VAnsiString));
        vtPointer :
          writeln('Pointer = ',
          Longint(Args[i].VPointer));
        vtObject :
          writeln('Object = ',
          Args[i].VObject.Classname);
        vtClass :
          writeln('Class = ',
          Args[i].VClass.Classname);
      else
        writeln('desconhecido!');
      end
  else
    writeln('Sem argumentos.');
  writeln;
end;

Ensaio com os arrays de argumentos

Coloquemos o procedimento Foo numa unit denominada ArgMgr (de Argument Manager). Para a testar, iremos importar a unit classes para podermos testar a passagem de classes e objectos. Note-se que é necessária a compiler directive {$mode objfpc} para permitir o seu uso.

{$mode objfpc}
program artigo45;
uses ArgMgr, classes;

var s : string = 'variavel s';
    sl : TStringList;

begin
  s := TStringList.Create;
  Foo([]);
  Foo(['Igor Nunes', 31]);
  Foo([@Foo, NIL, false, 'K']);
  Foo([s, sl, 3.14]);
  s.Free;
end.

Analisemos o output do programa:

Sem argumentos.

AnsiString = Igor Nunes
Integer = 31

Pointer = 471399
Pointer = 0
Boolean = FALSE
Char = K

ShortString = variavel s
Class = TStringList
Extended = 3.1400000000

Repare-se que a variável s, do tipo string, foi considerada uma ShortString. Isto acontece uma vez que, por defeito, o Free Pascal considera que o tipo de dados string se refere ao tipo ShortString.

Por outro lado, uma string escrita directamente no argumento é considerada uma AnsiString. Mais uma vez, o Free Pascal assume que uma string escrita directamente como argumento é deste tipo, uma vez que, segundo as actuais regras, estas strings não têm um tamanho definido e só podem assumir caracteres ASCII por defeito.

Como última referência, é de notar o output para a variável sl: o procedimento informou-nos que esta é uma classe, mais propriamente a classe TStringList.

printf – criação de bindings com o C

O Free Pascal oferece uma utilidade que outros compiladores, incluindo o próprio Delphi, não oferecem do mesmo modo. Com os arrays of const, é possível criar facilmente um binding com a função printf da linguagem C. O seguinte código indica como se deve declarar a função standard da linguagem C, exemplificando de seguida o seu uso:

{$mode objfpc}
program artigo45;

// Declaração standard da função
procedure printf(fmt : pchar;
          args : array of const);
cdecl; external 'c';

begin
  printf('%s igual a %d.',
         ['Dobro de 3', 6]);
end.

Concluímos assim a nossa viagem pelos arrays de argumentos, comummente designados apenas por arrays of const, os quais nos permitem mimetizar procedimentos e funções variádicos. Não é, decerto, uma das ferramentas mais utilizadas pelos programadores de Free Pascal, Object Pascal e Delphi. Todavia, é um instrumento útil que está à disposição, o qual fornece imensa flexibilidade na passagem de argumentos a procedimentos e funções.

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