Programação genérica

Em resumo, a programação genérica permite definir algoritmos, sem concretizar no momento da sua definição, os tipos de dados sobre os quais operam.

A linguagem C dispõe dos seguintes mecanismos para realizar esse tipo de programação:

  • Ponteiro *void

  • Ponteiro para função

  • Função com número de parâmetros variável

Ponteiro *void

Não existem objetos do tipo void mas existem objetos do tipo void *. São ponteiros para objetos sem tipo definido. Utilizam-se para programar algoritmos do modo independente do tipo de dados concreto.

As funcões memcpy e memmove são exemplos destes casos. Servem para copiar objetos tendo como referência os ponteiros para esses objetos. Como, para efeitos de cópia, a diferença entre os objetos é apenas a quantidade de memória que ocupam, o terceiro parâmetro – count – serve para fornecer essa informação. Em termos algoritmicos estas funções operam como se um array de bytes de tratasse. O primeiro parâmetro recebe o endereço de destino, o segundo o endereço fonte e o terceiro a quantidade de bytes a copiar.

A diferença de funcionamento entre estas funções deve-se a razões de eficiencia. A função memmove acautela a eventual sobreposição das zonas de memória em causa e por isso é menos eficiente que memcopy. A função memcpy só deve ser usada se as zonas de memória em causa não tiverem zonas de interseção.

void *memcpy(void *dest, const void *src, size_t count);

void *memmove(void *dest, const void *src, size_t count);

Exemplo

Deslocar cada elemento de um array para a posição seguinte. O elemento da posição 0 é copiado para a posição 1; o elemento da posição 1 é copiado para a posição 2 e assim sucessivamente até à penúltima posição. O último elemento é descartado.

Os elementos do array, desde a posição 1 até à última, são copiados como um bloco de bytes. Escolhe-se a função memmove porque as zonas de memória em causa não são disjuntas. A dimensão deste bloco é calculada pela expressão sizeof array - sizeof array[0] – a dimensão de array menos a dimensão de um elemento.

Listagem 72 Utilização de memmove para deslocar elementos num array
 1#include <stdlib.h>
 2#include <string.h>
 3#include <stdio.h>
 4
 5#define ARRAY_SIZE(a)	(sizeof(a) / sizeof(a[0]))
 6
 7int array[] = { 20, 4, 6, 2, 325, 24, 45};
 8
 9int main() {
10	for (int i = 0; i < ARRAY_SIZE(array); ++i)
11		printf("[%d] - %d\n", i, array[i]);
12	putchar('\n');
13
14	memmove(array + 1, array, sizeof array - sizeof array[0]);
15
16	for (int i = 0; i < ARRAY_SIZE(array); ++i)
17		printf("[%d] - %d\n", i, array[i]);
18}

Ponteiro para função

Um ponteiro para função pode ser afetado a uma variável, passado como argumento ou devolvido como valor de função. É utilizado para invocar uma operação de forma indireta. Exemplos de aplicação deste método são as funções de biblioteca qsort e bsearch, em que se parametriza o critério de comparação dos elementos de um array com vista à ordenação ou à procura.

Nestas funções é também aplicado um método de definição de array genérico com base em três componentes: ponteiro inicial; número de elementos; dimensão dos elementos.

void qsort( void* ptr, size_t count, size_t size,
            int (*comp)(const void*, const void*) );

void* bsearch( const void *key, const void *ptr, size_t count, size_t size,
               int (*comp)(const void*, const void*) );

Exemplo

No exemplo da Listagem 73 a função qsort é utilizada para ordenar um array de inteiros. O critério de comparação é programado na função específica, cmp_int cujo ponteiro é passado como último argumento na invocação de qsort.

Esta função é invocada em callback sempre que o algoritmo determina a comparação de elementos do array, recebemdo como argumentos os ponteiros para as posições a comparar.

A função de comparação é programada no contexto do conhecimento do tipo de dados em causa. Os seus parâmetros formais, do tipo void *, são convertidos explicitamente para ponteiros para o tipo dos elementos do array. No exemplo, os parâmetros a e b são convertidos para int * nas linhas 10 e 12.

Listagem 73 Ordenação de array de inteiros utilizando qsort
 1#include <stdlib.h>
 2#include <string.h>
 3#include <stdio.h>
 4
 5#define ARRAY_SIZE(a)	(sizeof(a) / sizeof(a[0]))
 6
 7int array[] = { 20, 4, 6, 2, 325, 24, 45};
 8
 9int cmp_int(const void *a, const void *b) {
10	if (*(int *)a > *(int*) b)
11		return 1;
12	else if (*(int *)a < *(int*) b)
13		return -1;
14	return 0;
15}
16
17int main() {
18	for (int i = 0; i < ARRAY_SIZE(array); ++i)
19		printf("%d\n", array[i]);
20	putchar('\n');
21	qsort(array, ARRAY_SIZE(array), sizeof(array[0]), cmp_int);
22
23	for (int i = 0; i < ARRAY_SIZE(array); ++i)
24		printf("%d\n", array[i]);
25}

Exercício

Modifique o programa da Listagem 73 de modo a exemplificar a ordenação de um array de ponteiros para strings pelo critério da ordem alfabética do conteúdo das strings.

Função com número de parâmetros variável

A função com número de parâmetros variável mais conhecida é a função printf, com a seguinte assinatura:

int printf(const char *format, ...);

As reticências significam que a função pode ser invocada com qualquer número e tipo de argumentos, depois do argumento do parâmetro format.

Como é que a função printf «sabe» quantos e de que tipo são os argumentos que lhe foram passados numa dada invocação? É pela análise da string passada no argumento format. As definições de conversão marcadas com %, caraterizam os retantes argumento a processar, em número e em tipo.

O acesso aos argumentos da zona variável faz-se segundo o seguinte modelo:
  • os argumentos constituem uma sequência;

  • a macro va_start inicializa um ponteiro para o inìcio da sequência tomando como referência o último parâmetro fixo;

  • o valor dos argumentos é obtido através da macro va_arg;

  • a macro va_arg além de extrair o valor do argumento corrente avança para o próximo argumento.

Exemplo

../_images/va_arg.svg
Listagem 74 Função que adiciona um número variável de valores.
 1#include <stdarg.h>
 2
 3int sum(int n, ...) {
 4	if (n <= 0)
 5		return 0;
 6	va_list ap;
 7
 8	va_start(ap, n);
 9	int result = va_arg(ap, int);
10	for (int i = 1; i < n; ++i)
11		 result += va_arg(ap, int);
12	va_end(ap);
13	return result;
14}
15
16int main() {
17	int a = sum(2, 10, 10);
18	int b = sum(3, 10, 20, 30);
19}

A função sum realiza a adição dos valores de uma sequência variável de valores inteiros. A dimensão da sequência é indicado pelo parâmetro n.

Na linha 8, a variável ap é inicializada por va_start(ap, n) tomando n como referência.

Na linha 11, cada um dos valores a adicionar é obtido por va_arg(ap, int). ap é avançado para a posição seguinte. O argumento int indica o tipo do valor a extarir.

A título ilustrativo, apresenta-se uma definição simplificada (e imprecisa) das macros va_start e va_arg:

#define va_start(ap, fixed)   ap = &fixed + 1

#define va_arg(ap, type)      *((type) *)ap++)

No final, a macro va_end deve ser invocada, após o que, o ponteiro ap fica indefinido.

Exemplo

Outro exemplo bem ilustrativo do funcionamento dos mecanismos de processamento de lista de parâmetros variável é a programação da função printf. Da qual se apresenta uma versão reduzida da variante snprintf.

Destaca-se a invocação da macro va_arg nas linhas 23, 33, 43 e 52. O tipo do valor a extrair, indicado como segundo argumento, depende do especicicador %. Este argumento determina também a amplitude do avanço de ap.

Listagem 75 Implementação reduzida de snprintf.
 1#include <stdarg.h>
 2#include <stdlib.h>
 3#include <string.h>
 4
 5static size_t int_to_string(unsigned value, int base, char buffer[], size_t buffer_size);
 6
 7int mini_snprintf(char *buffer, size_t buffer_size, const char *fmt, ...) {
 8	if (buffer_size == 0)
 9		return 0;
10	va_list ap;
11	va_start(ap, fmt);
12	char *str = buffer;
13	size_t str_size = 0;
14	for (const char *p = fmt; *p && buffer_size > 1; p++) {
15		if (*p != '%') {
16			*str++ = *p;
17			str_size++;
18			buffer_size--;
19			continue;
20		}
21		switch (*++p) {
22			case 'd': {
23				int ival = va_arg(ap, int);
24				size_t nbytes = int_to_string(ival, 10, str, buffer_size);
25				if (nbytes == 0)
26					goto ret;
27				str_size += nbytes;
28				str += nbytes;
29				buffer_size -= nbytes;
30				break;
31			}
32			case 'x': {
33				int ival = va_arg(ap, int);
34				size_t nbytes = int_to_string(ival, 16, str, buffer_size);
35				if (nbytes == 0)
36					goto ret;
37				str_size += nbytes;
38				str += nbytes;
39				buffer_size -= nbytes;
40				break;
41			}
42			case 'c': {
43				char cval = va_arg(ap, int);
44				if (buffer_size < 2)
45					goto ret;
46				*str++ = cval;
47				str_size++;
48				buffer_size--;
49				break;
50			}
51			case 's': {
52				char *sval = va_arg(ap, char *);
53				size_t str_len = strlen(sval);
54				if (buffer_size < str_len + 1)
55					goto ret;
56				strcpy(str, sval);
57				str += str_len;
58				str_size += str_len;
59				buffer_size -= str_len;
60				break;
61			}
62			default:
63				*str++ = *p;
64				str_size++;
65				buffer_size--;
66		}
67	}
68ret:
69	*str = '\0';
70	va_end(ap);
71	return str_size;
72}

Variante vprintf

A lista de argumentos variável pode ser propagada como argumento de outras funções sem ser processada localmente.

Para que isso aconteça, é necessário que a função invocada receba como parâmetro fixo um ponteiro do tipo va_list.

As variantes de printf prefixadas com «v» são casos de funções que recebem uma lista de parâmetros variável pertencente a outra função.

int vprintf(const char* format, va_list vlist);

Exemplo

A função trace escreve em stdout uma string de informação etiquetada com uma referência de tempo. Depois de escrever a indicação de tempo – linha 11 – invoca vprintf para escrever a informação, passando a lista de argumentos variável através do parâmetro vlist.

Listagem 76 Registo de informação com etiqueta de tempo.
 1#include <stdio.h>
 2#include <stdarg.h>
 3#include <time.h>
 4
 5void trace(const char *format, ...) {
 6	struct timespec time;
 7	timespec_get(&time, TIME_UTC);
 8
 9	va_list ap;
10	va_start(ap, format);
11	printf("[%ld.%ld] ", time.tv_sec, time.tv_nsec / 1000000);
12	vprintf(format, ap);
13	putchar('\n');
14	va_end(ap);
15}
16
17
18int main() {
19	trace("Informação = %d", 123);
20}

Referências

The C Programming Language, secçãos 5.11, secção 7.3